Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [2.1.0] - 2025-09-25

### Added

- Screencast for the Onto-DESIDE use case (#228).
- Presentation as presented during the SemDev Workhop co-located with SEMANTiCS 2025 (#234).

### Fixed

- Link in table result header is no longer arbitrary if more than one predicate has the same object (#230.)
- Avoided "Error getting variable options..." in templated queries with indirect sources to which the user has no read access (#231).
- Corrected fetch status in templated queries with indirect sources to which the user has no read access (#232).

## [2.0.0] - 2025-05-29

### Added
Expand Down Expand Up @@ -270,4 +283,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[1.6.0]: https://github.com/SolidLabResearch/miravi-a-linked-data-viewer/releases/tag/v1.6.0
[1.7.0]: https://github.com/SolidLabResearch/miravi-a-linked-data-viewer/releases/tag/v1.7.0
[2.0.0]: https://github.com/SolidLabResearch/miravi-a-linked-data-viewer/releases/tag/v2.0.0
[Unreleased]: https://github.com/SolidLabResearch/miravi-a-linked-data-viewer/compare/v2.0.0...HEAD
[2.1.0]: https://github.com/SolidLabResearch/miravi-a-linked-data-viewer/releases/tag/v2.1.0
[Unreleased]: https://github.com/SolidLabResearch/miravi-a-linked-data-viewer/compare/v2.1.0...HEAD
37 changes: 25 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ Rooted in the Latin *mirari* ("to look with wonder"), it turns fragmented knowle

<img src="./doc/miravi.png" width="200">

Miravi is fully configurable. In the illustration below, [this configuration](./main/configs/onto-deside/config.json) is at work.

![A screencast about configs/onto-deside](doc/screencast-onto-deside.gif)

For a more complete presentation of Miravi and description of the design choices, please have a look at our
[presentation as presented during the SemDev Workhop co-located with SEMANTiCS 2025](./doc/slides-miravi-semdev-2025.pdf).

Table of contents:

* [Preface](#preface)
Expand All @@ -27,9 +34,10 @@ Table of contents:
* [Custom queries](#custom-queries)
* [Representation Mapper](#representation-mapper)
* [Advanced topics](#advanced-topics)
* [Adding your own configuration](#adding-your-own-configuration)
* [Converting custom queries into common queries](#converting-custom-queries-into-common-queries)
* [Illustrations](#illustrations)
* [For developers](#for-developers)
* [Adding your own configuration](#adding-your-own-configuration)
* [Testing](#testing)
* [Additional prerequisites](#additional-prerequisites)
* [Testing the production version](#testing-the-production-version)
Expand Down Expand Up @@ -366,6 +374,18 @@ They've already got styling matching that of `react-admin` and are easy to use.

## Advanced topics

### Adding your own configuration

The easiest way to add your own configuration is:

1. Get inspired by the configuration in `main/configs/demo`.
2. Choose your `<your-config>`: a string obeying regex `[a-z0-9-]+`; directory `main/configs/<your-config>` should not yet be in use.
3. Add your own queries in the `main/configs/<your-config>/public/queries` directory and in general, your own resources in the `main/configs/<your-config>/public` directory.
4. Add your own additional resources in `main/configs/<your-config>/public`, if the defaults you'll get from `main/config-defaults/public` are not satisfactory for you.
5. Write your own `main/configs/<your-config>/config.json` file, following the [configuration file documentation above](#configuration-file).
6. Run or build as documented above for the `demo` configuration, of course now using `<your-config>`.
7. Consider a pull request to add your configuration to this repo.

### Converting custom queries into common queries

Once you have your basic configuration working, you may extend it with custom queries interactively with the query editor
Expand All @@ -384,19 +404,12 @@ Follow these steps to get started:
5. **Adapt any other properties** according to your preferences.
6. **Save `main/configs/<your-config>/config.json`**, rerun or rebuild and refresh your browser to test.

## For developers
## Illustrations

### Adding your own configuration
* [A screencast about configs/onto-deside](doc/screencast-onto-deside.gif)
* [Presentation as presented during the SemDev Workhop co-located with SEMANTiCS 2025](./doc/slides-miravi-semdev-2025.pdf)

The easiest way to add your own configuration is:

1. Get inspired by the configuration in `main/configs/demo`.
2. Choose your `<your-config>`: a string obeying regex `[a-z0-9-]+`; directory `main/configs/<your-config>` should not yet be in use.
3. Add your own queries in the `main/configs/<your-config>/public/queries` directory and in general, your own resources in the `main/configs/<your-config>/public` directory.
4. Add your own additional resources in `main/configs/<your-config>/public`, if the defaults you'll get from `main/config-defaults/public` are not satisfactory for you.
5. Write your own `main/configs/<your-config>/config.json` file, following the [configuration file documentation above](#configuration-file).
6. Run or build as documented above for the `demo` configuration, of course now using `<your-config>`.
7. Consider a pull request to add your configuration to this repo.
## For developers

### Testing

Expand Down
Binary file added doc/screencast-onto-deside.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added doc/slides-miravi-semdev-2025.pdf
Binary file not shown.
58 changes: 58 additions & 0 deletions main/configs/test/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,38 @@
"queryLocation": "/sourceQueries/index_example_common_lt.rq"
}
},
{
"id": "9081",
"queryGroupId": "gr-test",
"queryLocation": "component_material_one_variable.rq",
"name": "Component and materials - 1 variable (indirect source & indirect variables; one unauthorized source)",
"description": "Query components (including details about materials) with the sources obtained from index files and variables from the sources. One unauthorized indirect source, to check lenient while getting variable options.",
"indirectVariables": {
"queryLocations": [
"variableQueries/components_name_variable.rq"
]
},
"sourcesIndex": {
"url": "http://localhost:8080/example/index-example-with-unauthorized-source-lt",
"queryLocation": "/sourceQueries/index_example_common_lt.rq"
}
},
{
"id": "9082",
"queryGroupId": "gr-test",
"queryLocation": "component_material_one_variable.rq",
"name": "Component and materials - 1 variable (indirect source & indirect variables; no indirect sources found)",
"description": "Query components (including details about materials) with zero sources obtained from index files and variables from the sources.",
"indirectVariables": {
"queryLocations": [
"variableQueries/components_name_variable.rq"
]
},
"sourcesIndex": {
"url": "http://localhost:8080/example/index-example-texon-only-lt",
"queryLocation": "/sourceQueries/index_example_common_lt_bad.rq"
}
},
{
"id": "9090",
"queryGroupId": "gr-test",
Expand Down Expand Up @@ -549,6 +581,32 @@
"http://localhost:8080/example/favourite-musicians-file-does-not-exist"
]
}
},
{
"id": "9200",
"queryGroupId": "gr-test",
"queryLocation": "schema_name.rq",
"name": "A query that looks for names that are the object of predicate schema:name",
"description": "Tests a single link in the 'name' column header.",
"comunicaContext": {
"sources": [
"http://localhost:8080/example/names-labels"
],
"lenient": true
}
},
{
"id": "9201",
"queryGroupId": "gr-test",
"queryLocation": "schema_name_rdfs_label.rq",
"name": "A query that looks for names that are both the objects of predicates schema:name and rdfs:label",
"description": "Tests two links in the 'name' column header.",
"comunicaContext": {
"sources": [
"http://localhost:8080/example/names-labels"
],
"lenient": true
}
}
]
}
6 changes: 6 additions & 0 deletions main/configs/test/public/queries/schema_name.rq
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
PREFIX schema: <http://schema.org/>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>

SELECT ?name WHERE {
?s schema:name ?name.
}
7 changes: 7 additions & 0 deletions main/configs/test/public/queries/schema_name_rdfs_label.rq
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
PREFIX schema: <http://schema.org/>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>

SELECT ?name WHERE {
?s schema:name ?name.
?s rdfs:label ?name.
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>

SELECT DISTINCT ?source WHERE {
?s rdfs:seeAlso_hihihahahoho ?source.
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import PendingIcon from '@mui/icons-material/Pending';
import CheckIcon from '@mui/icons-material/Check';
import CancelIcon from "@mui/icons-material/Cancel";
import RemoveCircleOutlineIcon from '@mui/icons-material/RemoveCircleOutline';
Expand All @@ -14,7 +15,13 @@ import comunicaEngineWrapper from '../../../comunicaEngineWrapper/comunicaEngine
function SourceFetchStatusIcon({ source }) {
const status = comunicaEngineWrapper.getFetchStatusNumber(source);

if (comunicaEngineWrapper.getFetchSuccess(source)) {
if (comunicaEngineWrapper.getFetchSuccess(source) === undefined) {
return (
<Tooltip title="Not fetched">
<PendingIcon size="small" />
</Tooltip>
);
} else if (comunicaEngineWrapper.getFetchSuccess(source)) {
return (
<Tooltip title="Fetch was successful">
<CheckIcon size="small" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,16 +60,18 @@ function TableHeader({ children }) {
</span>
</Tooltip>}
{!!variableOntology && 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>
variableOntology[child.props.source].map((link) => (
<Link
target="_blank"
href={link}
sx={{ height: "100%", margin: "0 0 0 5px", "& > *": { verticalAlign: "middle" } }}
>
<LinkIcon
fontSize="small"
sx={{ height: "100%", color: "gray" }}
/>
</Link>
))
)}
{sort.field === child.props.source && (
<>
Expand Down
12 changes: 7 additions & 5 deletions main/src/comunicaEngineWrapper/comunicaEngineWrapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ class ComunicaEngineWrapper {
this._fetchSuccess = {};
this._fetchStatusNumber = {};
this._underlyingFetchFunction = undefined;
// LOG console.log(`Comunica engines reset`);
}

getFetchSuccess(arg) {
Expand Down Expand Up @@ -168,13 +169,14 @@ class ComunicaEngineWrapper {
* @param {array} httpProxies - array of httpProxy definitions
*/
_prepareQuery(context, httpProxies) {
// avoid faulty fetch status for sources cached in Comunica
for (const source of context.sources) {
this._fetchSuccess[source] = true;
}
this._underlyingFetchFunction = fetch;
// note: there is no need to preset this._fetchSuccess[source] here;
// if Comunica caches, we still have the previous value
if (getDefaultSession().info.isLoggedIn) {
this._underlyingFetchFunction = authFetch;
// LOG console.log(`Using authFetch as underlying fetch function`);
} else {
this._underlyingFetchFunction = fetch;
// LOG console.log(`Using fetch as underlying fetch function`);
}
context.fetch = ComunicaEngineWrapper._getWrappedFetchFunction(this._underlyingFetchFunction, httpProxies, this);
}
Expand Down
80 changes: 42 additions & 38 deletions main/src/dataProvider/SparqlDataProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -189,10 +189,10 @@ function replaceVariables(rawText, variableValues) {
}

/**
* Given a query and an object, this function returns the predicate of the object in the query.
* Given a query and an object, this function returns the predicates of the object in the query.
*
* @param {object} query - the parsed query in which the predicate is to be looked for.
* @returns {object} an object with the variable as key and the predicate as value.
* @returns {object} an object with the variable as key and as value an array of predicates.
*/
function findPredicates(query) {
const ontologyMapper = {};
Expand All @@ -204,7 +204,11 @@ function findPredicates(query) {
if (part.triples) {
for (const triple of part.triples) {
if (triple.predicate.termType !== "Variable") {
ontologyMapper[triple.object.value] = triple.predicate.value;
if (!ontologyMapper[triple.object.value]) {
ontologyMapper[triple.object.value] = [triple.predicate.value];
} else if (!ontologyMapper[triple.object.value].includes(triple.predicate.value)) {
ontologyMapper[triple.object.value].push(triple.predicate.value);
}
}
}
}
Expand Down Expand Up @@ -293,22 +297,19 @@ async function getSourcesFromSourcesIndex(sourcesIndex, httpProxies) {

const bindingsStream = await comunicaEngineWrapper.queryBindings(queryStringIndexSource,
{ lenient: true, sources: [sourcesIndex.url] }, httpProxies, { engine: "link-traversal" });
await new Promise((resolve, reject) => {
bindingsStream.on('data', (bindings) => {
// LOG console.log(`getSourcesFromSourcesIndex bindings: ${bindings.toString()}`);
for (const term of bindings.values()) { // check for 1st value
const source = term.value;
if (!sourcesList.includes(source)) {
// LOG console.log(`getSourcesFromSourcesIndex adding source: ${source}`);
sourcesList.push(source);
}
// we only want the first term, whatever the variable's name is (note: a for ... of loop seems the only way to access it)
break;
const bindingsArray = await bindingsStream.toArray();
for (const bindings of bindingsArray) {
// LOG console.log(`getSourcesFromSourcesIndex bindings: ${bindings.toString()}`);
for (const term of bindings.values()) { // check for 1st value
const source = term.value;
if (!sourcesList.includes(source)) {
// LOG console.log(`getSourcesFromSourcesIndex adding source: ${source}`);
sourcesList.push(source);
}
});
bindingsStream.on('end', resolve);
bindingsStream.on('error', reject);
});
// we only want the first term, whatever the variable's name is (note: a for ... of loop seems the only way to access it)
break;
}
}
}
catch (error) {
throw new Error(`Error adding sources from index: ${error.message}`);
Expand Down Expand Up @@ -380,6 +381,9 @@ async function getVariableOptions(query) {
}
// END duplicated chunk of code

if (query.comunicaContext.sources.length === 0) {
throw new Error(`Error getting variable options... No sources found.`);
}

let variableOptions;
let queryStringList = [];
Expand Down Expand Up @@ -417,35 +421,35 @@ async function getVariableOptions(query) {

try {
for (const queryString of queryStringList) {
// queryBindings with lenient true to avoid errors with unauthorized sources
const bindingsStream = await comunicaEngineWrapper.queryBindings(queryString,
{ sources: query.comunicaContext.sources }, query.httpProxies);
await new Promise((resolve, reject) => {
bindingsStream.on('data', (bindings) => {
// LOG console.log(`getVariableOptions bindings: ${bindings.toString()}`);
for (const [variable, term] of bindings) {
const name = variable.value;
if (!variableOptions[name]) {
variableOptions[name] = [];
}
const variableValue = termToSparqlCompatibleString(term);
if (variableValue && !variableOptions[name].includes(variableValue)) {
// LOG console.log(`getVariableOptions adding variable option for '${name}': ${variableValue}`);
variableOptions[name].push(variableValue);
}
{ lenient: true, sources: query.comunicaContext.sources }, query.httpProxies);
// convert stream to array (works when no bindings found - handling events 'data', 'end' and 'error' does not work when no bindints found)
const bindingsArray = await bindingsStream.toArray();
for (const bindings of bindingsArray) {
// LOG console.log(`getVariableOptions bindings: ${bindings.toString()}`);
for (const [variable, term] of bindings) {
const name = variable.value;
if (!variableOptions[name]) {
variableOptions[name] = [];
}
});
bindingsStream.on('end', resolve);
bindingsStream.on('error', reject);
});
const variableValue = termToSparqlCompatibleString(term);
if (variableValue && !variableOptions[name].includes(variableValue)) {
// LOG console.log(`getVariableOptions adding variable option for '${name}': ${variableValue}`);
variableOptions[name].push(variableValue);
}
}
}
}
}
catch (error) {
throw new Error(`Error getting variable options... ${error.message}`);
}

if (variableOptions == {}) {
throw new Error(`Error getting variable options... The variable options are empty`);
if (Object.keys(variableOptions).length === 0) {
throw new Error(`Error getting variable options... No variable options found.`);
}

return variableOptions;
}

Expand Down
Loading