Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
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
4 changes: 3 additions & 1 deletion base/templates/base/_header.html
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,9 @@
<div class="dropdown-menu" aria-labelledby="navbarDropdownOntology">
<a class="dropdown-item" href="{% url 'ontology:index' %}">Overview</a>
<a class="dropdown-item"
href="{% url 'ontology:oeo-classes' 'oeo' 'BFO_0000001' %}">OEO Classes</a>
href="{% url 'ontology:ontology-entity-search' 'oeo' %}">OEO Search</a>
<a class="dropdown-item"
href="{% url 'ontology:oeo-class-detail' 'oeo' 'BFO_0000001' %}">OEO Entity Details</a>
<a class="dropdown-item" href="{% url 'oeo_viewer:index' %}">OEO Viewer</a>
<a class="dropdown-item" href="{% url 'ontology:oeo-s-c' %}">OEO Steering Committee</a>
<a class="dropdown-item"
Expand Down
12 changes: 6 additions & 6 deletions modelview/templates/modelview/modellist.html
Original file line number Diff line number Diff line change
Expand Up @@ -91,13 +91,13 @@ <h3>Fields</h3>
{% block factsheets_content %}
{% if label == "Model" %}
<div>
The <a href="{% url 'ontology:oeo-classes' 'oeo' 'OEO_00000277' %}">model factsheet</a> characterise models used in energy system analysis in a
structured way regarding important characteristics, such as their scope and <a href="{% url 'ontology:oeo-classes' 'oeo' 'OEO_00020015' %}">licence</a>
and their developing <a href="{% url 'ontology:oeo-classes' 'oeo' 'OEO_00030022' %}">institutions</a>. You are developing a model? Then you can inform
The <a href="{% url 'ontology:oeo-class-detail' 'oeo' 'OEO_00000277' %}">model factsheet</a> characterise models used in energy system analysis in a
structured way regarding important characteristics, such as their scope and <a href="{% url 'ontology:oeo-class-detail' 'oeo' 'OEO_00020015' %}">licence</a>
and their developing <a href="{% url 'ontology:oeo-class-detail' 'oeo' 'OEO_00030022' %}">institutions</a>. You are developing a model? Then you can inform
about its characteristics here. The models available as a factsheet can be integrated as an element into a <a href="{% url 'factsheet:factsheets_index' %}">scenario bundle</a>
which weave together important information about <a href="{% url 'ontology:oeo-classes' 'oeo' 'OEO_00000364' %}">scenarios</a>, such as the model used for conducting a
<a href="{% url 'ontology:oeo-classes' 'oeo' 'OEO_00010262' %}">scenario projection</a>, <a href="{% url 'ontology:oeo-classes' 'oeo' 'IAO_0000027' %}">data</a>
provided on the OEP, and <a href="{% url 'ontology:oeo-classes' 'oeo' 'OEO_00020012' %}">publications</a>.
which weave together important information about <a href="{% url 'ontology:oeo-class-detail' 'oeo' 'OEO_00000364' %}">scenarios</a>, such as the model used for conducting a
<a href="{% url 'ontology:oeo-class-detail' 'oeo' 'OEO_00010262' %}">scenario projection</a>, <a href="{% url 'ontology:oeo-class-detail' 'oeo' 'IAO_0000027' %}">data</a>
provided on the OEP, and <a href="{% url 'ontology:oeo-class-detail' 'oeo' 'OEO_00020012' %}">publications</a>.
</div>
<br>
{% endif %}
Expand Down
50 changes: 50 additions & 0 deletions ontology/frontend/src/App.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// SPDX-FileCopyrightText: 2025 Jonas Huber <https://github.com/jh-RLI> © Reiner Lemoine Institut
// SPDX-License-Identifier: AGPL-3.0-or-later
import React from 'react';
import { BrowserRouter, Routes, Route, Navigate, useParams } from 'react-router-dom';
import { EuiProvider } from '@elastic/eui';
import TssConfigProvider from './features/terminology/config/TssConfigProvider';

// Your Pages
import EntitySearchPage from './pages/EntitySearchPage';
import OeoIriPages from './pages/OeoIriPages'; // Reusing your IRI page

// // Helper to extract parameters for the detail page
// const EntityDetailWrapper = () => {
// const { ontology, short_form } = useParams();
// // Construct the full IRI or pass the short_form to your component
// const iri = `https://openenergyplatform.org/ontology/${ontology}/${short_form}`;
// return <OeoIriPages iri={iri} />;
// };

const EntityDetailWrapper = () => {
// We no longer construct the IRI here!
// We let OeoIriPages do the smart resolution.
return <OeoIriPages />;
};

