diff --git a/cypress/e2e/spec.cy.js b/cypress/e2e/spec.cy.js index 6c97e556..28e10a8c 100644 --- a/cypress/e2e/spec.cy.js +++ b/cypress/e2e/spec.cy.js @@ -69,6 +69,14 @@ describe("Web app", () => { ); }); + it("Variables in column header contain link to ontology", () => { + cy.visit("/"); + + cy.contains("My favourite musicians").click(); + cy.contains("Finished in:"); + cy.get('a[href="http://schema.org/name"]'); + }) + it("When one source throws an error, the results of other sources are still shown", () => { cy.visit("/"); diff --git a/src/components/ListResultTable/QueryResultList/QueryResultList.jsx b/src/components/ListResultTable/QueryResultList/QueryResultList.jsx index e652a5ff..797f5128 100644 --- a/src/components/ListResultTable/QueryResultList/QueryResultList.jsx +++ b/src/components/ListResultTable/QueryResultList/QueryResultList.jsx @@ -4,6 +4,7 @@ import ActionBar from "../../ActionBar/ActionBar"; import GenericField from "../../../representationProvider/GenericField"; import { Term } from "sparqljs"; import config from "../../../config"; +import TableHeader from "./TableHeader/TableHeader"; /** * @param {object} props - the props passed down to the component @@ -25,7 +26,7 @@ function QueryResultList(props) { <ListView title=" " actions={<ActionBar />} {...props}> {values && ( - <Datagrid> + <Datagrid header={<TableHeader config={config}/>}> {Object.keys(values).map((key) => { return ( <GenericField diff --git a/src/components/ListResultTable/QueryResultList/TableHeader/TableHeader.css b/src/components/ListResultTable/QueryResultList/TableHeader/TableHeader.css new file mode 100644 index 00000000..aa9f020a --- /dev/null +++ b/src/components/ListResultTable/QueryResultList/TableHeader/TableHeader.css @@ -0,0 +1,9 @@ + +.header-button{ + height: 100%; + vertical-align: middle; +} + +.header-button:hover{ + cursor: pointer; +} \ No newline at end of file diff --git a/src/components/ListResultTable/QueryResultList/TableHeader/TableHeader.jsx b/src/components/ListResultTable/QueryResultList/TableHeader/TableHeader.jsx new file mode 100644 index 00000000..015dbd43 --- /dev/null +++ b/src/components/ListResultTable/QueryResultList/TableHeader/TableHeader.jsx @@ -0,0 +1,98 @@ +import { Link, TableCell, TableHead, TableRow } from "@mui/material"; +import React from "react"; +import { useListContext } from "react-admin"; +import "./TableHeader.css"; +import NorthIcon from "@mui/icons-material/North"; +import SouthIcon from "@mui/icons-material/South"; +import LinkIcon from "@mui/icons-material/Link"; +import PropTypes from "prop-types"; +import { Component } from "react"; + +/** + * + * @param {object} props - the props passed down to the component + * @param {Array<Component>} props.children - the children of the component + * @param {object} props.config - the config object of the application + * @returns {Component} the header of the table containing the column names, the sort icons and ontology links + */ +function TableHeader({ children, config }) { + const { sort, setSort, resource } = useListContext(); + const { variableOntology } = config.queries.filter( + (query) => query.id === resource + )[0]; + + /** + * Handles the click on a header and sets the sort state accordingly + * @param {string} target - the source of the column that was clicked + */ + function handleHeaderClick(target) { + const newSort = { field: target, order: "DESC" }; + if (sort) { + if (sort.order === "ASC") { + newSort.order = "DESC"; + } else { + newSort.order = "ASC"; + } + } + setSort(newSort); + } + + return ( + <TableHead> + <TableRow> + <TableCell> </TableCell> + {React.Children.map(children, (child) => ( + <> + <TableCell + key={child.props.source} + sx={{ height: "100%", "& > *": { verticalAlign: "middle" } }} + > + <span + role="button" + className="header-button" + onClick={() => handleHeaderClick(child.props.source)} + > + {child.props.label} + </span> + {variableOntology[child.props.source] && ( + <Link + target="_blank" + href={variableOntology[child.props.source]} + sx={{ height: "100%", margin: "0 5px", "& > *": { verticalAlign: "middle" } }} + > + <LinkIcon + fontSize="small" + sx={{ height: "100%", color: "gray" }} + /> + </Link> + )} + {sort.field === child.props.source && ( + <> + {sort && sort.order === "DESC" && ( + <NorthIcon + fontSize="small" + sx={{ height: "100%", color: "gray" }} + /> + )} + {sort && sort.order === "ASC" && ( + <SouthIcon + fontSize="small" + sx={{ height: "100%", color: "gray" }} + /> + )} + </> + )} + </TableCell> + </> + ))} + </TableRow> + </TableHead> + ); +} + +TableHeader.propTypes = { + children: PropTypes.node, + config: PropTypes.object.isRequired, + +} +export default TableHeader; diff --git a/src/dataProvider/SparqlDataProvider.js b/src/dataProvider/SparqlDataProvider.js index 5b25d5de..546de586 100644 --- a/src/dataProvider/SparqlDataProvider.js +++ b/src/dataProvider/SparqlDataProvider.js @@ -94,6 +94,9 @@ async function fetchQuery(query) { const rawText = await result.text(); query.rawText = rawText; const parsedQuery = parser.parse(rawText); + if (!query.variableOntology) { + query.variableOntology = findPredicates(parsedQuery); + } if (!parsedQuery.limit) { parsedQuery.limit = query.limit; } @@ -116,6 +119,26 @@ async function fetchQuery(query) { } } +/** + * Given a query and an object, this function returns the predicate of the object in the query. + * @param {object} query - the paresed query in which the predicate is to be looked for. + * @returns {object} an object with the variable as key and the predicate as value. + */ +function findPredicates(query) { + const ontologyMapper = {}; + if (!query.variables) { + return query; + } + for (const part of query.where) { + for (const triple of part.triples) { + if(triple.predicate.termType !== "Variable"){ + ontologyMapper[triple.object.value] = triple.predicate.value; + } + } + } + return ontologyMapper; +} + /** * A function that executes a given query and processes every result. * @param {object} query - the query which is to be executed and additional information about the query. @@ -125,12 +148,9 @@ async function executeQuery(query) { try { query.queryText = await fetchQuery(query); return handleQueryExecution( - await myEngine.query( - query.queryText, - { - ...generateContext(query.comunicaContext) - } - ), + await myEngine.query(query.queryText, { + ...generateContext(query.comunicaContext), + }), query ); } catch (error) {