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/App.css b/src/App.css index 03377a920cda0f5024f535531200e82d0d749ebf..eaffbecc078dbbab291e0e31af08e3fed901e571 100644 --- a/src/App.css +++ b/src/App.css @@ -20,74 +20,3 @@ .App-link { color: #1315c4; } - -.Observations-header { - background-color: #282c34; - min-height: 100vh; - display: flex; - flex-direction: column; - align-items: flex-start; - justify-content: inherit; - font-size: calc(10px + 2vmin); - color: white; -} - -.card-img-top { - width: 30vh; - height: 15vw; - object-fit: cover; - background-color: darkgrey; -} - -.card-datasource { - display: flex; - object-fit: cover; - background-color: #d4d4d4; -} - -.card-description { - display: flex; - - //background-color: #BBDEFB; - font-family: Raleway; - //font-size: 14pt; -} - -.card-telescope { - display: flex; - width: 40vh; - height: 35vw; -} - -.card-query { - display: inline; - align-items: flex-start; - justify-content: inherit; - background-color: lightgrey; -} - -.key { - font-weight: bold; -} - -.value { - font-style: italic; -} - -@media (min-width:400px) { - .card-columns { - column-count: 1; - } -} - -@media (min-width:800px) { - .card-columns { - column-count: 2; - } -} - -@media (min-width:1200px) { - .card-columns { - column-count: 3; - } -} diff --git a/src/App.js b/src/App.js index 147e3a98a9d20e76780052f4694a573e85a37813..c462317b967f19164906133fc16492611255fa21 100644 --- a/src/App.js +++ b/src/App.js @@ -2,6 +2,7 @@ import React from "react"; import "./App.css"; import Routes from "./routes/Routes"; import { GlobalContextProvider } from "./contexts/GlobalContext"; +import { QueryContextProvider } from "./contexts/QueryContext"; // This is the App, only application global stuff goes here, like the global state provider. @@ -9,7 +10,9 @@ export default function App() { return ( <div> <GlobalContextProvider> - <Routes /> + <QueryContextProvider> + <Routes /> + </QueryContextProvider> </GlobalContextProvider> </div> ); diff --git a/src/components/Interactive.js b/src/components/Interactive.js index b3508f3beaf43e391098e289588ba751960d6a54..e219659de3e859639ca23cd1d4825e69099fceb4 100644 --- a/src/components/Interactive.js +++ b/src/components/Interactive.js @@ -3,6 +3,13 @@ import { Alert } from "react-bootstrap"; export default function Interactive() { return ( + // <div class="embed-responsive embed-responsive-16by9"> + // <iframe + // class="embed-responsive-item" + // src="http://130.246.212.44/escape/" + // allowfullscreen + // ></iframe> + // </div> <Alert variant="warning"> <p>You will leave ESAP GUI and be redirected to</p> <a diff --git a/src/components/NavBar.js b/src/components/NavBar.js index 5a32597e5769f6307a02c787966fb31223319563..10b442c39a2dc383051ac606a01c60382e16cb99 100644 --- a/src/components/NavBar.js +++ b/src/components/NavBar.js @@ -3,10 +3,10 @@ import { Navbar, Nav } from "react-bootstrap"; import { NavLink } from "react-router-dom"; import AuthControl from "./auth/authControl"; -import { GlobalContext } from "../contexts/GlobalContext"; +import { QueryContext } from "../contexts/QueryContext"; export default function NavBar() { - const { config } = useContext(GlobalContext); + const { config } = useContext(QueryContext); if (!config) return null; // construct the navigation bar based on the configuration const navlist = config.navbar; diff --git a/src/components/Rucio.js b/src/components/Rucio.js index 6dd2613edf49f20b5644027270190debe7f9ff91..9e28f718d3b9146559fb66ce4411a5b453f0c6f2 100644 --- a/src/components/Rucio.js +++ b/src/components/Rucio.js @@ -1,17 +1,14 @@ import React from "react"; -import { Alert } from "react-bootstrap"; export default function Rucio() { return ( - <Alert variant="warning"> - <p>You will leave ESAP GUI and be redirected to</p> - <a - target="_blank" - rel="noopener noreferrer" - href="https://escape-dios-dl.cern.ch/ui/" - > - Rucio Web UI - </a> - </Alert> + <div className="embed-responsive embed-responsive-16by9"> + <iframe + title="rucio" + className="embed-responsive-item" + src="https://escape-rucio.cern.ch" + allowFullScreen + ></iframe> + </div> ); } diff --git a/src/components/archives/ArchiveCard.js b/src/components/archives/ArchiveCard.js index a4dcad088c8fb0aab1c1c8058499e088fc440d92..76ac2ea4322496771790fcfbb1d40ef467f57393 100644 --- a/src/components/archives/ArchiveCard.js +++ b/src/components/archives/ArchiveCard.js @@ -1,16 +1,14 @@ import React from "react"; -import { Card, Container, Row, Button, Image } from "react-bootstrap"; +import { Card, Button, Image } from "react-bootstrap"; import { NavLink } from "react-router-dom"; // display a single archive on a card export default function ArchiveCard({ archive }) { return ( - <Card className="card-description"> + <Card> <Card.Body> <Card.Title className="h2">{archive.name}</Card.Title> - <Container fluid> - <Row> - <Card className="card-description"> + <Card className="card-description"> <Card.Body> <Image className="mx-auto d-block" @@ -21,19 +19,24 @@ export default function ArchiveCard({ archive }) { <Card.Title className="h3 pt-3">{archive.short_description}</Card.Title> <Card.Text className="text-justify">{archive.long_description}</Card.Text> </Card.Body> + </Card> + {/* <Container fluid> + <Row> + </Card> </Row> <Row className="p-2"> - <Button - className="mx-auto" + + </Row> + </Container> */} + <Button + className="mt-3" as={NavLink} variant="outline-info" to={`/archives/${archive.uri}`} > Visit {archive.name} Archives - </Button> - </Row> - </Container> + </Button> </Card.Body> </Card> ); diff --git a/src/components/archives/Archives.js b/src/components/archives/Archives.js index f2148c055ddd5cfab06cfe00ec15b63f65f5be7c..8bc97d45349ae7045c87acf346601c07b1935b07 100644 --- a/src/components/archives/Archives.js +++ b/src/components/archives/Archives.js @@ -1,5 +1,5 @@ import React, { useContext } from "react"; -import { CardColumns } from "react-bootstrap"; +import { Container, Row, Col } from "react-bootstrap"; import ArchiveCard from "./ArchiveCard"; import { GlobalContext } from "../../contexts/GlobalContext"; @@ -9,11 +9,13 @@ export function Archives() { console.log("archives: ", { archives }); return ( - <CardColumns className="row-2"> + <Container fluid> + <Row> {archives.map((archive) => { let key = "archive-" + archive.id; - return <ArchiveCard key={key} archive={archive} />; + return <Col><ArchiveCard key={key} archive={archive} /></Col>; })} - </CardColumns> + </Row> + </Container> ); } diff --git a/src/components/archives/DataProductCategories.js b/src/components/archives/DataProductCategories.js index d7f4d71f5be66d7bcf2a123e35784e95ad746f56..beb3cf92a1c894cb2d095d3c24b3631774c58837 100644 --- a/src/components/archives/DataProductCategories.js +++ b/src/components/archives/DataProductCategories.js @@ -1,12 +1,15 @@ import React, { useState, useContext, useEffect } from "react"; -import { NavLink } from "react-router-dom"; +import { useHistory } from "react-router-dom"; import { ListGroup, Card, Button, Row, Col } from "react-bootstrap"; import { GlobalContext } from "../../contexts/GlobalContext"; +import { QueryContext } from "../../contexts/QueryContext"; import axios from "axios"; export default function DataProductCategories({ archive }) { const { api_host } = useContext(GlobalContext); + const { setDPLevel, setCollection } = useContext(QueryContext); const [categories, setCategories] = useState([]); + const history = useHistory(); useEffect(() => { axios @@ -39,9 +42,18 @@ export default function DataProductCategories({ archive }) { let query_url = `${category.archive_uri_derived}/query`; console.log("query_url:", query_url); button = ( - <Button as={NavLink} variant="outline-info" to={query_url}> + // need to add level (e.g raw) and category (e.g imaging) infomation to send to the form + // probably need to define onSubmit instead of point to query_url + <Button + onClick={() => { + console.log('onClick', category) + setDPLevel(category.level); + setCollection(category.collection); + history.push(query_url);}} + > Browse Catalog & Run Queries </Button> + ); } else if (category.catalog_user_url_derived) { button = ( diff --git a/src/components/basket/addtobasket.js b/src/components/basket/addtobasket.js index 10408fd34cd4b902ff8ba7ffffbde840a81fb96b..93122ad910e7fc8e943f45758e9bae384f0d428d 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 } 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..5fbe63ae7cc35bbc4efc1710e2e4fb5a6e683a92 --- /dev/null +++ b/src/components/basket/savebasket.js @@ -0,0 +1,55 @@ +import React, { useContext } 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 } = 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/ASTRONVOResults.js b/src/components/query/ASTRONVOResults.js index 1a5735b385b6690444b403f2acf21d0739a5773a..76937c2d05845f7bc1f3bd89f15e83578a2d4005 100644 --- a/src/components/query/ASTRONVOResults.js +++ b/src/components/query/ASTRONVOResults.js @@ -3,16 +3,30 @@ import { Table, Alert } from "react-bootstrap"; import { QueryContext } from "../../contexts/QueryContext"; import LoadingSpinner from "../LoadingSpinner"; import Paginate from "../Paginate"; +import HandlePreview from "./HandlePreview"; +import Preview from "./Preview"; export default function ASTRONVOResults({ catalog }) { - const { queryMap } = useContext(QueryContext); + const { queryMap, page, setPage, preview } = useContext(QueryContext); if (!queryMap) return null; if (queryMap.get(catalog).status === "fetched") { + if (!("results" in queryMap.get(catalog).results)) + return <Alert variant="warning">{queryMap.get(catalog).results}</Alert>; if (queryMap.get(catalog).results.results.length === 0) return <Alert variant="warning">No matching results found!</Alert>; + + const numPages = queryMap.get(catalog).results.pages; console.log(queryMap.get(catalog).results.results); return ( <> + <Paginate + getNewPage={(args) => { + return args.target ? setPage(parseFloat(args.target.text)) : null; + }} + currentPage={page} + numAdjacent={3} + numPages={numPages} + /> <Table className="mt-3" responsive> <thead> <tr className="bg-light"> @@ -29,30 +43,45 @@ export default function ASTRONVOResults({ catalog }) { <th>Calibration Level</th> <th>Size</th> <th>Link to data</th> + <th></th> </tr> </thead> <tbody> {queryMap.get(catalog).results.results.map((result) => { return ( - <tr key={result.result}> - {/* <th> - <InputGroup> - <InputGroup.Checkbox /> - </InputGroup> - </th> */} - <td>{result.obs_collection}</td> - <td>{Number(result.ra).toFixed(1)}</td> - <td>{Number(result.dec).toFixed(1)}</td> - <td>{Number(result.fov).toFixed(1)}</td> - <td>{result.dataproduct_type}</td> - <td>{result.calibration_level}</td> - <td>{Number((result.size / 1024).toFixed(1))} MB</td> - <td> - <a href={result.url} rel="noopener noreferrer" download> - Download - </a> - </td> - </tr> + <> + <tr key={result.url}> + {/* <th> + <InputGroup> + <InputGroup.Checkbox /> + </InputGroup> + </th> */} + <td>{result.obs_collection}</td> + <td>{Number(result.ra).toFixed(1)}</td> + <td>{Number(result.dec).toFixed(1)}</td> + <td>{Number(result.fov).toFixed(1)}</td> + <td>{result.dataproduct_type}</td> + <td>{result.calibration_level}</td> + <td>{Number((result.size / 1024).toFixed(1))} MB</td> + <td> + <a href={result.url} rel="noopener noreferrer" download> + Download data + </a> + </td> + <td> + <HandlePreview result={result} /> + </td> + </tr> + {preview === result.url && + <tr key={result.url}> + <td></td> + <td></td> + <td></td> + <td></td> + <td colSpan="4" ><Preview /></td> + <td></td> + </tr>} + </> ); })} </tbody> diff --git a/src/components/query/ApertifResults.js b/src/components/query/ApertifResults.js index b360f12e6653e6da0080e96407dcd16fbfc4f24f..15b35243b35cd29cc5d0dd3d4439504bcd2481ac 100644 --- a/src/components/query/ApertifResults.js +++ b/src/components/query/ApertifResults.js @@ -3,16 +3,30 @@ import { Table, Alert } from "react-bootstrap"; import { QueryContext } from "../../contexts/QueryContext"; import LoadingSpinner from "../LoadingSpinner"; import Paginate from "../Paginate"; +import HandlePreview from "./HandlePreview"; +import Preview from "./Preview"; export default function ApertifResults({ catalog }) { - const { queryMap } = useContext(QueryContext); + const { queryMap, page, setPage, preview } = useContext(QueryContext); if (!queryMap) return null; if (queryMap.get(catalog).status === "fetched") { + if (!("results" in queryMap.get(catalog).results)) + return <Alert variant="warning">{queryMap.get(catalog).results}</Alert>; if (queryMap.get(catalog).results.results.length === 0) return <Alert variant="warning">No matching results found!</Alert>; + + const numPages = queryMap.get(catalog).results.pages; console.log("Query results:", queryMap.get(catalog).results.results); return ( <> + <Paginate + getNewPage={(args) => { + return args.target ? setPage(parseFloat(args.target.text)) : null; + }} + currentPage={page} + numAdjacent={3} + numPages={numPages} + /> <Table className="mt-3" responsive> <thead> <tr className="bg-light"> @@ -29,11 +43,13 @@ export default function ApertifResults({ catalog }) { <th>Data Product Type</th> <th>Data Product Subtype</th> <th>Link to data</th> + <th></th> </tr> </thead> <tbody> {queryMap.get(catalog).results.results.map((result) => { return ( + <> <tr key={result.PID}> {/* <th> <InputGroup> @@ -52,26 +68,29 @@ export default function ApertifResults({ catalog }) { href={result.url} target="_blank" rel="noopener noreferrer" + download > - View data + Download data </a> - {result.dataProductSubType === "continuumMF" ? ( - <a - href={result.thumbnail} - target="_blank" - rel="noopener noreferrer" - className="ml-3" - > - Thumbnail - </a> - ) : null} + </td> + <td> + <HandlePreview result={result} /> </td> </tr> + {preview === result.url && + <tr key={result.url}> + <td></td> + <td></td> + <td></td> + <td></td> + <td colSpan="4" ><Preview /></td> + <td></td> + </tr>} + </> ); })} </tbody> </Table> - {/* <Paginate /> */} </> ); } else { diff --git a/src/components/query/HandlePreview.js b/src/components/query/HandlePreview.js new file mode 100644 index 0000000000000000000000000000000000000000..04946bb3d282af85c5a94978c78df55ea596038b --- /dev/null +++ b/src/components/query/HandlePreview.js @@ -0,0 +1,34 @@ +import React, { useContext } from 'react'; +import { Button } from 'react-bootstrap'; +import { QueryContext } from '../../contexts/QueryContext'; + +export default function HandlePreview({ result }) { + const { preview, setPreview, setDS9, setURL } = useContext(QueryContext); + return ( + <> + {/* if results is in .fits format and is smaller than 10 MB, + display it with js9 */} + {((result.url.includes('fits') || (result.url.includes('FITS'))) && + Number(result.size) < 10000) ? + (<Button + onClick={() => { + preview ? setPreview("") : setPreview(result.url); + setURL(result.url); + setDS9(true); + }} + >View fits with DS9</Button>) : + (result.thumbnail && ( + <Button + onClick={()=>{ + preview ? setPreview("") : setPreview(result.url); + setURL(result.thumbnail); + }} + > + View Thumbnail + </Button> + )) + } + </> + ) +} + diff --git a/src/components/query/LOFARResults.js b/src/components/query/LOFARResults.js new file mode 100644 index 0000000000000000000000000000000000000000..0aaf6bd37cf93b60bf9d9fd277993681a2dfd05f --- /dev/null +++ b/src/components/query/LOFARResults.js @@ -0,0 +1,97 @@ +import React, { useContext } from "react"; +import { Table, Alert } from "react-bootstrap"; +import { QueryContext } from "../../contexts/QueryContext"; +import LoadingSpinner from "../LoadingSpinner"; + +export default function LOFARResults({ catalog }) { + const { queryMap } = useContext(QueryContext); + if (!queryMap) return null; + if (queryMap.get(catalog).status === "fetched") { + if (!("results" in queryMap.get(catalog).results)) + return <Alert variant="warning">{queryMap.get(catalog).results}</Alert>; + if (queryMap.get(catalog).results.results.length === 0) + return <Alert variant="warning">No matching results found!</Alert>; + + //const numPages = queryMap.get(catalog).results.pages; + console.log("Query results:", queryMap.get(catalog).results.results); + return ( + <> + {/* <Paginate + getNewPage={(args) => { + return args.target ? setPage(parseFloat(args.target.text)) : null; + }} + currentPage={page} + numAdjacent={3} + numPages={numPages} + /> */} + <Table className="mt-3" responsive> + <thead> + <tr className="bg-light"> + {/* <th> + <InputGroup> + <InputGroup.Checkbox /> + </InputGroup> + </th> */} + <th>Project</th> + <th>SAS ID</th> + <th>Target Name</th> + <th>RA</th> + <th>Dec</th> + <th>Release Date</th> + <th>Start Time</th> + <th>Duration</th> + <th>Pipeline</th> + <th>Antenna Set</th> + <th>Instrument Filter</th> + </tr> + </thead> + <tbody> + {queryMap.get(catalog).results.results.map((result) => { + return ( + <tr key={result.PID}> + {/* <th> + <InputGroup> + <InputGroup.Checkbox /> + </InputGroup> + </th> */} + <td>{result.project}</td> + <td>{result.sas_id}</td> + <td>{result.target}</td> + <td>{Number(result.ra).toFixed(1)}</td> + <td>{Number(result.dec).toFixed(1)}</td> + <td>{result.releaseDate}</td> + <td>{result.startTime}</td> + <td>{result.duration}</td> + <td>{result.pipeline}</td> + <td>{result.antennaSet}</td> + <td>{result.instrumentFilter}</td> + {/* <td> + <a + href={result.url} + target="_blank" + rel="noopener noreferrer" + > + View data + </a> + {result.dataProductSubType === "continuumMF" ? ( + <a + href={result.thumbnail} + target="_blank" + rel="noopener noreferrer" + className="ml-3" + > + Thumbnail + </a> + ) : null} + </td> */} + </tr> + ); + })} + </tbody> + </Table> + </> + ); + } else { + return <LoadingSpinner />; + } +} diff --git a/src/components/query/Preview.js b/src/components/query/Preview.js new file mode 100644 index 0000000000000000000000000000000000000000..84896fd86620a12333644ec9ffa947e9eff198dd --- /dev/null +++ b/src/components/query/Preview.js @@ -0,0 +1,32 @@ +import React, { useContext } from 'react'; +import { Image } from 'react-bootstrap'; +import { QueryContext } from '../../contexts/QueryContext'; + +export default function Preview() { + const { preview, ds9, url } = useContext(QueryContext); + return ( + <> + {console.log("preview: ", preview)} + {console.log("url: ", url)} + {(preview && + (ds9 ? + <iframe + title="DS9" + className="embed-responsive-item" + height="700" + width="700" + src={"https://js9.si.edu/js9/js9.html?url="+JSON.parse(JSON.stringify(url))+"&colormap=viridis&scale=log"} + allowFullScreen + ></iframe> + : + <Image + width={700} + className={"mt-3"} + src={url} + alt="" + />) + )} + </> + ) +} + diff --git a/src/components/query/QueryADEX.js b/src/components/query/QueryADEX.js new file mode 100644 index 0000000000000000000000000000000000000000..96243532d47e28f408aa03835d7851d2a0f477ff --- /dev/null +++ b/src/components/query/QueryADEX.js @@ -0,0 +1,152 @@ +import React, { useContext, useEffect } from "react"; +import { useParams, useHistory } from "react-router-dom"; +import axios from "axios"; +import { Container } from "react-bootstrap"; +import Form from "react-jsonschema-form"; +import { GlobalContext } from "../../contexts/GlobalContext"; +import { QueryContext } from "../../contexts/QueryContext"; +import QueryResults from "./QueryResults"; +import parseQueryForm from "../../utils/form/parseQueryForm"; + +export default function QueryCatalogs() { + // queryMap is a map of dictionaries, where each dictionary consists of + // {"catalog": "catalogname", + // "catalogquery": "querystring", + // "status": "fetching|fechted", + // "results": null} + const { config, setConfigName, defaultConf, queryMap, formData, setFormData, page } = useContext(QueryContext); + const { api_host } = useContext( + GlobalContext + ); + const { uri } = useParams(); + const history = useHistory(); + console.log("uri:", uri); + console.log("default conf:", defaultConf); + + // set ConfigName for archive + useEffect(() => { + switch (uri) { + case "adex": + setConfigName("adex"); + break; + case "apertif": + setConfigName("apertif"); + break; + case "zooniverse": + setConfigName("zooniverse"); + break; + case "astron_vo": + setConfigName("astron_vo"); + break; + case "lofar": + setConfigName("lofar"); + break; + } + return () => { + console.log("cleaned up"); + queryMap.clear(); + setFormData(); + setConfigName(uri); + }; + }, [uri]); + + useEffect(() => { + console.log(config.query_schema); + if (!formData) return; + const gui = config.query_schema.name; + const queries = parseQueryForm(gui, formData, page); + + // Ideally query for each catalog is sent to ESAP API Gateway, and query results is returned + // This is under development in the backend at the moment + queryMap.clear(); + queries.forEach((query) => { + queryMap.set(query.catalog, { + catalog: query.catalog, + esapquery: query.esapquery, + status: "fetching", + results: null, + }); + const url = api_host + "query/query/?" + query.esapquery; + axios + .get(url) + .then((queryResponse) => { + queryMap.set(query.catalog, { + catalog: query.catalog, + esapquery: query.esapquery, + status: "fetched", + results: queryResponse.data, + }); + }) + .catch(() => { + queryMap.set(query.catalog, { + catalog: query.catalog, + esapquery: query.esapquery, + status: "error", + results: null, + }); + }); + }); + }, [formData, page]); + + function formTemplate({ TitleField, properties, title, description }) { + return ( + <div> + <TitleField title={title} /> + <div className="row"> + {properties.map((prop) => ( + <div + className="col-lg-2 col-md-4 col-sm-6 col-xs-12" + key={prop.content.key} + > + {prop.content} + </div> + ))} + </div> + {description} + </div> + ); + } + + console.log("queryMap", Array.from(queryMap.values())); + + const uiSchemaProp = config.ui_schema ? { uiSchema: config.ui_schema } : {}; + return ( + <Container fluid> + <Form + schema={config.query_schema} + ObjectFieldTemplate={formTemplate} + formData={formData} + onBlur={(field, value) => { + if (field == "root_catalog") { + console.log("Change query catalog to : ", value); + if (value == "adex") { + history.push("/query"); + } else { + history.push("/archives/" + value + "/query"); + } + } + }} + onSubmit={({ formData }) => setFormData(formData)} + {...uiSchemaProp} + ></Form> + {Array.from(queryMap.keys()).map((catalog) => { + console.log("catalog:", catalog); + const details = queryMap.get(catalog); + console.log("Details:", details); + console.log("Results:", details.results); + let catalogName = + config.query_schema.properties.catalog.enumNames[ + config.query_schema.properties.catalog.enum.findIndex( + (name) => name === catalog + ) + ]; + return ( + <div key={catalog} className="mt-3"> + <h4>Query results for {catalogName}</h4> + <QueryResults catalog={catalog} /> + </div> + ); + })} + </Container> + ); +} diff --git a/src/components/query/QueryCatalogs.js b/src/components/query/QueryCatalogs.js index 913d2aabf0450eb7cbfba1350a6b2bd375782360..4fa5fc8732722480e6d356978f272e6fc55a5109 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"; @@ -14,10 +15,13 @@ export default function QueryCatalogs() { // "catalogquery": "querystring", // "status": "fetching|fechted", // "results": null} - const { queryMap, formData, setFormData, page } = useContext(QueryContext); - const { config, api_host, setConfigName } = useContext(GlobalContext); + const { config, setConfigName, defaultConf, queryMap, formData, setFormData, page } = useContext(QueryContext); + const { api_host } = useContext( + GlobalContext + ); const { uri } = useParams(); - console.log(uri); + console.log("uri:", uri); + console.log("default conf:", defaultConf); // set ConfigName for archive useEffect(() => { @@ -31,6 +35,9 @@ export default function QueryCatalogs() { case "zooniverse": setConfigName("zooniverse"); break; + case "esap_rucio": + setConfigName("esap_rucio"); + break; case "astron_vo": setConfigName("astron_vo"); break; @@ -38,11 +45,13 @@ export default function QueryCatalogs() { setConfigName("lofar"); break; default: - setConfigName("esap_ivoa"); + break; } return () => { console.log("cleaned up"); - setConfigName("esap_ivoa"); + queryMap.clear(); + setFormData(); + setConfigName(defaultConf); }; }, [uri]); @@ -129,7 +138,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/QueryIVOARegistry.js b/src/components/query/QueryIVOARegistry.js index cf1101a5d9266a9e42c8ac3a5eb7ca5e00351250..677b69c452fd27f2ecfb61d55aa0643647513cc4 100644 --- a/src/components/query/QueryIVOARegistry.js +++ b/src/components/query/QueryIVOARegistry.js @@ -17,11 +17,16 @@ export default function QueryIVOARegistry() { // "catalogquery": "querystring", // "status": "fetching|fechted", // "results": null} - const { queryMap, formData, setFormData } = useContext(QueryContext); - const { config, api_host, setConfigName } = useContext(GlobalContext); - const { selectedRegistry, queryStep, setQueryStep } = useContext(IVOAContext); + const { config, setConfigName, defaultConf, queryMap, formData, setFormData, page } = useContext(QueryContext); + const { api_host } = useContext( + GlobalContext + ); + const { selectedRegistry, queryStep, setQueryStep, regPage } = useContext( + IVOAContext + ); const { uri } = useParams(); console.log("uri:", uri); + console.log("default conf: ", defaultConf); // set ConfigName for archive useEffect(() => { @@ -46,7 +51,7 @@ export default function QueryIVOARegistry() { } return () => { console.log("Set configuration back to default!"); - setConfigName("esap_ivoa"); + setConfigName(defaultConf); }; }, [uri]); @@ -59,16 +64,17 @@ export default function QueryIVOARegistry() { if (queryStep === "run-query") { selectedRegistry.forEach((access_url) => { - queries = [...queries, ...parseVOServiceForm(formData, access_url)]; + queries = [ + ...queries, + ...parseVOServiceForm(formData, access_url, page), + ]; }); } else { - queries = parseQueryForm(gui, formData); + queries = parseQueryForm(gui, formData, regPage); } console.log("queries:", queries); - // Ideally query for each catalog is sent to ESAP API Gateway, and query results is returned - // This is under development in the backend at the moment queryMap.clear(); queries.forEach((query) => { queryMap.set(query.catalog, { @@ -82,25 +88,43 @@ export default function QueryIVOARegistry() { axios .get(url) .then((queryResponse) => { - queryMap.set(query.catalog, { - catalog: query.catalog, - service_type: query.service_type, - esapquery: query.esapquery, - status: "fetched", - results: queryResponse.data, - }); + if(queryStep === "run-query") { + let tf_url = api_host + "query/get-tables-fields/?dataset_uri=vo_reg&access_url=" + query.catalog; + console.log("table fields url: ", tf_url); + axios + .get(tf_url) + .then((tfResponse) => { + queryMap.set(query.catalog, { + catalog: query.catalog, + service_type: query.service_type, + vo_table_schema: tfResponse.data.results.find((item) => item.table_name === "ivoa.obscore"), + esapquery: query.esapquery, + status: "fetched", + results: queryResponse.data, + }); + }) + } + else { + queryMap.set(query.catalog, { + catalog: query.catalog, + service_type: query.service_type, + esapquery: query.esapquery, + status: "fetched", + results: queryResponse.data, + })}; }) .catch(() => { queryMap.set(query.catalog, { catalog: query.catalog, service_type: query.service_type, + vo_table_schema:"", esapquery: query.esapquery, status: "error", results: null, }); }); }); - }, [formData]); + }, [formData, page, regPage]); function formTemplate({ TitleField, properties, title, description }) { return ( @@ -132,6 +156,7 @@ export default function QueryIVOARegistry() { query: { "ui:widget": "textarea" }, keyword: { "ui:widget": "hidden" }, tap_schema: { "ui:widget": "hidden" }, + waveband: { "ui:widget": "hidden" }, }; console.log("new ui schema:", uiSchemaProp); return ( @@ -176,7 +201,7 @@ export default function QueryIVOARegistry() { {...uiSchemaProp} > <div> - <Button type="submit">Get Registry Services</Button> + <Button type="submit">Query VO Registry</Button> </div> </Form> {Array.from(queryMap.keys()).map((catalog) => { @@ -184,17 +209,17 @@ export default function QueryIVOARegistry() { const details = queryMap.get(catalog); console.log("Details:", details); console.log("Results:", details.results); - let catalogName = - config.query_schema.properties.catalog.enumNames[ - config.query_schema.properties.catalog.enum.findIndex( - (name) => name === catalog - ) - ]; + // let catalogName = + // config.query_schema.properties.catalog.enumNames[ + // config.query_schema.properties.catalog.enum.findIndex( + // (name) => name === catalog + // ) + // ]; return ( <div key={catalog} className="mt-3"> <Row> <Col> - <h4>List of registries</h4> + <h4>List of resources</h4> </Col> <Col> {selectedRegistry.length === 0 ? ( @@ -206,7 +231,7 @@ export default function QueryIVOARegistry() { setQueryStep("run-query"); }} > - Query selected registry + Query selected resources </Button> )} </Col> diff --git a/src/components/query/QueryResults.js b/src/components/query/QueryResults.js index edc9542aed74a5a35a07649a4e78e932fe3c9c94..69e23c17de7eb00bbe9e39e62a16a18c98bbbd0b 100644 --- a/src/components/query/QueryResults.js +++ b/src/components/query/QueryResults.js @@ -2,7 +2,9 @@ import React from "react"; import ApertifResults from "./ApertifResults"; import ASTRONVOResults from "./ASTRONVOResults"; import ZooniverseResults from "./ZooniverseResults"; -import VORegistryResults from "./VORegistryResults"; +import VORegListResults from "./VORegListResults"; +import LOFARResults from "./LOFARResults"; +import RucioResults from "./RucioResults"; export default function QueryResults({ catalog }) { switch (catalog) { @@ -15,7 +17,11 @@ export default function QueryResults({ catalog }) { case "zooniverse_workflows": return <ZooniverseResults catalog={catalog} />; case "vo_reg": - return <VORegistryResults catalog={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..4788f1368995d50d6729918f17cc37012ed6bb91 --- /dev/null +++ b/src/components/query/RucioResults.js @@ -0,0 +1,131 @@ +import React, { useContext } from "react"; +import { Table, Alert, Form } from "react-bootstrap"; +import { QueryContext } from "../../contexts/QueryContext"; +// import { BasketContext } from "../../contexts/BasketContext"; +import LoadingSpinner from "../LoadingSpinner"; +import Paginate 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/VORegistryResults.js b/src/components/query/VORegListResults.js similarity index 87% rename from src/components/query/VORegistryResults.js rename to src/components/query/VORegListResults.js index d8481c93430700d4c8ef622cc70a238703f9fa6c..19c6f2a23877f25cea921000ab15f62bd2eca8b6 100644 --- a/src/components/query/VORegistryResults.js +++ b/src/components/query/VORegListResults.js @@ -1,11 +1,11 @@ -import React, { useContext, useEffect, useState } from "react"; +import React, { useContext, useEffect } from "react"; import { Table, Alert, InputGroup } from "react-bootstrap"; import { QueryContext } from "../../contexts/QueryContext"; import LoadingSpinner from "../LoadingSpinner"; import Paginate from "../Paginate"; import { IVOAContext } from "../../contexts/IVOAContext"; -export default function VORegistryResults({ catalog }) { +export default function VORegListResults({ catalog }) { const { queryMap } = useContext(QueryContext); const { selectedRegistry, @@ -13,6 +13,8 @@ export default function VORegistryResults({ catalog }) { removeRegistry, registryList, setRegistryList, + regPage, + setRegPage, } = useContext(IVOAContext); // const [checkAll, setCheckAll] = useState(""); @@ -35,8 +37,20 @@ export default function VORegistryResults({ catalog }) { setRegistryList(queryMap.get(catalog).results.results); console.log("Registry List:", registryList); + const numPages = queryMap.get(catalog).results.pages; + return ( <> + <Paginate + getNewPage={(args) => { + return args.target + ? setRegPage(parseFloat(args.target.text)) + : null; + }} + currentPage={regPage} + numAdjacent={3} + numPages={numPages} + /> <Table className="mt-3" responsive> <thead> <tr className="bg-light"> @@ -60,7 +74,7 @@ export default function VORegistryResults({ catalog }) { /> </InputGroup> </th> - <th>Name</th> + <th>Resource</th> <th>Access URL</th> <th>Waveband</th> <th>Title</th> @@ -96,7 +110,6 @@ export default function VORegistryResults({ catalog }) { })} </tbody> </Table> - {/* <Paginate /> */} </> ); } else { diff --git a/src/components/query/VOServiceResults.js b/src/components/query/VOServiceResults.js index 0f9ee8c79bc649c1c5e2b91202a8dcea1c09145d..3d8147c0a76b4284a0802fe8060eca4cf4a2f9f3 100644 --- a/src/components/query/VOServiceResults.js +++ b/src/components/query/VOServiceResults.js @@ -1,40 +1,113 @@ import React, { useContext } from "react"; -import { Alert, Table } from "react-bootstrap"; +import { Alert, Table, Button } from "react-bootstrap"; import { QueryContext } from "../../contexts/QueryContext"; +import LoadingSpinner from "../LoadingSpinner"; +import Paginate from "../Paginate"; +import Preview from "./Preview"; export default function VORegistryResults({ catalog }) { - const { queryMap } = useContext(QueryContext); + const { queryMap, page, setPage, preview, setPreview, setURL } = useContext(QueryContext); if (!queryMap.get(catalog)) return null; console.log("VO service queryMap:", queryMap.get(catalog)); + if (queryMap.get(catalog).status === "fetched") { + // if (queryMap.get(catalog).results[0].includes("ERROR")) + // return ( + // <Alert variant="warning">{queryMap.get(catalog).results[0]}</Alert> + // ); if (queryMap.get(catalog).results.results.length === 0) return <Alert variant="warning">No matching results found!</Alert>; + + const numPages = queryMap.get(catalog).results.pages; + let indice = []; + return ( <div> <h1>Results from {catalog}</h1> + <Paginate + getNewPage={(args) => { + return args.target ? setPage(parseFloat(args.target.text)) : null; + }} + currentPage={page} + numAdjacent={3} + numPages={numPages} + /> <Table className="mt-3" responsive> <thead> <tr className="bg-light"> - <th>Link to data</th> + {queryMap.get(catalog).vo_table_schema.fields.map((field, index) => { + if ((field.name === "dataproduct_type") || (field.name === "dataproduct_subtype") || + (field.name === "calib_level") || (field.name === "obs_collection") || + (field.name === "obs_id") || + (field.name === "calib_level") || (field.name === "access_url") || + (field.name === "access_estsize") || (field.name === "target_name") || + (field.name === "s_ra") || (field.name === "s_dec") || + (field.name === "s_fov") || + (field.name === "instrument_name") || (field.name === "preview") + ) { + indice.push(index); + return (<th>{field.name}</th>); + } + return null; + })} </tr> </thead> <tbody> {queryMap.get(catalog).results.results.map((result) => { + + let queryResult = result.result.split(","); + return ( - <tr key={result.result}> - <td> - <a href={result.result} rel="noopener noreferrer" download> - {result.result} - </a> - </td> - </tr> + <> + <tr key={queryResult[queryMap.get(catalog).vo_table_schema.fields.findIndex((item) => item.name === "access_url")]}> + {queryResult.map((field, index) => { + if (indice.includes(index)) { + if (queryMap.get(catalog).vo_table_schema.fields[index].name === "access_url") { + return (<td><a href={field} rel="noopener noreferrer" download>Download data</a></td>); + } + if (queryMap.get(catalog).vo_table_schema.fields[index].name === "preview") { + return (<td> + { + <Button + onClick={()=>{ + setPreview(field); + setURL(field); + }} + > + View Thumbnail + </Button> + } + </td> ); + } + if ((queryMap.get(catalog).vo_table_schema.fields[index].name === "s_ra") || + (queryMap.get(catalog).vo_table_schema.fields[index].name === "s_dec") || + (queryMap.get(catalog).vo_table_schema.fields[index].name === "s_fov") + ) { + return (<td>{Number(field).toFixed(1)}</td>) + } + return (<td>{field}</td>) ; + } + return null; + })} + </tr> + { + preview === queryResult[queryMap.get(catalog).vo_table_schema.fields.findIndex((item) => item.name === "preview")] && + <tr key={queryResult.preview}> + <td></td> + <td></td> + <td></td> + <td colSpan={queryResult.length-3} ><Preview /></td> + </tr> + } + </> ); })} </tbody> </Table> </div> ); + } else { + return <LoadingSpinner />; } - return null; } 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..35880d0d8d8dcc13a4c7771095486a6e2c17af87 100644 --- a/src/contexts/BasketContext.js +++ b/src/contexts/BasketContext.js @@ -1,7 +1,4 @@ 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 +15,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 d8c42dcfbba472643df6364b482f3c14dfd3a148..f76cf0c95d339ceab169d32001f3fa768383f0d9 100644 --- a/src/contexts/GlobalContext.js +++ b/src/contexts/GlobalContext.js @@ -1,29 +1,19 @@ import React, { createContext, useState, useEffect } from "react"; +import { Alert } from "react-bootstrap"; import axios from "axios"; import getCookie from "../utils/getCookie"; export const GlobalContext = createContext(); export function GlobalContextProvider({ children }) { + console.log("ASTRON ESAP version ", Date()); const api_host = process.env.NODE_ENV === "development" - ? "http://sdc.astron.nl:5555/esap-api/" + ? "https://sdc.astron.nl:5555/esap-api/" : "/esap-api/"; - - const [config, setConfig] = useState(); - const [configName, setConfigName] = useState("esap_ivoa"); - - useEffect(() => { - let configNameString = ""; - if (configName) { - configNameString = `?name=${configName}`; - } - axios - .get(api_host + "query/configuration" + configNameString) - .then((response) => setConfig(response.data["configuration"])); - }, [api_host, configName]); - console.log("config: ", { config }); + // "https://sdc.astron.nl:5555/esap-api/" + // "http://localhost:5555/esap-api/" const [archives, setArchives] = useState(); useEffect(() => { @@ -32,6 +22,7 @@ export function GlobalContextProvider({ children }) { .then((response) => setArchives(response.data.results)); }, [api_host]); + // !!!!! Still need to look at sessionid and stuff const [sessionid, setSessionid] = useState(getCookie("sessionid")); console.log("waah", sessionid, getCookie("sessionid"), document.cookie); const [isAuthenticated, setIsAuthenticated] = useState( @@ -52,17 +43,29 @@ export function GlobalContextProvider({ children }) { return null; }; + const handleError = (event) => { + setIsAuthenticated(false); + setSessionid(null); + + return ( + <> + <Alert variant="warning">An error has occurred during login!</Alert> + <Alert variant="warning">{event.staticContext}</Alert> + </> + ); + }; + + return ( <GlobalContext.Provider value={{ api_host, isAuthenticated, sessionid, - config, archives, handleLogin, handleLogout, - setConfigName, + handleError, }} > {children} diff --git a/src/contexts/IVOAContext.js b/src/contexts/IVOAContext.js index a5363958e05f9505b8046e3577721360e5c4b50d..e3a755312e9a4905448677479f54ea5cbb3a7993 100644 --- a/src/contexts/IVOAContext.js +++ b/src/contexts/IVOAContext.js @@ -1,5 +1,4 @@ import React, { createContext, useState } from "react"; -import useMap from "../hooks/useMap"; export const IVOAContext = createContext(); @@ -7,6 +6,8 @@ export function IVOAContextProvider({ children }) { const [registryList, setRegistryList] = useState([]); const [selectedRegistry, setSelectedRegistry] = useState([]); const [queryStep, setQueryStep] = useState("get-services"); + const [regPage, setRegPage] = useState(1); + /* IVOA query steps: 1. get-services @@ -40,6 +41,8 @@ export function IVOAContextProvider({ children }) { selectedRegistry, addRegistry: handleAddRegistry, removeRegistry: handleRemoveRegistry, + regPage, + setRegPage, registryList, setRegistryList, queryStep, diff --git a/src/contexts/QueryContext.js b/src/contexts/QueryContext.js index e0ad5419511ddddacc8a575a820f7ae199e5a56e..f9a37e027eb697ed67688040c6fff206ed7a4018 100644 --- a/src/contexts/QueryContext.js +++ b/src/contexts/QueryContext.js @@ -1,12 +1,53 @@ -import React, { createContext, useState } from "react"; +import React, { createContext, useState, useEffect, useContext } from "react"; +import axios from "axios"; import useMap from "../hooks/useMap"; +import { GlobalContext } from "./GlobalContext"; export const QueryContext = createContext(); export function QueryContextProvider({ children }) { + const defaultConf = "esap_ivoa"; const queryMap = useMap(); const [formData, setFormData] = useState(); const [page, setPage] = useState(1); + const [url, setURL] = useState("https://uilennest.net/astrobase/data/191231001/3836665.fits"); + const [dplevel, setDPLevel] = useState(); + const [collection, setCollection] = useState(); + const [config, setConfig] = useState(); + const [configName, setConfigName] = useState(defaultConf); + const { api_host } = useContext(GlobalContext); + const [preview, setPreview] = useState(false); + const [ds9, setDS9] = useState(false); + + useEffect(() => { + let configNameString = ""; + if (configName) { + configNameString = `?name=${configName}`; + } + axios + .get(api_host + "query/configuration" + configNameString) + .then((response) => { + let config = response.data["configuration"]; + let props = config.query_schema.properties; + console.log("config props: ", props); + console.log("collection value: ", collection); + console.log("dplevel value: ", dplevel); + Object.keys(props).map((key) => { + if (key === "collection" && collection) { + console.log("has key collection, default value is: ", props[key]["default"]); + props[key]["default"] = collection; + } + if (key === "level" && dplevel) { + console.log("has key dplevel, default value is: ", props[key]["default"]); + props[key]["default"] = dplevel; + } + return null; + }); + setConfig(config); + }); + }, [api_host, configName, dplevel, collection]); + console.log("config: ", { config }); + return ( <QueryContext.Provider value={{ @@ -15,6 +56,19 @@ export function QueryContextProvider({ children }) { queryMap, formData, setFormData, + url, + setURL, + dplevel, + setDPLevel, + collection, + setCollection, + config, + setConfigName, + defaultConf, + ds9, + setDS9, + preview, + setPreview, }} > {children} diff --git a/src/routes/Routes.js b/src/routes/Routes.js index cc4098e741edcc92f1ea27b75432113034c25f56..b70b60d98bdb5dad03f40c9bcaa7260bd923fcce 100644 --- a/src/routes/Routes.js +++ b/src/routes/Routes.js @@ -7,32 +7,23 @@ import QueryCatalogs from "../components/query/QueryCatalogs"; import QueryIVOARegistry from "../components/query/QueryIVOARegistry"; import { BrowserRouter as Router } from "react-router-dom"; import NavBar from "../components/NavBar"; -import { QueryContextProvider } from "../contexts/QueryContext"; +import { QueryContext } from "../contexts/QueryContext"; import Rucio from "../components/Rucio"; import Interactive from "../components/Interactive"; import { IVOAContextProvider } from "../contexts/IVOAContext"; export default function Routes() { - const { config, handleLogin, handleLogout } = useContext(GlobalContext); + const { handleLogin, handleLogout, handleError } = useContext(GlobalContext); + const { config } = useContext(QueryContext); if (!config) return null; return ( <Router basename={config.frontend_basename}> <NavBar /> <Switch> - <Route exact path="/"> + <Route exact path={["/", "/archives"]}> <Archives /> </Route> - <Route exact path="/archives"> - <Archives /> - </Route> - <Route exact path="/query"> - <QueryContextProvider> - <IVOAContextProvider> - <QueryIVOARegistry /> - </IVOAContextProvider> - </QueryContextProvider> - </Route> <Route exact path="/rucio"> <Rucio /> </Route> @@ -41,18 +32,15 @@ export default function Routes() { </Route> <Route exact path="/login" component={handleLogin} /> <Route exact path="/logout" component={handleLogout} /> + <Route exact path="/error" component={handleError} /> <Route exact path="/archives/:uri" component={ArchiveDetails} /> - <Route exact path="/archives/ivoa/query"> - <QueryContextProvider> - <IVOAContextProvider> - <QueryIVOARegistry /> - </IVOAContextProvider> - </QueryContextProvider> + <Route exact path={["/vo-query", "/archives/ivoa/query"]}> + <IVOAContextProvider> + <QueryIVOARegistry /> + </IVOAContextProvider> </Route> - <Route exact path="/archives/:uri/query"> - <QueryContextProvider> - <QueryCatalogs /> - </QueryContextProvider> + <Route exact path={["/adex-query", "/archives/:uri/query"]}> + <QueryCatalogs /> </Route> </Switch> </Router> diff --git a/src/utils/form/parseADEXForm.js b/src/utils/form/parseADEXForm.js index 351fabef237b4d6f60208855092efa1e64e4060b..75f134d0e1ae7407460128178484854c52e4ae1d 100644 --- a/src/utils/form/parseADEXForm.js +++ b/src/utils/form/parseADEXForm.js @@ -1,5 +1,5 @@ -export default function ParseADEXForm(formData) { - let catalogs = ["apertif", "astron_vo"]; +export default function ParseADEXForm(formData, page) { + let catalogs = ["apertif", "astron_vo", "lofar"]; let queries = []; // queries is an array of dictionaries, where each dictionary consists of // {"catalog": "catalogname", @@ -20,10 +20,14 @@ export default function ParseADEXForm(formData) { // "status": "null|fetching|fetched", // "results": null} let catalog = formInput.find(([key]) => key === "catalog")[1]; - if (catalog === "all") { + if (catalog === "adex") { console.log("Catalogs:", catalogs); catalogs.map((catalog) => { - let esapquery = query + `${`${query}` ? "&" : ""}archive_uri=` + catalog; + let esapquery = + query + + `${`${query}` ? "&" : ""}archive_uri=` + + catalog + + "&page_size=20"; queries.push({ catalog: catalog, @@ -32,7 +36,11 @@ export default function ParseADEXForm(formData) { return null; }); } else { - let esapquery = query + `${`${query}` ? "&" : ""}archive_uri=` + catalog; + let esapquery = + query + + `${`${query}` ? "&" : ""}archive_uri=` + + catalog + + `&page_size=30&page=${page}`; queries.push({ catalog: catalog, diff --git a/src/utils/form/parseASTRONVOForm.js b/src/utils/form/parseASTRONVOForm.js index 9162c828a9635cabe750e5dbbcccfd933dc8ea4c..6406cf850f4d9ce0453210e69e1bef7cb9ff5269 100644 --- a/src/utils/form/parseASTRONVOForm.js +++ b/src/utils/form/parseASTRONVOForm.js @@ -1,4 +1,4 @@ -export default function ParseASTRONVOForm(formData) { +export default function ParseASTRONVOForm(formData, page) { let queries = []; // queries is an array of dictionaries, where each dictionary consists of // {"catalog": "catalogname", @@ -19,7 +19,9 @@ export default function ParseASTRONVOForm(formData) { // "status": "null|fetching|fetched", // "results": null} let catalog = formInput.find(([key]) => key === "catalog")[1]; - let esapquery = query + `${`${query}` ? "&" : ""}archive_uri=` + catalog; + let esapquery = query + `${`${query}` ? "&" : ""}archive_uri=` + catalog; //+ `&page=${page}` + // testing api with page=1, failing at the backend at the moment + page === 1 ? console.log("Page number is 1") : (esapquery += `&page=${page}`); queries.push({ catalog: catalog, esapquery: esapquery, diff --git a/src/utils/form/parseApertifForm.js b/src/utils/form/parseApertifForm.js index 4a764050acb24deea951cb6778e8e023e62364c6..6aab904362942e15d814bdf12d9ca3f12d7b244e 100644 --- a/src/utils/form/parseApertifForm.js +++ b/src/utils/form/parseApertifForm.js @@ -1,4 +1,4 @@ -export default function ParseApertifForm(formData) { +export default function ParseApertifForm(formData, page) { let queries = []; // queries is an array of dictionaries, where each dictionary consists of // {"catalog": "catalogname", @@ -19,7 +19,8 @@ export default function ParseApertifForm(formData) { // "status": "null|fetching|fetched", // "results": null} let catalog = formInput.find(([key]) => key === "catalog")[1]; - let esapquery = query + `${`${query}` ? "&" : ""}archive_uri=` + catalog; + let esapquery = + query + `${`${query}` ? "&" : ""}archive_uri=` + catalog + `&page=${page}`; queries.push({ catalog: catalog, esapquery: esapquery, diff --git a/src/utils/form/parseIVOAForm.js b/src/utils/form/parseIVOAForm.js index e518ff0ccaf91f1b5730b7f98bdc8fe9df92bef7..2b3fadcf898d051b1ed7b733e549fd25e049fe1b 100644 --- a/src/utils/form/parseIVOAForm.js +++ b/src/utils/form/parseIVOAForm.js @@ -1,4 +1,4 @@ -export default function ParseIVOAForm(formData) { +export default function ParseIVOAForm(formData, page) { let queries = []; // queries is an array of dictionaries, where each dictionary consists of // {"catalog": "catalogname", @@ -25,7 +25,11 @@ export default function ParseIVOAForm(formData) { let service_type = formInput.find(([key]) => key === "service_type")[1]; let esapquery = - "get-services/?" + query + `${`${query}` ? "&" : ""}dataset_uri=` + catalog; + "get-services/?" + + query + + `${`${query}` ? "&" : ""}dataset_uri=` + + catalog + + `&page=${page}`; queries.push({ catalog: catalog, diff --git a/src/utils/form/parseLOFARForm.js b/src/utils/form/parseLOFARForm.js index badc2b8512cc5c64457384b6e23c3b435f4eb1d8..8f8ff91cf9c6cd1e297449c00b2e41702ed20a51 100644 --- a/src/utils/form/parseLOFARForm.js +++ b/src/utils/form/parseLOFARForm.js @@ -1,4 +1,4 @@ -export default function ParseLOFARForm(formData) { +export default function ParseLOFARForm(formData, page) { let queries = []; // queries is an array of dictionaries, where each dictionary consists of // {"catalog": "catalogname", @@ -19,7 +19,8 @@ export default function ParseLOFARForm(formData) { // "status": "null|fetching|fetched", // "results": null} let catalog = formInput.find(([key]) => key === "catalog")[1]; - let esapquery = query + `${`${query}` ? "&" : ""}archive_uri=` + catalog; + let esapquery = + query + `${`${query}` ? "&" : ""}archive_uri=` + catalog + `&page=${page}`; queries.push({ catalog: catalog, esapquery: esapquery, diff --git a/src/utils/form/parseQueryForm.js b/src/utils/form/parseQueryForm.js index ecf4e15093d67ab8c6142f82e434ee406d4128d9..62d7ec47344ccab0d0631caf302fb3369b441a42 100644 --- a/src/utils/form/parseQueryForm.js +++ b/src/utils/form/parseQueryForm.js @@ -4,21 +4,24 @@ 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) { case "adex": - return parseADEXForm(formData); + return parseADEXForm(formData, page); case "zooniverse": return parseZooniverseForm(formData, page); case "lofar": - return parseLOFARForm(formData); + return parseLOFARForm(formData, page); case "apertif": - return parseApertifForm(formData); + return parseApertifForm(formData, page); case "astron_vo": - return parseASTRONVOForm(formData); + return parseASTRONVOForm(formData, page); case "ivoa": - return parseIVOAForm(formData); + 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}` + }]; +} diff --git a/src/utils/form/parseVOServiceForm.js b/src/utils/form/parseVOServiceForm.js index c42aec9a71378c01d5b44ba9d9e675ff0b09abf9..69d411fc81eab9f8780108629e78264c70f23caa 100644 --- a/src/utils/form/parseVOServiceForm.js +++ b/src/utils/form/parseVOServiceForm.js @@ -1,4 +1,4 @@ -export default function ParseVOServiceForm(formData, access_url) { +export default function ParseVOServiceForm(formData, access_url, page) { let queries = []; // queries is an array of dictionaries, where each dictionary consists of // {"catalog": "catalogname", @@ -17,7 +17,7 @@ export default function ParseVOServiceForm(formData, access_url) { query += `${`${query}` ? "&" : ""}` + key + "=" + value; } } - query += `${`${query}` ? "&" : ""}` + "access_url=" + access_url; + query += `${`${query}` ? "&" : ""}access_url=${access_url}`; console.log("Query:", query); // If catalog is set to "all", query for each catalog needs to be generated @@ -29,7 +29,11 @@ export default function ParseVOServiceForm(formData, access_url) { let service_type = formInput.find(([key]) => key === "service_type")[1]; let esapquery = - "query/?" + query + `${`${query}` ? "&" : ""}dataset_uri=` + catalog; + "query/?" + + query + + `${`${query}` ? "&" : ""}dataset_uri=` + + catalog + + `&page=${page}`; queries.push({ catalog: access_url,