export default function App() {
return (
<EuiProvider colorMode="light">
<TssConfigProvider>
{/* Base must match the Django app mount point */}
<BrowserRouter basename="/ontology">
<Routes>
{/* Captured Variables:
:ontology -> "oeo"
*/}
<Route path=":ontology/entities" element={<EntitySearchPage />} />

{/* Captured Variables:
:ontology -> "oeo"
:short_form -> "OEO_00000040"
*/}
<Route path=":ontology/:short_form" element={<EntityDetailWrapper />} />

<Route path="*" element={<div>Page not found</div>} />
</Routes>
</BrowserRouter>
</TssConfigProvider>
</EuiProvider>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// SPDX-FileCopyrightText: 2025 Jonas Huber <https://github.com/jh-RLI> © Reiner Lemoine Institut
// SPDX-License-Identifier: AGPL-3.0-or-later

// ontology/frontend/src/features/terminology/components/TssEntityInfo.jsx
import React from "react";
import { EntityInfoWidget } from "@ts4nfdi/terminology-service-suite";
import { useTssConfig } from "../hooks/useTssConfig";

export default function TssEntityInfo({
iri,
ontologyId,
entityType = "class", // Default, but can be overridden
onNavigateToEntity,
onNavigateToOntology
}) {
const { apiBase, ontology: configOntology } = useTssConfig();
// const activeOntology = ontologyId || configOntology;

// Check if it's an external term by looking at the IRI
const isExternal = iri.includes("purl.obolibrary.org");

return (
<EntityInfoWidget
api={apiBase}
ontologyId={isExternal ? "" : ontologyId}
iri={iri}
entityType={entityType}
hasTitle={false} // We handle the title in the page layout
showBadges={true}
useLegacy={true}
parameter=""
onNavigateToDisambiguate={() => { }}
onNavigateToEntity={onNavigateToEntity || (() => { })}
onNavigateToOntology={onNavigateToOntology || (() => { })}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// SPDX-FileCopyrightText: 2025 Jonas Huber <https://github.com/jh-RLI> © Reiner Lemoine Institut
// SPDX-License-Identifier: AGPL-3.0-or-later

import React from "react";
import { useQuery } from "react-query";
import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiText } from "@elastic/eui";
import { useTssConfig } from "../hooks/useTssConfig";

export default function TssEntityNavButtons({ iri, ontologyId, onNavigate }) {
const { apiBase } = useTssConfig();

// URL-encode the IRI as required by the TIB API path
const encodedIri = encodeURIComponent(encodeURIComponent(iri));

// Construct base URL (handle global vs specific ontology routing)
const baseUrl = ontologyId
? `${apiBase}ontologies/${ontologyId}/terms/${encodedIri}`
: `${apiBase}terms/${encodedIri}`;

// Fetch Parents
const { data: parents, isLoading: loadingParents } = useQuery(
["entityParents", ontologyId, iri],
() => fetch(`${baseUrl}/parents`).then((res) => (res.ok ? res.json() : null)),
{ enabled: !!iri }
);

// Fetch Children
const { data: children, isLoading: loadingChildren } = useQuery(
["entityChildren", ontologyId, iri],
() => fetch(`${baseUrl}/children`).then((res) => (res.ok ? res.json() : null)),
{ enabled: !!iri }
);

// Extract the arrays from the HAL JSON response format
const parentTerms = parents?._embedded?.terms || [];
const childTerms = children?._embedded?.terms || [];

if (loadingParents || loadingChildren) {
return <EuiLoadingSpinner size="m" />;
}

if (parentTerms.length === 0 && childTerms.length === 0) {
return null; // Don't render anything if no hierarchy exists
}

return (
<EuiFlexGroup wrap responsive={false} gutterSize="s" alignItems="center">
{/* Parents (Up) */}
{parentTerms.length > 0 && (
<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize="xs" wrap responsive={false}>
{parentTerms.map((parent) => (
<EuiFlexItem key={parent.iri} grow={false}>
<EuiButton
size="s"
iconType="arrowUp"
onClick={() => onNavigate(parent)}
>
{parent.label || parent.short_form}
</EuiButton>
</EuiFlexItem>
))}
</EuiFlexGroup>
</EuiFlexItem>
)}

{/* Separator if both exist */}
{parentTerms.length > 0 && childTerms.length > 0 && (
<EuiFlexItem grow={false}>
<EuiText color="subdued" size="s">|</EuiText>
</EuiFlexItem>
)}

{/* Children (Down) */}
{childTerms.length > 0 && (
<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize="xs" wrap responsive={false}>
{childTerms.map((child) => (
<EuiFlexItem key={child.iri} grow={false}>
<EuiButton
size="s"
iconType="arrowDown"
iconSide="right"
onClick={() => onNavigate(child)}
>
{child.label || child.short_form}
</EuiButton>
</EuiFlexItem>
))}
</EuiFlexGroup>
</EuiFlexItem>
)}
</EuiFlexGroup>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// SPDX-FileCopyrightText: 2025 Jonas Huber <https://github.com/jh-RLI> © Reiner Lemoine Institut
// SPDX-License-Identifier: AGPL-3.0-or-later

// ontology/frontend/src/features/terminology/components/TssEntityRelations.jsx
import React from "react";
import { EntityRelationsWidget } from "@ts4nfdi/terminology-service-suite";
import { useTssConfig } from "../hooks/useTssConfig";

export default function TssEntityRelations({
iri,
ontologyId,
entityType = "term", // "term" often works best for relations to show instances too
onNavigateToEntity
}) {
const { apiBase, ontology: configOntology } = useTssConfig();
const activeOntology = ontologyId || configOntology;

return (
<EntityRelationsWidget
api={apiBase}
ontologyId={activeOntology}
iri={iri}
entityType={entityType}
hasTitle={true}
showBadges={true}
parameter=""
onNavigateToDisambiguate={() => { }}
onNavigateToEntity={onNavigateToEntity || (() => { })}
onNavigateToOntology={() => { }}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// SPDX-FileCopyrightText: 2025 Jonas Huber <https://github.com/jh-RLI> © Reiner Lemoine Institut
// SPDX-License-Identifier: AGPL-3.0-or-later

import React from "react";
import { IriWidget } from "@ts4nfdi/terminology-service-suite";

export default function TssIriWidget({ iri }) {
return (
<IriWidget
iri={iri}
copyButton="left"
color="text"
externalIcon={true}
className=""
iriText=""
urlPrefix=""
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// SPDX-FileCopyrightText: 2025 Jonas Huber <https://github.com/jh-RLI> © Reiner Lemoine Institut
// SPDX-License-Identifier: AGPL-3.0-or-later

import React, { useMemo } from "react";
import { SearchResultsListWidget } from "@ts4nfdi/terminology-service-suite";
import { useTssConfig } from "../hooks/useTssConfig";

export default function TssSearchResultsList({
ontologyId,
query = "d*",
onNavigateToEntity,
onNavigateToOntology,
}) {
const { apiBase, ontology: configOntology } = useTssConfig();

const activeOntology = ontologyId || configOntology;

const parameter = useMemo(() => {
return `collection=nfdi4energy&ontology=${activeOntology}&fieldList=description,label,iri,ontology_name,type,short_form`;
}, [activeOntology]);

return (
<SearchResultsListWidget
api={apiBase}
ontologyId={activeOntology}
parameter={parameter}
query={query}
initialItemsPerPage={10}
itemsPerPageOptions={[10, 25, 50, 100]}
useLegacy={true}
preselected={[]}
// Setting targetLink to an empty string disables the default <a> href behavior in the TSS widget,
// allowing our custom onNavigateToEntity handler to process the click and route locally.
targetLink=""
onNavigateToEntity={onNavigateToEntity}
onNavigateToOntology={onNavigateToOntology || (() => { })}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// SPDX-FileCopyrightText: 2025 Jonas Huber <https://github.com/jh-RLI> © Reiner Lemoine Institut
// SPDX-License-Identifier: AGPL-3.0-or-later

import React, { createContext, useContext, useMemo } from "react";

const TssConfigCtx = createContext(null);

export function TssConfigProvider({
children,
// read from Vite env (fallback to OLS API + OEO)
apiBase = import.meta.env.VITE_TSS_API_BASE ?? "",
ontology = import.meta.env.VITE_TSS_DEFAULT_ONTOLOGY ?? "",
lang = import.meta.env.VITE_TSS_LANG ?? "en",
requestHeaders = {},
}) {
const value = useMemo(() => ({ apiBase, ontology, lang, requestHeaders }), [
apiBase, ontology, lang, requestHeaders,
]);
return <TssConfigCtx.Provider value={value}>{children}</TssConfigCtx.Provider>;
}

export function useTssConfigCtx() {
const ctx = useContext(TssConfigCtx);
if (!ctx) throw new Error("useTssConfigCtx must be used within <TssConfigProvider>");
return ctx;
}

export default TssConfigProvider;
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// SPDX-FileCopyrightText: 2025 Jonas Huber <https://github.com/jh-RLI> © Reiner Lemoine Institut
// SPDX-License-Identifier: AGPL-3.0-or-later

import { useTssConfigCtx } from "../config/TssConfigProvider";

export function useTssConfig() {
// later you can read from env and merge, but for now just return the ctx
return useTssConfigCtx();
}
20 changes: 20 additions & 0 deletions ontology/frontend/src/features/terminology/services/tssClient.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// SPDX-FileCopyrightText: 2025 Jonas Huber <https://github.com/jh-RLI> © Reiner Lemoine Institut
// SPDX-License-Identifier: AGPL-3.0-or-later

import axios from "axios";

// Small helper to build a client bound to the current config
export function makeTssClient({ apiBase, requestHeaders = {} }) {
const http = axios.create({
baseURL: apiBase.replace(/\/+$/, "/"), // ensure trailing slash
headers: { ...requestHeaders },
timeout: 20_000,
});

return {
// Example endpoints – adjust to the suite/gateway you use
search: (params) => http.get("search", { params }),
concept: (iri) => http.get("concept", { params: { iri } }),
hierarchy: (iri) => http.get("hierarchy", { params: { iri } }),
};
}
Loading