Skip to content
Snippets Groups Projects
Commit dad7a5a6 authored by geant-release-service's avatar geant-release-service
Browse files

Finished release 0.59.

parents be786065 936c4b52
Branches
Tags 0.59
No related merge requests found
Showing
with 11152 additions and 19847 deletions
......@@ -34,4 +34,7 @@ node_modules
/docs/build/
/config.json
.env
\ No newline at end of file
.env
# script output
survey_text.*
\ No newline at end of file
......@@ -2,28 +2,32 @@
All notable changes to this project will be documented in this file.
## [0.59] - 2024-07-10
- COMP-411: Add Matomo tracking for page visits to all Compendium Data pages and survey pages
- COMP-413: Merge survey and compendium data frontends into one
## [0.58] - 2024-04-18
- fix filename and titles for siem vendor and traffic volume page
## [0.57] - 2024-04-17
- COMP-370 : Network - Capacity - total traffic from different sources
- COMP-405 : Traffic Volumes page with 4 graphs
- COMP-406 : Certification services used by NRENs - weird list
- COMP-370: Network - Capacity - total traffic from different sources
- COMP-405: Traffic Volumes page with 4 graphs
- COMP-406: Certification services used by NRENs - weird list
## [0.56] - 2024-04-05
- security control and crisis excersies fix for new 2023 mapping
## [0.55] - 2024-04-05
- COMP-372 : S&P - Standards - Audits
- COMP-373 : S&P - Standards - Business Continuity
- COMP-374 : S&P - Standards - Crisis Management
- COMP-375 : S&P - Standards - Crisis Exercises
- COMP-376 : S&P - Standards - Security controls
- COMP-402 : crisis management shows years with no data
- COMP-395 : typo issue
- COMP-359 : Tables - only show most recent data
- COMP-390 : Remove line dots on the URL pages
- COMP-391 : Fix place to click on side menu to pop out
- COMP-372: S&P - Standards - Audits
- COMP-373: S&P - Standards - Business Continuity
- COMP-374: S&P - Standards - Crisis Management
- COMP-375: S&P - Standards - Crisis Exercises
- COMP-376: S&P - Standards - Security controls
- COMP-402: crisis management shows years with no data
- COMP-395: typo issue
- COMP-359: Tables - only show most recent data
- COMP-390: Remove line dots on the URL pages
- COMP-391: Fix place to click on side menu to pop out
## [0.54] - 2024-03-05
- Close the menu/sidebar when clicking outside of it
......
recursive-include compendium_v2/static *
include compendium_v2/templates/index.html
include compendium_v2/templates/survey-index.html
include compendium_v2/migrations/alembic.ini
recursive-include compendium_v2/migrations/versions *
recursive-include compendium_v2/migrations/surveymodels *
......
This diff is collapsed.
......@@ -2,61 +2,56 @@
"name": "compendium-v2",
"version": "1.0.0",
"devDependencies": {
"@babel/core": "^7.20.5",
"@babel/plugin-transform-runtime": "^7.19.6",
"@babel/preset-env": "^7.20.2",
"@babel/preset-react": "^7.18.6",
"@babel/preset-typescript": "^7.18.6",
"@babel/runtime": "^7.20.6",
"@types/react": "^18.0.26",
"@types/react-dom": "^18.0.9",
"@types/react-router-dom": "^5.3.3",
"@types/webpack": "^5.28.0",
"@typescript-eslint/eslint-plugin": "^5.47.0",
"@typescript-eslint/parser": "^5.47.0",
"babel-loader": "^9.1.0",
"babel-plugin-transform-class-properties": "^6.24.1",
"css-loader": "^6.7.3",
"date-fns": "^2.29.3",
"eslint": "^8.30.0",
"eslint-plugin-react": "^7.31.11",
"eslint-plugin-react-hooks": "^4.6.0",
"fork-ts-checker-webpack-plugin": "^7.2.14",
"image-webpack-loader": "^8.1.0",
"sass": "^1.57.1",
"sass-loader": "^13.2.0",
"style-loader": "^3.3.1",
"ts-node": "^10.9.1",
"typescript": "^4.9.4",
"url-loader": "^4.1.1",
"webpack": "^5.75.0",
"webpack-cli": "^5.0.1",
"webpack-dev-server": "^4.11.1"
},
"resolutions": {
"@types/webpack": "^5.28.0",
"@types/webpack-dev-server": "^4.7.2"
"@babel/core": "~7.24",
"@babel/plugin-proposal-class-properties": "~7.18",
"@babel/plugin-transform-runtime": "~7.24",
"@babel/preset-env": "~7.24.7",
"@babel/preset-react": "~7.24",
"@babel/preset-typescript": "~7.24",
"@babel/runtime": "~7.24",
"@types/react": "~18.3",
"@types/react-dom": "~18.3",
"@types/react-router-dom": "~5.3.3",
"@types/webpack": "~5.28",
"@typescript-eslint/eslint-plugin": "~6.21",
"@typescript-eslint/parser": "~6.21",
"babel-loader": "~9.1.3",
"css-loader": "~6.11.0",
"date-fns": "~3.6.0",
"eslint": "^8.57.0",
"eslint-plugin-react": "~7.34.3",
"eslint-plugin-react-hooks": "~4.6.2",
"fork-ts-checker-webpack-plugin": "~9.0.2",
"mini-css-extract-plugin": "~2.9.0",
"sass": "~1.77.6",
"sass-loader": "~13.3.3",
"style-loader": "~3.3.4",
"ts-node": "~10.9.2",
"typescript": "~5.5.3",
"url-loader": "~4.1.1",
"webpack": "~5.92.1",
"webpack-cli": "~5.1.4",
"webpack-dev-server": "~4.15.2"
},
"scripts": {
"start": "webpack serve --mode development --open --port 4000",
"build": "webpack --mode production"
},
"dependencies": {
"bootstrap": "^5.2.3",
"cartesian-product-multiple-arrays": "^1.0.9",
"chart.js": "^4.2.1",
"chartjs-plugin-datalabels": "^2.2.0",
"core-js": "^3.26.1",
"html-to-image": "^1.11.11",
"install": "^0.13.0",
"npm": "^9.2.0",
"react": "^18.2.0",
"react-bootstrap": "^2.7.0",
"react-chartjs-2": "^5.1.0",
"react-dom": "^18.2.0",
"react-icons": "4.8.0",
"react-router-dom": "^6.5.0",
"xlsx": "^0.18.5"
"bootstrap": "~5.3.3",
"cartesian-product-multiple-arrays": "~1.0.9",
"chart.js": "~4.4.3",
"chartjs-plugin-datalabels": "~2.2.0",
"html-to-image": "~1.11.11",
"react": "~18.3.1",
"react-bootstrap": "~2.10.4",
"react-chartjs-2": "~5.2.0",
"react-dom": "~18.3.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",
"main": "index.js",
......
......@@ -43,6 +43,12 @@ import SNPStandardsCrisisManagementPage from "./pages/SNPStandardsCrisisManageme
import SNPStandardsSecurityControlsPage from "./pages/SNPStandardsSecurityControl";
import NetworkTrafficVolumePage from "./pages/NetworkTrafficVolume";
// Survey pages
import SurveyManagementComponent from './survey/SurveyManagementComponent';
import UserManagementComponent from './survey/UserManagementComponent';
import SurveyContainerComponent from "./survey/SurveyContainerComponent";
import SurveyLanding from "./survey/Landing";
const router = createBrowserRouter([
{ path: "/budget", element: <BudgetPage /> },
......@@ -97,6 +103,12 @@ const router = createBrowserRouter([
{ path: "/crisis-management", element: <SNPStandardsCrisisManagementPage /> },
{ path: "/crisis-exercise", element: <SNPStandardsCrisisExercisesPage /> },
{ path: "/security-control", element: <SNPStandardsSecurityControlsPage /> },
{ path: "survey/admin/surveys", element: <SurveyManagementComponent /> },
{ path: "survey/admin/users", element: <UserManagementComponent /> },
{ path: "survey/admin/inspect/:year", element: <SurveyContainerComponent loadFrom={'/api/response/inspect/'} /> },
{ path: "survey/admin/try/:year", element: <SurveyContainerComponent loadFrom={'/api/response/try/'} /> },
{ path: "survey/response/:year/:nren", element: <SurveyContainerComponent loadFrom={'/api/response/load/'} /> },
{ path: "survey/*", element: <SurveyLanding /> },
{ path: "*", element: <Landing /> },
]);
......
import React, { ReactElement } from "react";
import SidebarProvider from "./helpers/SidebarProvider";
import UserProvider from "./shared/UserProvider";
import UserProvider from "./helpers/UserProvider";
import FilterSelectionProvider from "./helpers/FilterSelectionProvider";
import ChartContainerProvider from "./helpers/ChartContainerProvider";
import PreviewProvider from "./helpers/PreviewProvider";
import NrenProvider from "./helpers/NrenProvider";
import MatomoProvider from "./matomo/MatomoProvider";
import { createInstance } from "./matomo/MatomoTracker";
const matomoInstance = createInstance({
urlBase: 'https://uat-swd-webanalytics01.geant.org/',
siteId: 2,
});
function Providers({ children }): ReactElement {
return (
<SidebarProvider>
<UserProvider>
<FilterSelectionProvider>
<ChartContainerProvider>
<PreviewProvider>
<NrenProvider>
{children}
</NrenProvider>
</PreviewProvider>
</ChartContainerProvider>
</FilterSelectionProvider>
</UserProvider>
</SidebarProvider>
<MatomoProvider value={matomoInstance}>
<SidebarProvider>
<UserProvider>
<FilterSelectionProvider>
<ChartContainerProvider>
<PreviewProvider>
<NrenProvider>
{children}
</NrenProvider>
</PreviewProvider>
</ChartContainerProvider>
</FilterSelectionProvider>
</UserProvider>
</SidebarProvider>
</MatomoProvider>
);
}
......
......@@ -270,4 +270,17 @@ export interface ConnectivityLoad extends NrenAndYearDatapoint {
export interface ConnectivityGrowth extends NrenAndYearDatapoint {
user_category: string,
growth: number
}
export interface User {
id: string,
email: string,
role: string,
name: string,
oidc_sub: string,
nrens: string[],
permissions: {
admin: boolean,
active: boolean,
}
}
\ No newline at end of file
......@@ -13,15 +13,16 @@ import NetworkSidebar from "./NetworkSidebar";
import ConnectedUsersSidebar from "./ConnectedUsersSidebar";
import ServicesSidebar from "./ServicesSidebar";
import DownloadContainer from "../components/DownloadContainer";
import useMatomo from "../matomo/UseMatomo";
ChartJS.defaults.font.size = 16;
ChartJS.defaults.font.family = 'Open Sans';
ChartJS.defaults.font.weight = '700';
ChartJS.defaults.font.weight = 700;
interface inputProps {
title: string,
description?: string|ReactElement,
description?: string | ReactElement,
filter: ReactElement,
children: ReactElement,
category: Sections,
......@@ -33,6 +34,14 @@ function DataPage({ title, description, filter, children, category, data, filena
const preview = usePreview();
const locationWithoutPreview = window.location.origin + window.location.pathname;
const { trackPageView } = useMatomo()
React.useEffect(() => {
trackPageView({
documentTitle: title
})
}, [trackPageView])
return (
<>
{category === Sections.Organisation && <OrganizationSidebar />}
......@@ -53,7 +62,7 @@ function DataPage({ title, description, filter, children, category, data, filena
<p className="p-md-4">{description}</p>
</Row>
{data && filename &&
<Row align="right" style={{position: 'relative'}}>
<Row align="right" style={{ position: 'relative' }}>
<DownloadContainer data={data} filename={filename} />
</Row>}
<Row>
......
import { useContext, useEffect } from "react";
import { PreviewContext } from "./PreviewProvider";
import { useSearchParams } from "react-router-dom";
import { userContext } from "../shared/UserProvider";
import { userContext } from "./UserProvider";
export function usePreview() {
......
import React, { createContext } from 'react'
import MatomoTracker from './MatomoTracker'
export interface MatomoProviderProps {
children?: React.ReactNode
value: MatomoTracker
}
export const MatomoContext = createContext<MatomoTracker | null>(null)
const MatomoProvider: React.FC<MatomoProviderProps> = function ({
children,
value,
}) {
const Context = MatomoContext
return <Context.Provider value={value}>{children}</Context.Provider>
}
export default MatomoProvider
\ No newline at end of file
import { TRACK_TYPES } from './constants'
import {
CustomDimension,
TrackEventParams,
TrackLinkParams,
TrackPageViewParams,
TrackParams,
UserOptions,
} from './types'
class MatomoTracker {
mutationObserver?: MutationObserver
constructor(userOptions: UserOptions) {
if (!userOptions.urlBase) {
throw new Error('Matomo urlBase is required.')
}
if (!userOptions.siteId) {
throw new Error('Matomo siteId is required.')
}
this.initialize(userOptions)
}
private initialize({
urlBase,
siteId,
userId,
trackerUrl,
srcUrl,
disabled,
heartBeat,
requireConsent = false,
configurations = {},
}: UserOptions) {
const normalizedUrlBase =
urlBase[urlBase.length - 1] !== '/' ? `${urlBase}/` : urlBase
if (typeof window === 'undefined') {
return
}
// @ts-expect-error - _paq is defined in the Matomo script
window._paq = window._paq || []
// @ts-expect-error - _paq is defined in the Matomo script
if (window._paq.length !== 0) {
return
}
if (disabled) {
return
}
if (requireConsent) {
this.pushInstruction('requireConsent')
}
this.pushInstruction(
'setTrackerUrl',
trackerUrl ?? `${normalizedUrlBase}matomo.php`,
)
this.pushInstruction('setSiteId', siteId)
if (userId) {
this.pushInstruction('setUserId', userId)
}
Object.entries(configurations).forEach(([name, instructions]) => {
if (instructions instanceof Array) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
this.pushInstruction(name, ...instructions)
} else {
this.pushInstruction(name, instructions)
}
})
// accurately measure the time spent on the last pageview of a visit
if (!heartBeat || (heartBeat && heartBeat.active)) {
this.enableHeartBeatTimer((heartBeat && heartBeat.seconds) ?? 15)
}
const doc = document
const scriptElement = doc.createElement('script')
const scripts = doc.getElementsByTagName('script')[0]
scriptElement.type = 'text/javascript'
scriptElement.async = true
scriptElement.defer = true
scriptElement.src = srcUrl || `${normalizedUrlBase}matomo.js`
if (scripts && scripts.parentNode) {
scripts.parentNode.insertBefore(scriptElement, scripts)
}
}
enableHeartBeatTimer(seconds: number): void {
this.pushInstruction('enableHeartBeatTimer', seconds)
}
private trackEventsForElements(elements: HTMLElement[]) {
if (elements.length) {
elements.forEach((element) => {
element.addEventListener('click', () => {
const { matomoCategory, matomoAction, matomoName, matomoValue } =
element.dataset
if (matomoCategory && matomoAction) {
this.trackEvent({
category: matomoCategory,
action: matomoAction,
name: matomoName,
value: Number(matomoValue),
})
} else {
throw new Error(
`Error: data-matomo-category and data-matomo-action are required.`,
)
}
})
})
}
}
// Tracks events based on data attributes
trackEvents(): void {
const matchString = '[data-matomo-event="click"]'
let firstTime = false
if (!this.mutationObserver) {
firstTime = true
this.mutationObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
// only track HTML elements
if (!(node instanceof HTMLElement)) return
// check the inserted element for being a code snippet
if (node.matches(matchString)) {
this.trackEventsForElements([node])
}
const elements = Array.from(
node.querySelectorAll<HTMLElement>(matchString),
)
this.trackEventsForElements(elements)
})
})
})
}
this.mutationObserver.observe(document, { childList: true, subtree: true })
// Now track all already existing elements
if (firstTime) {
const elements = Array.from(
document.querySelectorAll<HTMLElement>(matchString),
)
this.trackEventsForElements(elements)
}
}
stopObserving(): void {
if (this.mutationObserver) {
this.mutationObserver.disconnect()
}
}
// Tracks events
// https://matomo.org/docs/event-tracking/#tracking-events
trackEvent({
category,
action,
name,
value,
...otherParams
}: TrackEventParams): void {
if (category && action) {
this.track({
data: [TRACK_TYPES.TRACK_EVENT, category, action, name, value],
...otherParams,
})
} else {
throw new Error(`Error: category and action are required.`)
}
}
// Gives consent for tracking, this is required for Matomo to start tracking when requireConsent is set to true
// https://developer.matomo.org/guides/tracking-consent
giveConsent(): void {
this.pushInstruction('setConsentGiven')
}
// Tracks outgoing links to other sites and downloads
// https://developer.matomo.org/guides/tracking-javascript-guide#enabling-download-outlink-tracking
trackLink({ href, linkType = 'link' }: TrackLinkParams): void {
this.pushInstruction(TRACK_TYPES.TRACK_LINK, href, linkType)
}
// Tracks page views
// https://developer.matomo.org/guides/spa-tracking#tracking-a-new-page-view
trackPageView(params?: TrackPageViewParams): void {
this.track({ data: [TRACK_TYPES.TRACK_VIEW], ...params })
}
// Sends the tracked page/view/search to Matomo
track({
data = [],
documentTitle = window.document.title,
href,
customDimensions = false,
}: TrackParams): void {
if (data.length) {
if (
customDimensions &&
Array.isArray(customDimensions) &&
customDimensions.length
) {
customDimensions.map((customDimension: CustomDimension) =>
this.pushInstruction(
'setCustomDimension',
customDimension.id,
customDimension.value,
),
)
}
this.pushInstruction('setCustomUrl', href ?? window.location.href)
this.pushInstruction('setDocumentTitle', documentTitle)
this.pushInstruction(...(data as [string, ...any[]])) // eslint-disable-line @typescript-eslint/no-explicit-any
}
}
/**
* Pushes an instruction to Matomo for execution, this is equivalent to pushing entries into the `_paq` array.
*
* For example:
*
* ```ts
* pushInstruction('setDocumentTitle', document.title)
* ```
* Is the equivalent of:
*
* ```ts
* _paq.push(['setDocumentTitle', document.title]);
* ```
*
* @param name The name of the instruction to be executed.
* @param args The arguments to pass along with the instruction.
*/
pushInstruction(name: string, ...args: any[]): MatomoTracker { // eslint-disable-line @typescript-eslint/no-explicit-any
if (typeof window !== 'undefined') {
// @ts-expect-error - _paq is defined in the Matomo script
window._paq.push([name, ...args])
}
return this
}
}
export function createInstance(params: UserOptions): MatomoTracker {
// if on localhost or not production,
// disable Matomo tracking
if (process.env.NODE_ENV !== 'production' || window.location.hostname === 'localhost') {
params.disabled = true
}
return new MatomoTracker(params)
}
export default MatomoTracker
\ No newline at end of file
import { useCallback, useContext } from 'react'
import { MatomoContext } from './MatomoProvider'
import {
TrackEventParams,
TrackLinkParams,
TrackPageViewParams
} from './types'
function useMatomo() {
const instance = useContext(MatomoContext)
const trackPageView = useCallback(
(params?: TrackPageViewParams) => instance?.trackPageView(params),
[instance],
)
const trackEvent = useCallback(
(params: TrackEventParams) => instance?.trackEvent(params),
[instance],
)
const trackEvents = useCallback(() => instance?.trackEvents(), [instance])
const trackLink = useCallback(
(params: TrackLinkParams) => instance?.trackLink(params),
[instance],
)
const enableLinkTracking = useCallback(() => {
// no link tracking for now
}, [])
const pushInstruction = useCallback(
(name: string, ...args: any[]) => { // eslint-disable-line @typescript-eslint/no-explicit-any
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
instance?.pushInstruction(name, ...args)
},
[instance],
)
return {
trackEvent,
trackEvents,
trackPageView,
trackLink,
enableLinkTracking,
pushInstruction,
}
}
export default useMatomo
\ No newline at end of file
export const TRACK_TYPES = {
TRACK_EVENT: 'trackEvent',
TRACK_LINK: 'trackLink',
TRACK_VIEW: 'trackPageView',
}
\ No newline at end of file
export interface CustomDimension {
id: number
value: string
}
export interface UserOptions {
urlBase: string
siteId: number
userId?: string
trackerUrl?: string
srcUrl?: string
disabled?: boolean
heartBeat?: {
active: boolean
seconds?: number
}
linkTracking?: boolean
configurations?: {
[key: string]: any // eslint-disable-line @typescript-eslint/no-explicit-any
},
requireConsent?: boolean
}
export interface TrackPageViewParams {
documentTitle?: string
href?: string | Location
customDimensions?: boolean | CustomDimension[]
}
export interface TrackParams extends TrackPageViewParams {
data: any[] // eslint-disable-line @typescript-eslint/no-explicit-any
}
export interface TrackEventParams extends TrackPageViewParams {
category: string
action: string
name?: string
value?: number
}
export interface TrackLinkParams {
href: string
linkType?: 'download' | 'link'
}
\ No newline at end of file
......@@ -6,10 +6,20 @@ import Banner from "../components/global/Banner";
import { Link } from "react-router-dom";
import { Sections } from "../helpers/constants";
import { usePreview } from "../helpers/usePreview";
import useMatomo from "../matomo/UseMatomo";
function CompendiumData(): ReactElement {
usePreview();
const { trackPageView } = useMatomo()
React.useEffect(() => {
trackPageView({
documentTitle: 'Compendium Data'
})
}, [trackPageView])
return (
<main className="grow">
<PageHeader type={'data'} />
......
import React, { ReactElement } from "react";
import React, { ReactElement, useEffect } from "react";
import { Link } from "react-router-dom";
import { Card, Container, Row, Col } from "react-bootstrap";
import SectionDataLogo from "../images/home_data_icon.svg";
import SectionReportsLogo from "../images/home_reports_icon.svg";
import useMatomo from "../matomo/UseMatomo";
function Landing(): ReactElement {
const { trackPageView } = useMatomo();
useEffect(() => {
trackPageView({ documentTitle: "GEANT Compendium Landing Page" });
}, [trackPageView]);
return (
<Container className="py-5 grey-container">
<Row>
......@@ -54,8 +61,7 @@ function Landing(): ReactElement {
<Card.Body>
<Card.Title>Compendium Data</Card.Title>
<Card.Text>
The results of the Compendium Surveys data given annually by
NRENs. Statical represetation of the data is available here.
<span>Statistical representation of the annual Compendium Survey data is available here</span>
</Card.Text>
</Card.Body>
</Link>
......
......@@ -2,7 +2,6 @@ import React, { ReactElement } from "react";
import { Col, Container, Row } from "react-bootstrap";
import GeantLogo from "../images/geant_logo_f2020_new.svg";
function ExternalPageNavBar(): ReactElement {
return (
<div className={'external-page-nav-bar'}>
......
import React, { useContext, useEffect } from 'react';
import { Button } from 'react-bootstrap';
import { userContext } from './UserProvider';
import { userContext } from '../helpers/UserProvider';
const Login = () => {
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment