Skip to content

Commit 9fd41bf

Browse files
committed
Merge branch 'main' into ontology-link
2 parents 92dba3e + 7c3518f commit 9fd41bf

File tree

21 files changed

+345
-72
lines changed

21 files changed

+345
-72
lines changed

.eslintrc.cjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ module.exports = {
5555
// 'jsdoc/require-description-complete-sentence': 1,
5656
// 'jsdoc/require-example': 1,
5757
// 'jsdoc/require-file-overview': 1,
58-
// 'jsdoc/require-hyphen-before-param-description': 1,
58+
'jsdoc/require-hyphen-before-param-description': 1,
5959
'jsdoc/require-jsdoc': 1, // Recommended
6060
'jsdoc/require-param': 1, // Recommended
6161
'jsdoc/require-param-description': 1, // Recommended

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,5 @@ dist-ssr
2626
pods*
2727

2828
lintPush.sh
29+
30+
cypress/screenshots

cypress/e2e/spec.cy.js

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,72 @@ describe("Web app", () => {
33
cy.visit("/");
44
});
55

6+
it("Fetch status source info on query failed", () => {
7+
cy.visit("/");
8+
9+
cy.contains("My favourite musicians").click();
10+
cy.contains("Finished in:");
11+
cy.get('[aria-label="Sources info"]').click();
12+
13+
cy.get('[aria-label="Query failed"]').should("exist");
14+
});
15+
16+
it("Fetch status source info on query success", () => {
17+
cy.visit("/");
18+
19+
cy.contains("My wish list").click();
20+
cy.contains("Finished in:");
21+
cy.get('[aria-label="Sources info"]').click();
22+
23+
cy.get('[aria-label="Query was succesful"]').should("exist");
24+
});
25+
26+
it("Authentication needed source info for query on public data", () => {
27+
cy.visit("/");
28+
29+
cy.contains("My wish list").click();
30+
cy.contains("Finished in:");
31+
cy.get('[aria-label="Sources info"]').click();
32+
33+
cy.get('[aria-label="No authentication required"]').should("exist");
34+
});
35+
36+
it("Authentication needed source info for query on private data", () => {
37+
cy.visit("/");
38+
39+
cy.get('[aria-label="Profile"]').click();
40+
cy.contains('[role="menuitem"]', "Login").click();
41+
42+
cy.get('input[name="idp"]').clear();
43+
cy.get('input[name="idp"]').type("http://localhost:8080");
44+
cy.contains("Login").click();
45+
46+
cy.get("input#email").type("hello@example.com");
47+
cy.get("input#password").type("abc123");
48+
cy.contains("button", "Log in").click();
49+
cy.contains("button", "Authorize").click();
50+
51+
cy.url().should("eq", "http://localhost:5173/");
52+
53+
cy.contains("A list of my favorite books").click();
54+
cy.contains("Finished in:");
55+
cy.get('[aria-label="Sources info"]').click();
56+
57+
cy.get('[aria-label="Authentication required"]').should("exist");
58+
});
59+
60+
it("Authentication needed source info for query on failing query", () => {
61+
cy.visit("/");
62+
63+
cy.contains("My favourite musicians").click();
64+
cy.contains("Finished in:");
65+
cy.get('[aria-label="Sources info"]').click();
66+
67+
cy.get('[aria-label="Uncertain if authentication is required"]').should(
68+
"exist"
69+
);
70+
});
71+
672
it("Variables in column header contain link to ontology", () => {
773
cy.visit("/");
874

@@ -17,7 +83,7 @@ describe("Web app", () => {
1783
cy.contains("My favourite musicians").click();
1884
cy.contains("Finished in:");
1985
cy.contains("Ludwig van Beethoven");
20-
})
86+
});
2187

2288
it("Log in and execute query on private data", () => {
2389
cy.visit("/");
@@ -37,14 +103,14 @@ describe("Web app", () => {
37103
cy.url().should("eq", "http://localhost:5173/");
38104

39105
cy.contains("A list of my favorite books").click();
40-
cy.contains('It Ends With Us');
106+
cy.contains("It Ends With Us");
41107
});
42108

43109
it("Query on private data unauthenticated", () => {
44110
cy.visit("/");
45111

46112
cy.contains("A list of my favorite books").click();
47-
cy.get("div").should("have.class", "MuiSnackbarContent-message")
113+
cy.get("div").should("have.class", "MuiSnackbarContent-message");
48114
});
49115

50116
it('Querying resource with "bad" cors header, though a proxy should work', () => {

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"dev": "vite",
88
"build": "vite build",
99
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
10+
"lint:fix": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0 --fix",
1011
"preview": "vite preview",
1112
"test": "cypress run",
1213
"prepare:pods": "npm run prepare:pods:accounts && npm run prepare:pods:data",

src/authenticationProvider/authenticationProvider.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ export default {
8787

8888
/**
8989
* Looks up the IDP of a WebID by querying the WebID .
90-
* @param {URL} webId the WebID to query the IDP from
90+
* @param {URL} webId - the WebID to query the IDP from
9191
* @returns {?Promise<URL>} the first IDP of the WebID or undefined if no IDP is found in the WebID document
9292
*/
9393
async function queryIDPfromWebId(webId) {
@@ -111,7 +111,7 @@ async function queryIDPfromWebId(webId) {
111111

112112
/**
113113
*
114-
* @param {object} webIdThing the webId (actually of type ProfileAll, but importing this throws an error https://github.com/SolidLabResearch/generic-data-viewer-react-admin/issues/15) document to get the name from
114+
* @param {object} webIdThing - the webId (actually of type ProfileAll, but importing this throws an error https://github.com/SolidLabResearch/generic-data-viewer-react-admin/issues/15) document to get the name from
115115
* @returns {?string} either the name or undefined if no foaf:name is found
116116
*/
117117
function getName(webIdThing) {
@@ -125,7 +125,7 @@ function getName(webIdThing) {
125125

126126
/**
127127
*
128-
* @param {object} webIdThing the webId (actually of type ProfileAll, but importing this throws an error https://github.com/SolidLabResearch/generic-data-viewer-react-admin/issues/15) document to get the profile picture from
128+
* @param {object} webIdThing - the webId (actually of type ProfileAll, but importing this throws an error https://github.com/SolidLabResearch/generic-data-viewer-react-admin/issues/15) document to get the profile picture from
129129
* @returns {?string} either a url to the profile picture or undefined if no foaf:img is found
130130
*/
131131
function getProfilePicture(webIdThing) {

src/components/ActionBar/ActionBar.jsx

Lines changed: 91 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,40 @@ import { ExportButton, TopToolbar, useListContext } from "react-admin";
33
import Time from "./Time";
44
import config from "../../config";
55
import "./ActionBar.css";
6+
import {
7+
Grid,
8+
IconButton,
9+
Paper,
10+
Table,
11+
TableBody,
12+
TableCell,
13+
TableContainer,
14+
TableHead,
15+
TableRow,
16+
Tooltip,
17+
} from "@mui/material";
18+
import InfoIcon from "@mui/icons-material/Info";
19+
import SourceAuthenticationIcon from "./SourceAuthenticationIcon/SourceAuthenticationIcon";
20+
import SourceFetchStatusIcon from "./SourceFetchStatusIcon/SourceFetchStatusIcon";
621

722
/**
8-
*
23+
*
924
* @returns {Component} custom action bar as defined by react-admin
1025
*/
1126
function ActionBar() {
12-
const { total, isLoading, perPage } = useListContext();
27+
const { total, isLoading, perPage, resource } = useListContext();
1328
const [time, setTime] = useState(0);
29+
const [sourceInfoOpen, setSourceInfoOpen] = useState(false);
1430

31+
const context = config.queries.filter((query) => query.id === resource)[0]
32+
.comunicaContext;
33+
34+
const sources = context.sources;
1535
useEffect(() => {
16-
if(isLoading){
17-
setTime(0)
36+
if (isLoading) {
37+
setTime(0);
1838
}
19-
}, [isLoading])
39+
}, [isLoading]);
2040

2141
useEffect(() => {
2242
let intervalId;
@@ -29,26 +49,72 @@ function ActionBar() {
2949
const resultCount = total <= perPage ? total : perPage;
3050

3151
return (
32-
<TopToolbar style={{ width: "100%" }}>
33-
<div style={{ flex: "1" }}></div>
34-
<div className="query-information">
35-
<div className="information-box">
36-
{isLoading && <strong>Loading: </strong>}
37-
{!isLoading && <strong>Loaded: </strong>}
38-
<span>{resultCount} results</span>
39-
</div>
40-
<div className="information-box">
41-
{isLoading && <strong>Runtime: </strong>}
42-
{!isLoading && <strong>Finished in: </strong>}
43-
<Time time={time} showMilliseconds={config.showMilliseconds} />
44-
</div>
45-
</div>
46-
<div className="action-box">
47-
<ExportButton
48-
disabled={total === 0 || isLoading}
49-
/>
50-
</div>
51-
</TopToolbar>
52+
<Grid container direction="row" width={"100%"} rowSpacing={1}>
53+
<Grid item height={"fit-content"} width={"100%"}>
54+
<TopToolbar style={{ width: "100%", height: "fit-content" }}>
55+
<div style={{ flex: "1" }}></div>
56+
<div className="query-information">
57+
<div className="information-box">
58+
{isLoading && <strong>Loading: </strong>}
59+
{!isLoading && <strong>Loaded: </strong>}
60+
<span>{resultCount} results</span>
61+
</div>
62+
<div className="information-box">
63+
{isLoading && <strong>Runtime: </strong>}
64+
{!isLoading && <strong>Finished in: </strong>}
65+
<Time time={time} showMilliseconds={config.showMilliseconds} />
66+
</div>
67+
<div className="information-box">
68+
<strong>Sources: </strong>
69+
<span>{sources.length}</span>
70+
<Tooltip title="Sources info">
71+
<IconButton
72+
size="small"
73+
sx={{ padding: "0px", marginLeft: "5px" }}
74+
onClick={() => setSourceInfoOpen(!sourceInfoOpen)}
75+
>
76+
<InfoIcon fontSize="small" />
77+
</IconButton>
78+
</Tooltip>
79+
</div>
80+
</div>
81+
<div className="action-box">
82+
<ExportButton disabled={total === 0 || isLoading} />
83+
</div>
84+
</TopToolbar>
85+
</Grid>
86+
{sourceInfoOpen && (
87+
<Grid item width={"100%"}>
88+
<TableContainer
89+
sx={{ width: "100%", marginBottom: "10px", maxHeight: "200px" }}
90+
component={Paper}
91+
>
92+
<Table size="small" >
93+
<TableHead>
94+
<TableRow>
95+
<TableCell>Source</TableCell>
96+
<TableCell>Authentication needed</TableCell>
97+
<TableCell>Fetch status</TableCell>
98+
</TableRow>
99+
</TableHead>
100+
<TableBody>
101+
{sources.map((source, index) => (
102+
<TableRow key={index}>
103+
<TableCell>{source}</TableCell>
104+
<TableCell>
105+
<SourceAuthenticationIcon source={source} />
106+
</TableCell>
107+
<TableCell>
108+
<SourceFetchStatusIcon proxyUrl={config.httpProxy} context={context} source={source} />
109+
</TableCell>
110+
</TableRow>
111+
))}
112+
</TableBody>
113+
</Table>
114+
</TableContainer>
115+
</Grid>
116+
)}
117+
</Grid>
52118
);
53119
}
54120

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { CircularProgress, Tooltip } from "@mui/material";
2+
import { useEffect, useState } from "react";
3+
import LockIcon from "@mui/icons-material/Lock";
4+
import LockOpenIcon from "@mui/icons-material/LockOpen";
5+
import QuestionMarkIcon from "@mui/icons-material/QuestionMark";
6+
import PropTypes from "prop-types";
7+
import { Component } from "react";
8+
9+
/**
10+
*
11+
* @param {object} props - the props passed to the component
12+
* @param {string} props.source - the source to check
13+
* @returns {Component} an icon indicating whether the source requires authentication or not (or if it is uncertain due to an error fetching the source)
14+
*/
15+
function SourceAuthenticationIcon({ source }) {
16+
const [isFetched, setIsFetched] = useState(false);
17+
const [isAuthenticationRequired, setAuthenticationRequired] = useState(false);
18+
19+
useEffect(() => {
20+
authenticationRequired(source).then((required) => {
21+
setAuthenticationRequired(required);
22+
setIsFetched(true);
23+
});
24+
}, [source]);
25+
26+
if (isFetched) {
27+
if (isAuthenticationRequired === undefined) {
28+
return (
29+
<Tooltip title="Uncertain if authentication is required">
30+
<QuestionMarkIcon size="small" />
31+
</Tooltip>
32+
);
33+
} else if (isAuthenticationRequired) {
34+
return (
35+
<Tooltip title="Authentication required">
36+
<LockIcon size="small" />
37+
</Tooltip>
38+
);
39+
} else {
40+
return (
41+
<Tooltip title="No authentication required">
42+
<LockOpenIcon size="small" />
43+
</Tooltip>
44+
);
45+
}
46+
} else {
47+
return <CircularProgress size={20} />;
48+
}
49+
}
50+
51+
SourceAuthenticationIcon.propTypes = {
52+
source: PropTypes.string.isRequired,
53+
};
54+
55+
/**
56+
* Given a source, check if it requires authentication or not
57+
* @param {string} source - the source to check
58+
* @returns {?boolean} whether the source requires authentication or not, or undefined if it is uncertain
59+
*/
60+
async function authenticationRequired(source) {
61+
try {
62+
const response = await fetch(source, {
63+
method: "HEAD",
64+
});
65+
return response.status === 401 || response.status === 403;
66+
} catch (error) {
67+
return undefined;
68+
}
69+
}
70+
71+
export default SourceAuthenticationIcon;

0 commit comments

Comments
 (0)