diff --git a/compendium-frontend/package-lock.json b/compendium-frontend/package-lock.json index 267776450cfb45876883e982c51bae3a6b51e2a7..1b535ab483db730283ac8e930a5cf52e26e70ec5 100644 --- a/compendium-frontend/package-lock.json +++ b/compendium-frontend/package-lock.json @@ -6843,10 +6843,11 @@ } }, "node_modules/mini-css-extract-plugin": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.0.tgz", - "integrity": "sha512-Zs1YsZVfemekSZG+44vBsYTLQORkPMwnlv+aehcxK/NLKC+EGhDB39/YePYYqx/sTk6NnYpuqikhSn7+JIevTA==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.1.tgz", + "integrity": "sha512-+Vyi+GCCOHnrJ2VPS+6aPoXN2k2jgUzDRhTFLjjTBn23qyXJXkjUWQgTL+mXpF5/A8ixLdCc6kWsoeOjKGejKQ==", "dev": true, + "license": "MIT", "dependencies": { "schema-utils": "^4.0.0", "tapable": "^2.2.1" @@ -14768,9 +14769,9 @@ "dev": true }, "mini-css-extract-plugin": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.0.tgz", - "integrity": "sha512-Zs1YsZVfemekSZG+44vBsYTLQORkPMwnlv+aehcxK/NLKC+EGhDB39/YePYYqx/sTk6NnYpuqikhSn7+JIevTA==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.1.tgz", + "integrity": "sha512-+Vyi+GCCOHnrJ2VPS+6aPoXN2k2jgUzDRhTFLjjTBn23qyXJXkjUWQgTL+mXpF5/A8ixLdCc6kWsoeOjKGejKQ==", "dev": true, "requires": { "schema-utils": "^4.0.0", diff --git a/compendium-frontend/package.json b/compendium-frontend/package.json index d8d23656ea6e40a827d7faf99623a8fdb58a5980..bd2d7d1f24fb55cd53de313fac89a3d1580bc8dd 100644 --- a/compendium-frontend/package.json +++ b/compendium-frontend/package.json @@ -47,10 +47,10 @@ "react-bootstrap": "~2.10.4", "react-chartjs-2": "~5.2.0", "react-dom": "~18.3.1", + "react-hot-toast": "~2.4.1", "react-icons": "~5.2.1", "react-router-dom": "~6.24.1", "survey-react-ui": "~1.11.5", - "react-hot-toast": "~2.4.1", "xlsx": "~0.18.5" }, "description": "## development environment", diff --git a/compendium-frontend/src/App.tsx b/compendium-frontend/src/App.tsx index 80fdf7d64b1409bba252ffb48a860b9914057407..1065b9ed40523c6398691442816282a87835bd4c 100644 --- a/compendium-frontend/src/App.tsx +++ b/compendium-frontend/src/App.tsx @@ -48,6 +48,10 @@ import SurveyManagementComponent from './survey/SurveyManagementComponent'; import UserManagementComponent from './survey/UserManagementComponent'; import SurveyContainerComponent from "./survey/SurveyContainerComponent"; import SurveyLanding from "./survey/Landing"; +import SNPServicesOfferedPage from "./pages/SNPServicesOffered"; +import SNPCorporateStrategyPage from "./pages/SNPCorporateStrategy"; +import SNPServiceManagementFrameworkPage from "./pages/SNPServiceManagementFramework"; +import SNPServiceLevelTargetsPage from "./pages/SNPServiceLevelTargets"; const router = createBrowserRouter([ @@ -103,6 +107,10 @@ const router = createBrowserRouter([ { path: "/crisis-management", element: <SNPStandardsCrisisManagementPage /> }, { path: "/crisis-exercise", element: <SNPStandardsCrisisExercisesPage /> }, { path: "/security-control", element: <SNPStandardsSecurityControlsPage /> }, + { path: "/services-offered", element: <SNPServicesOfferedPage /> }, + { path: "/service-management-framework", element: <SNPServiceManagementFrameworkPage /> }, + { path: "/service-level-targets", element: <SNPServiceLevelTargetsPage /> }, + { path: "/corporate-strategy", element: <SNPCorporateStrategyPage /> }, { path: "survey/admin/surveys", element: <SurveyManagementComponent /> }, { path: "survey/admin/users", element: <UserManagementComponent /> }, { path: "survey/admin/inspect/:year", element: <SurveyContainerComponent loadFrom={'/api/response/inspect/'} /> }, diff --git a/compendium-frontend/src/Schema.tsx b/compendium-frontend/src/Schema.tsx index f4f0ba424d307d8928a38c38096c9b8940e5d756..8a13cb3adc54d464a2a14b5bafee02cb3a514580 100644 --- a/compendium-frontend/src/Schema.tsx +++ b/compendium-frontend/src/Schema.tsx @@ -82,6 +82,11 @@ export interface AlienWaveInternal extends NrenAndYearDatapoint { alien_wave_internal: (string | null) } +export interface ServiceManagement extends NrenAndYearDatapoint { + service_management_framework: (string | null), + service_level_targets: (string | null), +} + export interface Standards extends NrenAndYearDatapoint { audits: (string | null), audit_specifics: (string | null), @@ -161,6 +166,10 @@ export interface Policy extends NrenAndYearDatapoint { strategic_plan: string } +export interface CorporateStrategy extends NrenAndYearDatapoint { + strategic_plan: string +} + export interface ConnectedInstitutionURLs extends NrenAndYearDatapoint { urls: string[] } @@ -272,6 +281,11 @@ export interface ConnectivityGrowth extends NrenAndYearDatapoint { growth: number } +export interface ServiceOffered extends NrenAndYearDatapoint { + user_category: string, + service_category: string +} + export interface User { id: string, email: string, diff --git a/compendium-frontend/src/components/NetworkSidebar.tsx b/compendium-frontend/src/components/NetworkSidebar.tsx index a6e6467b745f3760427880c47a2b4f1d2cfd8a52..43781c023979ccd768bdb93a53706bd3ff12b38c 100644 --- a/compendium-frontend/src/components/NetworkSidebar.tsx +++ b/compendium-frontend/src/components/NetworkSidebar.tsx @@ -7,7 +7,7 @@ const NetworkSidebar = () => { return ( <Sidebar> <h5>Network</h5> - <h6 className="section-title" >Connectivity</h6> + <h6 className="section-title" >Capacity</h6> <Row> <Link to="/traffic-volume" className="link-text-underline"> <span>NREN Traffic - NREN Customers & External Networks</span> diff --git a/compendium-frontend/src/components/PolicySidebar.tsx b/compendium-frontend/src/components/PolicySidebar.tsx index 8ac9d21d4d8774dbf30b4b589e87da3a40c8e6ba..d162df630fc0ef40353ac58e8913f4c779b40847 100644 --- a/compendium-frontend/src/components/PolicySidebar.tsx +++ b/compendium-frontend/src/components/PolicySidebar.tsx @@ -38,6 +38,26 @@ const PolicySidebar = () => { <span>Security Controls Used by NRENs</span> </Link> </Row> + <Row> + <Link to="/services-offered" className="link-text-underline"> + <span>Services Offered by NRENs by Types of Users</span> + </Link> + </Row> + <Row> + <Link to="/corporate-strategy" className="link-text-underline"> + <span>NREN Corporate Strategies </span> + </Link> + </Row> + <Row> + <Link to="/service-level-targets" className="link-text-underline"> + <span>NRENs Offering Service Level Targets</span> + </Link> + </Row> + <Row> + <Link to="/service-management-framework" className="link-text-underline"> + <span>NRENs Operating a Formal Service Management</span> + </Link> + </Row> </Sidebar> ) } diff --git a/compendium-frontend/src/helpers/ConnectivityDisplayConstant.tsx b/compendium-frontend/src/helpers/ConnectivityDisplayConstant.tsx index b8ad37623c8b9981d5dcbf88e50f08cf80590e73..57a6a8d4a20ec19e515421c3b004334a41dbe2b5 100644 --- a/compendium-frontend/src/helpers/ConnectivityDisplayConstant.tsx +++ b/compendium-frontend/src/helpers/ConnectivityDisplayConstant.tsx @@ -36,6 +36,11 @@ export const questionMap = { "growth": "What do you expect the traffic growth to be in the next 3 years?", "charging": "What are the typical charging levels for the following types of commercial connections?", "commercial": "What types of commercial organisations do you connect?", + "services": <> + <span> + The table below shows the different types of users served by NRENs. Selecting the institution type will expand the detail to show the categories of services offered by NRENs, with a tick indicating that the NREN offers a specific category of service to the type of user. + </span> + </> } export const dataKeyMap = { @@ -83,6 +88,16 @@ export const dataKeyMap = { "Yes - Including transit to other networks": "yes_incl_other", "Yes - only if sponsored by a connected institution": "yes_if_sponsored", + }, + "services": { + "Identity/T&I": "identity", + "Multimedia": "multimedia", + "Professional services": "professional_services", + "Network services": "network_services", + "Collaboration": "collaboration", + "Security": "security", + "Storage and Hosting": "storage_and_hosting", + "ISP support": "isp_support" } }; diff --git a/compendium-frontend/src/helpers/ConnectivityViewComponent.tsx b/compendium-frontend/src/helpers/ConnectivityViewComponent.tsx index 2fe9b21bf7c716461ca03fd7999131a38bc94753..0c1649b377364e9877f75c28737c20b3fb356d20 100644 --- a/compendium-frontend/src/helpers/ConnectivityViewComponent.tsx +++ b/compendium-frontend/src/helpers/ConnectivityViewComponent.tsx @@ -43,13 +43,18 @@ function TableSectionForTickIcon({ userCategoryMapForCarrier, title, dataKey, ca const data: any[] = []; userCategoryMapForCarrier.forEach((nrensMap: Map<string, Map<string, any>>) => { nrensMap.forEach((yearMap: Map<string, any>) => { + let matchFound = false; yearMap.forEach((entry: any) => { - if (dataKey == entry[category]) + + if (dataKey == entry[category]) { data.push(entry[category]); - else - data.push(null); - }) + matchFound = true; + } + }) + if (!matchFound) { + data.push(null); + } }); }); return <TableRowForTickIcon title={title} data={data} />; diff --git a/compendium-frontend/src/helpers/dataconversion.tsx b/compendium-frontend/src/helpers/dataconversion.tsx index 9e73b337c2b7675cbcb585068f2dd20da1fae746..8519fb78774861ed10882cc4725d099d4c055f6c 100644 --- a/compendium-frontend/src/helpers/dataconversion.tsx +++ b/compendium-frontend/src/helpers/dataconversion.tsx @@ -462,7 +462,31 @@ export function createConnectivityDataLookupForCarrier<Datatype extends NrenAndY if (!nrenEntry) { nrenEntry = new Map<number, Map<string, Datatype>>(); } - let yearEntry = nrenEntry.get(entry['carry_mechanism']); + let yearEntry = nrenEntry.get(entry[enumCategory]); + if (!yearEntry) { + yearEntry = new Map<string, Datatype>(); + } + yearEntry.set(entry[enumCategory], entry) + nrenEntry.set(entry.year, yearEntry); + serviceEntry.set(entry.nren, nrenEntry); + dataLookup.set(entry[columnProperty], serviceEntry); + }); + return dataLookup; +} + +export function createConnectivityDataLookupForServicesOffered<Datatype extends NrenAndYearDatapoint>(data: Datatype[], columnProperty: string, enumCategory) { + const dataLookup = new Map<string, Map<string, Map<number, Map<string, Datatype>>>>(); + + data.forEach(entry => { + let serviceEntry = dataLookup.get(entry[columnProperty]); + if (!serviceEntry) { + serviceEntry = new Map<string, Map<number, Map<string, Datatype>>>(); + } + let nrenEntry = serviceEntry.get(entry.nren); + if (!nrenEntry) { + nrenEntry = new Map<number, Map<string, Datatype>>(); + } + let yearEntry = nrenEntry.get(entry.year); if (!yearEntry) { yearEntry = new Map<string, Datatype>(); } diff --git a/compendium-frontend/src/pages/CompendiumData.tsx b/compendium-frontend/src/pages/CompendiumData.tsx index ee97c8f3cfcb0e79fb787493bd3d42c8d12bf419..8008801a65e90499edee0564e09b1967c9f25824 100644 --- a/compendium-frontend/src/pages/CompendiumData.tsx +++ b/compendium-frontend/src/pages/CompendiumData.tsx @@ -125,7 +125,26 @@ function CompendiumData(): ReactElement { <span>Security Controls Used by NRENs</span> </Link> </Row> - + <Row> + <Link to="/services-offered" className="link-text-underline"> + <span>Services Offered by NRENs by Types of Users</span> + </Link> + </Row> + <Row> + <Link to="/corporate-strategy" className="link-text-underline"> + <span>NREN Corporate Strategies </span> + </Link> + </Row> + <Row> + <Link to="/service-level-targets" className="link-text-underline"> + <span>NRENs Offering Service Level Targets</span> + </Link> + </Row> + <Row> + <Link to="/service-management-framework" className="link-text-underline"> + <span>NRENs Operating a Formal Service Management</span> + </Link> + </Row> </div> </CollapsibleBox> @@ -179,7 +198,7 @@ function CompendiumData(): ReactElement { </CollapsibleBox> <CollapsibleBox title={Sections.Network} startCollapsed> <div className="collapsible-column"> - <h6 className="section-title" >Connectivity</h6> + <h6 className="section-title" >Capacity</h6> <Row> <Link to="/traffic-volume" className="link-text-underline"> <span>NREN Traffic - NREN Customers & External Networks</span> diff --git a/compendium-frontend/src/pages/NetworkAlienWave.tsx b/compendium-frontend/src/pages/NetworkAlienWave.tsx index c7134d50d18f395779ccf6e06779b802a09a8da7..f7647a650e7fc52f0605ef690d86b4c5aad43ea4 100644 --- a/compendium-frontend/src/pages/NetworkAlienWave.tsx +++ b/compendium-frontend/src/pages/NetworkAlienWave.tsx @@ -34,11 +34,13 @@ function NetworkAlienWavePage(): React.ReactElement { <DataPage title="NREN Use of 3rd Party Alienwave/Lightpath Services" description="The table below shows NREN usage of alien wavelength or lightpath services provided by third parties. It does not include alien waves used internally inside the NRENs own networks, as that is covered in another table. - In the optical network world, the term “alien wavelength” or “alien wave” (AW) is used to describe wavelengths in a DWDM line system that pass through the network, i.e. they are not sourced/terminated by the line-system operator’s equipment (hence “alien”). This setup is in contrast to traditional DWDM systems, where the DWDM light source - (transponder) operates in the same management domain as the amplifiers." + (transponder) operates in the same management domain as the amplifiers. + + Where NRENs have given the number of individual alien wavelength services, the figure is available in a hover-over + box. These are indicated by a black line around the coloured marker." category={Sections.Network} filter={filterNode} data={selectedData} filename="alien_wave_nrens_per_year"> <> @@ -62,18 +64,24 @@ function NetworkAlienWavePage(): React.ReactElement { {Array.from(dataLookup.entries()).map(([nren, nrenMap]) => ( <tr key={nren}> <td>{nren}</td> - {["yes", - "planned", - "no"].map(column_key => ( - <td key={column_key}> - {nrenMap.has(column_key) && - showYears.map(year => { - const chargingYears = nrenMap.get(column_key)!; - return <ColorPill key={year} year={year} active={chargingYears.has(year)} tooltip={""}/>; - }) - } - </td> - ))} + {["yes", + "planned", + "no"].map(column_key => ( + <td key={column_key}> + {nrenMap.has(column_key) && + showYears.map(year => { + const chargingYears = nrenMap.get(column_key)!; + const alienWaveInfo = chargingYears.get(year); + return <ColorPill key={year} year={year} active={chargingYears.has(year)} + tooltip={ + alienWaveInfo?.nr_of_alien_wave_third_party_services != null + ? `No. of Individual alien wavelength services = ${alienWaveInfo.nr_of_alien_wave_third_party_services}` + : "" + } />; + }) + } + </td> + ))} </tr> ))} </tbody> diff --git a/compendium-frontend/src/pages/NetworkTrafficVolume.tsx b/compendium-frontend/src/pages/NetworkTrafficVolume.tsx index e2eaf735a6b225a8b8a549b637f96b263662ff81..8575f26b284fa5ce31470190d5b28b82f125bed4 100644 --- a/compendium-frontend/src/pages/NetworkTrafficVolume.tsx +++ b/compendium-frontend/src/pages/NetworkTrafficVolume.tsx @@ -53,7 +53,7 @@ const options = { label: (context) => { let tooltipLines: string[] = []; const datasetLabel = context.dataset.label || ''; - const dataPoint = context.parsed.y; + const dataPoint = `${context.parsed.y} TB `; if (dataPoint !== null) { tooltipLines.push(` ${dataPoint} ${datasetLabel}`); @@ -69,7 +69,7 @@ const options = { y: { ticks: { callback: (value: string | number) => { - return `${value}`; + return `${value} TB`; }, }, }, diff --git a/compendium-frontend/src/pages/SNPCorporateStrategy.tsx b/compendium-frontend/src/pages/SNPCorporateStrategy.tsx new file mode 100644 index 0000000000000000000000000000000000000000..ba856c57a0028dcf1382e0fc04c438a120bcabcc --- /dev/null +++ b/compendium-frontend/src/pages/SNPCorporateStrategy.tsx @@ -0,0 +1,89 @@ +import React, { useContext } from 'react'; +import { Table } from "react-bootstrap"; + +import { CorporateStrategy, Policy } from "../Schema"; +import { createDataLookup, getLatestData } from '../helpers/dataconversion'; +import DataPage from '../components/DataPage'; +import Filter from "../components/graphing/Filter" +import { Sections } from '../helpers/constants'; +import { FilterSelectionContext } from '../providers/FilterSelectionProvider'; +import { useData } from '../helpers/useData'; +import ChartContainer from '../components/graphing/ChartContainer'; + +function getJSXFromMap(data: Map<string, Map<number, CorporateStrategy>>) { + const policies = [ + ['acceptable_use', 'Acceptable Use Policy'], + ['connectivity', 'Connectivity Policy'], + ['data_protection', 'Data Protection Policy'], + ['environmental', 'Environmental Policy'], + ['equal_opportunity', 'Equal Opportunity Policy'], + ['gender_equality', 'Gender Equality Plan'], + ['privacy_notice', 'Privacy Notice'], + ['strategic_plan', 'Strategic Plan'] + ]; + + return Array.from(data.entries()).map(([nren, nrenMap]) => { + return Array.from(nrenMap.entries()).map(([year, policy], yearIndex) => ( + <tr key={nren + year} className='dotted-border'> + <td className='pt-3 nren-column text-nowrap'>{yearIndex == 0 && nren}</td> + <td className='pt-3 year-column'>{year}</td> + <td className='pt-3 blue-column'> + <ul className='no-list-style-type'> + {policies.map(([key, text]) => ( + !!policy[key] && ( + <li key={key}> + <a href={policy[key]} target="_blank" rel="noopener noreferrer" style={{ textDecoration: 'none' }}> + {text} + </a> + </li> + ) + ))} + </ul> + </td> + </tr> + )) + }) +} + +function SNPCorporateStrategyPage() { + const { filterSelection, setFilterSelection } = useContext(FilterSelectionContext); + const { data: fetchedData, years, nrens } = useData<CorporateStrategy>('/api/policy/corporate-strategy', setFilterSelection); + + const policyData = fetchedData ? getLatestData(fetchedData) : []; + + const selectedData = policyData.filter(project => + filterSelection.selectedNrens.includes(project.nren) + ); + const policyDataByYear = createDataLookup(selectedData); + + const filterNode = <Filter + filterOptions={{ availableYears: [], availableNrens: [...nrens.values()] }} + filterSelection={filterSelection} + setFilterSelection={setFilterSelection} + /> + + return ( + <DataPage title="NREN Corporate Strategies" + description='The table below contains links to the NRENs most recent corporate strategic plans. + NRENs are asked if updates have been made to their corporate strategy over the previous year. + To avoid showing outdated links, just the most recent responses are shown.' + category={Sections.Policy} filter={filterNode} + data={selectedData} filename='nren_corporate_strategy'> + <ChartContainer> + <Table borderless className='compendium-table'> + <thead> + <tr> + <th className='nren-column'>NREN</th> + <th className='year-column'>Year</th> + <th className='blue-column'>Corporate Strategy</th> + </tr> + </thead> + <tbody> + {getJSXFromMap(policyDataByYear)} + </tbody> + </Table> + </ChartContainer> + </DataPage> + ) +} +export default SNPCorporateStrategyPage; diff --git a/compendium-frontend/src/pages/SNPServiceLevelTargets.tsx b/compendium-frontend/src/pages/SNPServiceLevelTargets.tsx new file mode 100644 index 0000000000000000000000000000000000000000..93ba6bafbeb2e85707b617d714744de46e592917 --- /dev/null +++ b/compendium-frontend/src/pages/SNPServiceLevelTargets.tsx @@ -0,0 +1,77 @@ +import React, { useContext } from "react"; +import { Table } from "react-bootstrap"; + +import { ServiceManagement } from "../Schema"; +import { createMatrixDataLookup } from "../helpers/dataconversion"; +import ColorPill from "../components/ColorPill"; +import DataPage from "../components/DataPage"; +import Filter from "../components/graphing/Filter"; +import { Sections } from "../helpers/constants"; +import { FilterSelectionContext } from "../providers/FilterSelectionProvider"; +import ChartContainer from "../components/graphing/ChartContainer"; +import { useData } from "../helpers/useData"; + +function SNPServiceLevelTargetsPage(): React.ReactElement { + const { filterSelection, setFilterSelection } = useContext(FilterSelectionContext); + const { data: pertTeamData, years, nrens } = useData<ServiceManagement>('/api/standards-and-policies/service-management', setFilterSelection); + + const selectedData = (pertTeamData).filter(data => + filterSelection.selectedYears.includes(data.year) && filterSelection.selectedNrens.includes(data.nren) + ); + + const dataLookup = createMatrixDataLookup(selectedData, 'service_level_targets'); + console.log(dataLookup); + const filterNode = <Filter + filterOptions={{ availableYears: [...years], availableNrens: [...nrens.values()] }} + filterSelection={filterSelection} + setFilterSelection={setFilterSelection} + coloredYears + /> + + const showYears = [...filterSelection.selectedYears.filter(year => years.has(year))].sort(); + + return ( + <DataPage title="NRENs Offering Service Level Targets" + description="The table below shows which NRENs offer Service Levels Targets for their services. + If NRENs have never responded to this question in the survey, they are excluded. " + category={Sections.Policy} filter={filterNode} + data={selectedData} filename="service_level_targets"> + <> + <ChartContainer> + <Table className="charging-struct-table" striped bordered > + <colgroup> + <col span={1} style={{ width: "30%" }} /> + <col span={1} style={{ width: "35%" }} /> + <col span={1} style={{ width: "35%" }} /> + </colgroup> + <thead> + <tr> + <th></th> + <th>Yes</th> + <th>No</th> + </tr> + </thead> + <tbody> + {Array.from(dataLookup.entries()).map(([nren, nrenMap]) => ( + <tr key={nren}> + <td>{nren}</td> + {["True", "False"].map(column_key => ( + <td key={column_key}> + {nrenMap.has(column_key) && + showYears.map(year => { + const chargingYears = nrenMap.get(column_key)!; + return <ColorPill key={year} year={year} active={chargingYears.has(year)} tooltip={""} />; + }) + } + </td> + ))} + </tr> + ))} + </tbody> + </Table> + </ChartContainer> + </> + </DataPage> + ); +} +export default SNPServiceLevelTargetsPage; diff --git a/compendium-frontend/src/pages/SNPServiceManagementFramework.tsx b/compendium-frontend/src/pages/SNPServiceManagementFramework.tsx new file mode 100644 index 0000000000000000000000000000000000000000..c82e7b5fa0cc9f1d8b6e813e153a2769770e63fe --- /dev/null +++ b/compendium-frontend/src/pages/SNPServiceManagementFramework.tsx @@ -0,0 +1,77 @@ +import React, { useContext } from "react"; +import { Table } from "react-bootstrap"; + +import { ServiceManagement } from "../Schema"; +import { createMatrixDataLookup } from "../helpers/dataconversion"; +import ColorPill from "../components/ColorPill"; +import DataPage from "../components/DataPage"; +import Filter from "../components/graphing/Filter"; +import { Sections } from "../helpers/constants"; +import { FilterSelectionContext } from "../providers/FilterSelectionProvider"; +import ChartContainer from "../components/graphing/ChartContainer"; +import { useData } from "../helpers/useData"; + +function SNPServiceManagementFrameworkPage(): React.ReactElement { + const { filterSelection, setFilterSelection } = useContext(FilterSelectionContext); + const { data: pertTeamData, years, nrens } = useData<ServiceManagement>('/api/standards-and-policies/service-management', setFilterSelection); + + const selectedData = (pertTeamData).filter(data => + filterSelection.selectedYears.includes(data.year) && filterSelection.selectedNrens.includes(data.nren) + ); + + const dataLookup = createMatrixDataLookup(selectedData, 'service_management_framework'); + + const filterNode = <Filter + filterOptions={{ availableYears: [...years], availableNrens: [...nrens.values()] }} + filterSelection={filterSelection} + setFilterSelection={setFilterSelection} + coloredYears + /> + + const showYears = [...filterSelection.selectedYears.filter(year => years.has(year))].sort(); + + return ( + <DataPage title="NRENs Operating a Formal Service Management Framework" + description="The chart below shows which NRENs operate a formal service management framework + for all of their services. NRENs which have never answered this question cannot be selected." + category={Sections.Policy} filter={filterNode} + data={selectedData} filename="service_management_framework"> + <> + <ChartContainer> + <Table className="charging-struct-table" striped bordered > + <colgroup> + <col span={1} style={{ width: "30%" }} /> + <col span={1} style={{ width: "35%" }} /> + <col span={1} style={{ width: "35%" }} /> + </colgroup> + <thead> + <tr> + <th></th> + <th>Yes</th> + <th>No</th> + </tr> + </thead> + <tbody> + {Array.from(dataLookup.entries()).map(([nren, nrenMap]) => ( + <tr key={nren}> + <td>{nren}</td> + {["True", "False"].map(column_key => ( + <td key={column_key}> + {nrenMap.has(column_key) && + showYears.map(year => { + const chargingYears = nrenMap.get(column_key)!; + return <ColorPill key={year} year={year} active={chargingYears.has(year)} tooltip={""} />; + }) + } + </td> + ))} + </tr> + ))} + </tbody> + </Table> + </ChartContainer> + </> + </DataPage> + ); +} +export default SNPServiceManagementFrameworkPage; diff --git a/compendium-frontend/src/pages/SNPServicesOffered.tsx b/compendium-frontend/src/pages/SNPServicesOffered.tsx new file mode 100644 index 0000000000000000000000000000000000000000..c2421a98503c65f077df200e80d0472c4a981aeb --- /dev/null +++ b/compendium-frontend/src/pages/SNPServicesOffered.tsx @@ -0,0 +1,62 @@ +import React, { useContext } from "react"; + +import { ServiceOffered } from "../Schema"; +import { + createConnectivityDataLookupForServicesOffered +} from "../helpers/dataconversion"; +import DataPage from "../components/DataPage"; +import Filter from "../components/graphing/Filter"; +import { Sections } from "../helpers/constants"; +import { FilterSelectionContext } from "../providers/FilterSelectionProvider"; +import ChartContainer from "../components/graphing/ChartContainer"; +import { useData } from "../helpers/useData"; +import { ConnectedUserData } from "../helpers/ConnectivityViewComponent"; +import { questionMap, dataKeyMap, displayTitle } from "../helpers/ConnectivityDisplayConstant"; + +function SNPServicesOfferedPage(): React.ReactElement { + const { filterSelection, setFilterSelection } = useContext(FilterSelectionContext); + const apiUrl = '/api/standards-and-policies/services-offered'; + const { data: connectedProportion, years, nrens } = useData<ServiceOffered>(apiUrl, setFilterSelection); + let isTickIcon = true; + const selectedData = (connectedProportion).filter(data => + filterSelection.selectedYears.includes(data.year) + && filterSelection.selectedNrens.includes(data.nren) + ); + + const connectedProportionDict = {}; + selectedData.forEach(connectedProportion => { + connectedProportionDict[connectedProportion.user_category] = connectedProportion.user_category; + }); + let dataLookup: any; + let carrierCategoryList: any = ['service_category']; + dataLookup = createConnectivityDataLookupForServicesOffered(selectedData, 'user_category', 'service_category'); + + + const filterNode = <Filter + filterOptions={{ availableYears: [...years], availableNrens: [...nrens.values()] }} + filterSelection={filterSelection} + setFilterSelection={setFilterSelection} + /> + + return ( + <DataPage title="Services Offered by NRENs by Types of Users" + description={questionMap["services"]} + category={Sections.Policy} filter={filterNode} + data={selectedData} filename="nren_services_offered"> + <> + <div style={{ fontSize: "16px", marginBottom: "10px", fontWeight: "bold" }}> + </div> + <ChartContainer> + <ConnectedUserData + dataLookup={dataLookup} + filterSelection={filterSelection} + tableDataMap={dataKeyMap["services"]} + isTickIcon={isTickIcon} + carrierCategoryList={carrierCategoryList} + /> + </ChartContainer> + </> + </DataPage> + ); +} +export default SNPServicesOfferedPage; diff --git a/compendium-frontend/src/scss/layout/_components.scss b/compendium-frontend/src/scss/layout/_components.scss index 1090f2c078bd65426a3009f6cdb3840d5683dbe5..4deb06698e201d29601e964c6d17fd34f6e6ff4f 100644 --- a/compendium-frontend/src/scss/layout/_components.scss +++ b/compendium-frontend/src/scss/layout/_components.scss @@ -197,15 +197,15 @@ } $year-colors: ( - #fd7f6f, - #7eb0d5, - #ffee65, - #bd7ebe, - #beb9db, - #b2e061, - #ffb55a, - #fdcce5, - #8bd3c7 + #CE3D5B, + #1B90AC, + #FF8D5A, + #FF1790, + #1E82B6, + #13AC9C, + #5454A8, + #8C6896, + #0069b0, ); $service-tab-colors: ( diff --git a/compendium_v2/routes/policy.py b/compendium_v2/routes/policy.py index 1bd10ae139e2f1f8092afa48228fd3e3d242433b..2ebd74a22c56dcb30edfa479eb79dcc85e3d83d6 100644 --- a/compendium_v2/routes/policy.py +++ b/compendium_v2/routes/policy.py @@ -36,6 +36,26 @@ POLICY_RESPONSE_SCHEMA = { 'items': {'$ref': '#/definitions/policy'} } +CORPORATE_STRATEGY_RESPONSE_SCHEMA = { + '$schema': 'http://json-schema.org/draft-07/schema#', + 'definitions': { + 'strategy': { + 'type': 'object', + 'properties': { + 'nren': {'type': 'string'}, + 'nren_country': {'type': 'string'}, + 'year': {'type': 'integer'}, + 'strategic_plan': {'type': 'string'} + }, + 'required': ['nren', 'nren_country', 'year', 'strategic_plan'], + 'additionalProperties': False + } + }, + + 'type': 'array', + 'items': {'$ref': '#/definitions/strategy'} +} + def policy_extract_data(entry: Policy): return { @@ -70,3 +90,31 @@ def policy_view() -> Any: entries = [policy_extract_data(entry) for entry in common.get_data(Policy)] return jsonify(entries) + + +def corporate_strategy_extract_data(entry: Policy): + return { + 'nren': entry.nren.name, + 'nren_country': entry.nren.country, + 'year': entry.year, + 'strategic_plan': entry.strategic_plan, + } + + +@routes.route('/corporate-strategy', methods=['GET']) +@common.require_accepts_json +def corporate_strategy_view() -> Any: + """ + handler for /api/policy/corporate-strategy requests + returns policy information for each NREN/year combination + + response will be formatted as: + + .. asjson:: + compendium_v2.routes.policy.CORPORATE_STRATEGY_RESPONSE_SCHEMA + + :return: + """ + + entries = [corporate_strategy_extract_data(entry) for entry in common.get_data(Policy)] + return jsonify(entries) diff --git a/compendium_v2/routes/standards_and_policies.py b/compendium_v2/routes/standards_and_policies.py index 3f33bbfffabe4fc0364b448f2c9f887d5d439922..686d46aeb99ea9c24ed16cebc623e735df77f970 100644 --- a/compendium_v2/routes/standards_and_policies.py +++ b/compendium_v2/routes/standards_and_policies.py @@ -1,6 +1,7 @@ from typing import Any -from compendium_v2.db.presentation_models import Standards, CrisisExercises, SecurityControls +from compendium_v2.db.presentation_models import (Standards, CrisisExercises, SecurityControls, + ServiceUserTypes, ServiceManagement) from compendium_v2.routes import common from flask import Blueprint, jsonify @@ -67,6 +68,49 @@ SECURITY_CONTROLS_RESPONSE_SCHEMA = { 'items': {'$ref': '#/definitions/security_controls'} } +SERVICES_USER_TYPES = { + '$schema': 'http://json-schema.org/draft-07/schema#', + 'definitions': { + 'services_user_types': { + 'type': 'object', + 'properties': { + 'nren': {'type': 'string'}, + 'nren_country': {'type': 'string'}, + 'year': {'type': 'integer'}, + 'user_category': {'type': 'string'}, + 'service_category': {'type': 'string'} + + }, + 'required': ['nren', 'nren_country', 'year', 'user_category', 'service_category'], + 'additionalProperties': False + } + }, + + 'type': 'array', + 'items': {'$ref': '#/definitions/services_user_types'} +} + +SERVICE_MANAGEMENT = { + '$schema': 'http://json-schema.org/draft-07/schema#', + 'definitions': { + 'service_management': { + 'type': 'object', + 'properties': { + 'nren': {'type': 'string'}, + 'nren_country': {'type': 'string'}, + 'year': {'type': 'integer'}, + 'service_management_framework': {'type': 'string'}, + 'service_level_targets': {'type': 'string'}, + }, + 'required': ['nren', 'nren_country', 'year', 'service_management_framework', 'service_level_targets'], + 'additionalProperties': False + } + }, + + 'type': 'array', + 'items': {'$ref': '#/definitions/service_management'} +} + def standards_extract_data(standards: Standards) -> dict: return { @@ -202,3 +246,62 @@ def security_controls_view() -> Any: entries.append(security_controls_extract_data(entry)) return jsonify(entries) + + +def service_user_type_extract_data(service_user_types: ServiceUserTypes): + return { + 'nren': service_user_types.nren.name, + 'nren_country': service_user_types.nren.country, + 'year': service_user_types.year, + 'user_category': service_user_types.user_category.value, + 'service_category': service_user_types.service_category.value + if service_user_types.service_category is not None and service_user_types.service_category.value else '', + } + + +@routes.route('/services-offered', methods=['GET']) +@common.require_accepts_json +def service_user_types_view() -> Any: + """ + handler for /api/standards-and-policies/services-offered/ requests + returns policy information for each NREN/year combination + + response will be formatted as: + + .. asjson:: + compendium_v2.routes.standards_and_policies.SERVICES_USER_TYPES + + :return: + """ + + entries = [service_user_type_extract_data(entry) for entry in common.get_data(ServiceUserTypes)] + return jsonify(entries) + + +def service_management_extract_data(service_management: ServiceManagement): + return { + 'nren': service_management.nren.name, + 'nren_country': service_management.nren.country, + 'year': service_management.year, + 'service_management_framework': str(service_management.service_management_framework), + 'service_level_targets': str(service_management.service_level_targets) + } + + +@routes.route('/service-management', methods=['GET']) +@common.require_accepts_json +def service_management_view() -> Any: + """ + handler for /api/standards-and-policies/service-management/ requests + returns policy information for each NREN/year combination + + response will be formatted as: + + .. asjson:: + compendium_v2.routes.standards_and_policies.SERVICE_MANAGEMENT + + :return: + """ + + entries = [service_management_extract_data(entry) for entry in common.get_data(ServiceManagement)] + return jsonify(entries) diff --git a/setup.py b/setup.py index 643cdc9d16ddf0346d0cc2dce12fa679519f7361..1404f55f27c96f0cfa0b95977fe93261e45ebc19 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name='compendium-v2', - version="0.64", + version="0.65", author='GEANT', author_email='swd@geant.org', description='Flask and React project for displaying '