diff --git a/webapp/src/App.tsx b/webapp/src/App.tsx
index 95e7bf4a03b578654fee42f780fc3054cffe2728..316b04dc0f1dc0671fc5bdc7b397a9bb918cd1e3 100644
--- a/webapp/src/App.tsx
+++ b/webapp/src/App.tsx
@@ -10,6 +10,8 @@ import FundingSourcePage from "./pages/FundingSource";
import ChargingStructurePage from "./pages/ChargingStructure";
import StaffGraph from "./pages/StaffGraph";
import { FilterSelection } from "./Schema";
+import SubOrganisation from "./pages/SubOrganisation";
+import ParentOrganisation from "./pages/ParentOrganisation";
function App(): ReactElement {
@@ -31,6 +33,8 @@ function App(): ReactElement {
<Route path="/data/employment" element={<StaffGraph filterSelection={filterSelection} setFilterSelection={setFilterSelection} />} />
<Route path="/data/roles" element={<StaffGraph roles filterSelection={filterSelection} setFilterSelection={setFilterSelection} />} />
<Route path="/charging" element={<ChargingStructurePage />} />
+ <Route path="/suborganisations" element={<SubOrganisation filterSelection={filterSelection} setFilterSelection={setFilterSelection} />} />
+ <Route path="/parentorganisation" element={<ParentOrganisation filterSelection={filterSelection} setFilterSelection={setFilterSelection} />} />
<Route path="*" element={<Landing />} />
</Routes>
<GeantFooter />
diff --git a/webapp/src/Schema.tsx b/webapp/src/Schema.tsx
index 4d3a935d71baa6012e7264407c1605866ddc6e70..449752b6489019b3dc9f6585ed7cb5c51c34e913 100644
--- a/webapp/src/Schema.tsx
+++ b/webapp/src/Schema.tsx
@@ -122,4 +122,11 @@ export interface NrenStaffDataset {
stack: string
hidden: boolean
}[]
-}
\ No newline at end of file
+}
+
+export interface Organisation {
+ nren: string,
+ year: number,
+ name: string,
+ role?: string
+}
diff --git a/webapp/src/helpers/dataconversion.tsx b/webapp/src/helpers/dataconversion.tsx
index a40258cf84537ac0b99b06023ecb1bc60c7e6002..62d6e31ad32239cba53f29ff7a7008e952f75db1 100644
--- a/webapp/src/helpers/dataconversion.tsx
+++ b/webapp/src/helpers/dataconversion.tsx
@@ -3,7 +3,7 @@ import {
FundingSource,
FundingSourceDataset,
ChargingStructure,
- ChargingStructureDataset, Budget, BasicDataset, NrenStaff, NrenStaffDataset
+ ChargingStructureDataset, Budget, BasicDataset, NrenStaff, NrenStaffDataset, Organisation
} from "../Schema";
const DEFAULT_CHARGING_STRUCTURE_DATA = [
@@ -244,6 +244,26 @@ export function createChargingStructureDataset(chargingStructureData: ChargingSt
}
+export function createOrganisationDataLookup(organisationEntries: Organisation[]) {
+ const nrenMap = new Map<string, Map<number, Organisation[]>>();
+
+ organisationEntries.forEach(entry => {
+ let nrenEntry = nrenMap.get(entry.nren);
+ if (!nrenEntry) {
+ nrenEntry = new Map<number, Organisation[]>();
+ }
+ let suborgList = nrenEntry.get(entry.year);
+ if (!suborgList) {
+ suborgList = [];
+ }
+ suborgList.push(entry);
+ nrenEntry.set(entry.year, suborgList);
+ nrenMap.set(entry.nren, nrenEntry);
+ });
+ return nrenMap;
+}
+
+
export const createNRENStaffDataset = (data: NrenStaff[], roles: boolean) => {
function CreateDataLookup(data: NrenStaff[]) {
const dataLookup = new Map<string, Map<string, number>>();
diff --git a/webapp/src/pages/CompendiumData.tsx b/webapp/src/pages/CompendiumData.tsx
index 330196e30c4e3db5ae8258b194dde9f6286dd7be..998d8e4399ec32a78add487dfe0eaeeae37bfb54 100644
--- a/webapp/src/pages/CompendiumData.tsx
+++ b/webapp/src/pages/CompendiumData.tsx
@@ -56,8 +56,16 @@ function CompendiumData(): ReactElement {
<span>Types of employment for NRENs</span>
</Link>
</Row>
-
-
+ <Row>
+ <Link to="/suborganisations" className="link-text-underline">
+ <span>NREN Suborganisations</span>
+ </Link>
+ </Row>
+ <Row>
+ <Link to="/parentorganisation" className="link-text-underline">
+ <span>NREN Parent Organisations</span>
+ </Link>
+ </Row>
</div>
</CollapsibleBox>
</div>
diff --git a/webapp/src/pages/ParentOrganisation.tsx b/webapp/src/pages/ParentOrganisation.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..725428e51a8f6b06c0b7182e7ddd1ba072af5b73
--- /dev/null
+++ b/webapp/src/pages/ParentOrganisation.tsx
@@ -0,0 +1,116 @@
+import React, { useEffect, useMemo, useState } from 'react';
+import { Col, Container, Row, Table } from "react-bootstrap";
+import { createOrganisationDataLookup } from "../helpers/dataconversion";
+import Filter from "../components/graphing/Filter"
+
+import {
+ Organisation,
+ FilterSelection
+} from "../Schema";
+
+async function getData(): Promise<Organisation[]> {
+ try {
+ const response = await fetch('/api/organization/parent');
+ return response.json();
+ } catch (error) {
+ console.error(`Failed to load data: ${error}`);
+ throw error;
+ }
+}
+
+function getYearsAndNrens(sourceData: Organisation[]) {
+ const years = new Set<number>();
+ const nrens = new Set<string>();
+ sourceData.forEach(datapoint => {
+ years.add(datapoint.year);
+ nrens.add(datapoint.nren);
+ });
+ return { years: years, nrens: nrens };
+}
+
+interface inputProps {
+ filterSelection: FilterSelection
+ setFilterSelection: React.Dispatch<React.SetStateAction<FilterSelection>>
+}
+
+function ParentOrganisation({ filterSelection, setFilterSelection }: inputProps) {
+ const [organisationData, setOrganisationData] = useState<Organisation[]>();
+
+ const { years, nrens } = useMemo(
+ () => getYearsAndNrens(organisationData || []),
+ [organisationData]
+ );
+
+ const selectedData = (organisationData || []).filter(data =>
+ filterSelection.selectedYears.includes(data.year) && filterSelection.selectedNrens.includes(data.nren)
+ );
+ const organisationDataset = createOrganisationDataLookup(selectedData);
+
+ useEffect(() => {
+ const loadData = async () => {
+ const data = await getData();
+ setOrganisationData(data);
+
+ // filter fallback for when nothing is selected (only last year for all nrens)
+ const { years, nrens } = getYearsAndNrens(data);
+ setFilterSelection(previous => {
+ const visibleYears = previous.selectedYears.filter(year => years.has(year));
+ const newSelectedYears = visibleYears.length ? previous.selectedYears : [Math.max(...years)];
+ const visibleNrens = previous.selectedNrens.filter(nren => nrens.has(nren));
+ const newSelectedNrens = visibleNrens.length ? previous.selectedNrens : [...nrens];
+ return { selectedYears: newSelectedYears, selectedNrens: newSelectedNrens };
+ });
+ }
+ loadData()
+ }, [setFilterSelection]);
+
+ function sortOrganisation(a: Organisation, b: Organisation) {
+ return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
+ }
+
+ return (
+ <Container>
+ <Row>
+ <Col xs={9}>
+ <Row>
+ <h3>NREN Parent Organisations</h3>
+ </Row>
+ <Row>
+ <Table>
+ <thead>
+ <tr>
+ <th>NREN</th>
+ <th>Year</th>
+ <th>Parent Organisation</th>
+ </tr>
+ </thead>
+ <tbody>
+ {Array.from(organisationDataset).sort().map(([_, nrenEntry]) => (
+ Array.from(nrenEntry).sort().map(([_, nrenDataList], yearIndex) => (
+ nrenDataList.sort(sortOrganisation).map((nrenData, nrenDataIndex) => (
+ <tr key={nrenData.nren + nrenData.year + nrenData.name}>
+ <td>{yearIndex == 0 && nrenDataIndex == 0 && nrenData.nren}</td>
+ <td>{nrenDataIndex == 0 && nrenData.year}</td>
+ <td>{nrenData.name}</td>
+ </tr>
+ ))
+ )
+ )
+
+ ))}
+ </tbody>
+ </Table>
+ </Row>
+ </Col>
+ <Col xs={3}>
+ <Filter
+ filterOptions={{ availableYears: [...years], availableNrens: [...nrens] }}
+ filterSelection={filterSelection}
+ setFilterSelection={setFilterSelection}
+ />
+ </Col>
+ </Row>
+ </Container>
+ );
+}
+export default ParentOrganisation;
diff --git a/webapp/src/pages/SubOrganisation.tsx b/webapp/src/pages/SubOrganisation.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..6bc2602bf9fc5f1204d485a4812755d4a63b15a4
--- /dev/null
+++ b/webapp/src/pages/SubOrganisation.tsx
@@ -0,0 +1,118 @@
+import React, { useEffect, useMemo, useState } from 'react';
+import { Col, Container, Row, Table } from "react-bootstrap";
+import { createOrganisationDataLookup } from "../helpers/dataconversion";
+import Filter from "../components/graphing/Filter"
+
+import {
+ Organisation,
+ FilterSelection
+} from "../Schema";
+
+async function getData(): Promise<Organisation[]> {
+ try {
+ const response = await fetch('/api/organization/sub');
+ return response.json();
+ } catch (error) {
+ console.error(`Failed to load data: ${error}`);
+ throw error;
+ }
+}
+
+function getYearsAndNrens(sourceData: Organisation[]) {
+ const years = new Set<number>();
+ const nrens = new Set<string>();
+ sourceData.forEach(datapoint => {
+ years.add(datapoint.year);
+ nrens.add(datapoint.nren);
+ });
+ return { years: years, nrens: nrens };
+}
+
+interface inputProps {
+ filterSelection: FilterSelection
+ setFilterSelection: React.Dispatch<React.SetStateAction<FilterSelection>>
+}
+
+function SubOrganisation({ filterSelection, setFilterSelection }: inputProps) {
+ const [organisationData, setOrganisationData] = useState<Organisation[]>();
+
+ const { years, nrens } = useMemo(
+ () => getYearsAndNrens(organisationData || []),
+ [organisationData]
+ );
+
+ const selectedData = (organisationData || []).filter(data =>
+ filterSelection.selectedYears.includes(data.year) && filterSelection.selectedNrens.includes(data.nren)
+ );
+ const organisationDataset = createOrganisationDataLookup(selectedData);
+
+ useEffect(() => {
+ const loadData = async () => {
+ const data = await getData();
+ setOrganisationData(data);
+
+ // filter fallback for when nothing is selected (only last year for all nrens)
+ const { years, nrens } = getYearsAndNrens(data);
+ setFilterSelection(previous => {
+ const visibleYears = previous.selectedYears.filter(year => years.has(year));
+ const newSelectedYears = visibleYears.length ? previous.selectedYears : [Math.max(...years)];
+ const visibleNrens = previous.selectedNrens.filter(nren => nrens.has(nren));
+ const newSelectedNrens = visibleNrens.length ? previous.selectedNrens : [...nrens];
+ return { selectedYears: newSelectedYears, selectedNrens: newSelectedNrens };
+ });
+ }
+ loadData()
+ }, [setFilterSelection]);
+
+ function sortOrganisation(a: Organisation, b: Organisation) {
+ return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
+ }
+
+ return (
+ <Container>
+ <Row>
+ <Col xs={9}>
+ <Row>
+ <h3>NREN Suborganisations</h3>
+ </Row>
+ <Row>
+ <Table>
+ <thead>
+ <tr>
+ <th>NREN</th>
+ <th>Year</th>
+ <th>Suborganisation</th>
+ <th>Role</th>
+ </tr>
+ </thead>
+ <tbody>
+ {Array.from(organisationDataset).sort().map(([_, nrenEntry]) => (
+ Array.from(nrenEntry).sort().map(([_, nrenDataList], yearIndex) => (
+ nrenDataList.sort(sortOrganisation).map((nrenData, nrenDataIndex) => (
+ <tr key={nrenData.nren + nrenData.year + nrenData.name}>
+ <td>{yearIndex == 0 && nrenDataIndex == 0 && nrenData.nren}</td>
+ <td>{nrenDataIndex == 0 && nrenData.year}</td>
+ <td>{nrenData.name}</td>
+ <td>{nrenData.role}</td>
+ </tr>
+ ))
+ )
+ )
+
+ ))}
+ </tbody>
+ </Table>
+ </Row>
+ </Col>
+ <Col xs={3}>
+ <Filter
+ filterOptions={{ availableYears: [...years], availableNrens: [...nrens] }}
+ filterSelection={filterSelection}
+ setFilterSelection={setFilterSelection}
+ />
+ </Col>
+ </Row>
+ </Container>
+ );
+}
+export default SubOrganisation;