From 4ea87a9854352ad0f5a62dc51ea7c95e2d7068a4 Mon Sep 17 00:00:00 2001 From: Remco Tukker <remco.tukker@geant.org> Date: Tue, 25 Apr 2023 21:01:07 +0200 Subject: [PATCH] first version of organisation pages --- webapp/src/App.tsx | 4 + webapp/src/Schema.tsx | 9 +- webapp/src/helpers/dataconversion.tsx | 22 ++++- webapp/src/pages/CompendiumData.tsx | 12 ++- webapp/src/pages/ParentOrganisation.tsx | 116 +++++++++++++++++++++++ webapp/src/pages/SubOrganisation.tsx | 118 ++++++++++++++++++++++++ 6 files changed, 277 insertions(+), 4 deletions(-) create mode 100644 webapp/src/pages/ParentOrganisation.tsx create mode 100644 webapp/src/pages/SubOrganisation.tsx diff --git a/webapp/src/App.tsx b/webapp/src/App.tsx index 95e7bf4a..316b04dc 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 4d3a935d..449752b6 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 a40258cf..62d6e31a 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 330196e3..998d8e43 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 00000000..725428e5 --- /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 00000000..6bc2602b --- /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; -- GitLab