From e958883a89fa59ba4a1d1577306f885080896ee1 Mon Sep 17 00:00:00 2001 From: "saket.agrahari" <saket.agrahari@geant.org> Date: Wed, 12 Apr 2023 03:27:07 +0100 Subject: [PATCH] COMP-119: charging structure be fe migration test --- .../background_task/parse_excel_data.py | 47 ++++- compendium_v2/db/model.py | 19 ++ ...1a8f4c_adding_charging_structure_schema.py | 33 ++++ .../publishers/survey_publisher_v1.py | 13 ++ compendium_v2/routes/api.py | 2 + compendium_v2/routes/charging.py | 68 ++++++++ test/conftest.py | 18 ++ test/data/ChargingStructureTestData.csv | 85 +++++++++ test/test_charging_structure.py | 13 ++ test/test_survey_publisher_v1.py | 3 + webapp/src/App.tsx | 3 + webapp/src/Schema.tsx | 20 +++ webapp/src/helpers/dataconversion.tsx | 93 +++++++++- webapp/src/pages/ChargingStructure.tsx | 164 ++++++++++++++++++ webapp/src/pages/CompendiumData.tsx | 6 + webapp/tsconfig.json | 4 +- 16 files changed, 586 insertions(+), 5 deletions(-) create mode 100644 compendium_v2/migrations/versions/b123f21a8f4c_adding_charging_structure_schema.py create mode 100644 compendium_v2/routes/charging.py create mode 100644 test/data/ChargingStructureTestData.csv create mode 100644 test/test_charging_structure.py create mode 100644 webapp/src/pages/ChargingStructure.tsx diff --git a/compendium_v2/background_task/parse_excel_data.py b/compendium_v2/background_task/parse_excel_data.py index 69f54c6a..3e337765 100644 --- a/compendium_v2/background_task/parse_excel_data.py +++ b/compendium_v2/background_task/parse_excel_data.py @@ -2,6 +2,7 @@ import openpyxl import os import logging +from compendium_v2.db.model import FeeType from compendium_v2.environment import setup_logging setup_logging() @@ -14,7 +15,6 @@ EXCEL_FILE = os.path.join( def fetch_budget_excel_data(): - # load the xlsx file sheet_name = "1. Budget" wb = openpyxl.load_workbook(EXCEL_FILE, data_only=True, read_only=True) @@ -43,7 +43,6 @@ def fetch_budget_excel_data(): def fetch_funding_excel_data(): - # load the xlsx file wb = openpyxl.load_workbook(EXCEL_FILE, data_only=True, read_only=True) @@ -112,3 +111,47 @@ def fetch_funding_excel_data(): # For 2020 yield from create_points_for_year(8, 50, 2020, 3) + + +def fetch_charging_structure_excel_data(): + # load the xlsx file + wb = openpyxl.load_workbook(EXCEL_FILE, data_only=True, read_only=True) + + # select the active worksheet + sheet_name = "3. Charging mechanism" + ws = wb[sheet_name] + + # iterate over the rows in the worksheet + def create_points_for_year(start_row, end_row, year, col_start): + for row in range(start_row, end_row): + # extract the data from the row + nren = ws.cell(row=row, column=col_start).value + charging_structure = ws.cell(row=row, column=col_start + 1).value + logger.info( + f'NREN: {nren}, Charging Structure: {charging_structure},' + f' Year: {year}') + if charging_structure is not None: + if "do not charge" in charging_structure: + charging_structure = FeeType.no_charge.value + elif "combination" in charging_structure: + charging_structure = FeeType.combination.value + elif "flat" in charging_structure: + charging_structure = FeeType.flat_fee.value + elif "usage-based" in charging_structure: + charging_structure = FeeType.usage_based_fee.value + elif "Other" in charging_structure: + charging_structure = FeeType.other.value + else: + charging_structure = None + + logger.info( + f'NREN: {nren}, Charging Structure: {charging_structure},' + f' Year: {year}') + + yield nren, year, charging_structure + + # For 2021 + yield from create_points_for_year(3, 45, 2021, 2) + + # For 2019 + yield from create_points_for_year(3, 45, 2019, 6) diff --git a/compendium_v2/db/model.py b/compendium_v2/db/model.py index 45d4758d..9936dfb8 100644 --- a/compendium_v2/db/model.py +++ b/compendium_v2/db/model.py @@ -1,5 +1,6 @@ import logging import sqlalchemy as sa +from enum import Enum from typing import Any @@ -30,3 +31,21 @@ class FundingSource(base_schema): gov_public_bodies = sa.Column(sa.Numeric(asdecimal=False), nullable=False) commercial = sa.Column(sa.Numeric(asdecimal=False), nullable=False) other = sa.Column(sa.Numeric(asdecimal=False), nullable=False) + + +class FeeType(Enum): + flat_fee = "flat_fee" + usage_based_fee = "usage_based_fee" + combination = "combination" + no_charge = "no_charge" + other = "other" + + +class ChargingStructure(base_schema): + __tablename__ = 'charging_structure' + nren = sa.Column(sa.String(128), primary_key=True) + year = sa.Column(sa.Integer, primary_key=True) + fee_type = sa.Column('fee_type', sa.Enum("flat_fee", "usage_based_fee", + "combination", "no_charge", + "other", + name="fee_type"), nullable=True) diff --git a/compendium_v2/migrations/versions/b123f21a8f4c_adding_charging_structure_schema.py b/compendium_v2/migrations/versions/b123f21a8f4c_adding_charging_structure_schema.py new file mode 100644 index 00000000..e200725c --- /dev/null +++ b/compendium_v2/migrations/versions/b123f21a8f4c_adding_charging_structure_schema.py @@ -0,0 +1,33 @@ +"""adding charging structure schema + +Revision ID: b123f21a8f4c +Revises: b70ada054046 +Create Date: 2023-04-05 22:02:02.248236 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'b123f21a8f4c' +down_revision = 'b70ada054046' +branch_labels = None +depends_on = None + + +def upgrade(): + op.create_table( + 'charging_structure', + sa.Column('nren', sa.String(length=128), nullable=False), + sa.Column('year', sa.Integer(), nullable=False), + sa.Column('fee_type', sa.Enum("flat_fee", "usage_based_fee", + "combination", "no_charge", + "other", + name="fee_type"), nullable=True), + sa.PrimaryKeyConstraint('nren', 'year') + ) + + +def downgrade(): + op.drop_table('charging_structure') diff --git a/compendium_v2/publishers/survey_publisher_v1.py b/compendium_v2/publishers/survey_publisher_v1.py index 6c6320f8..59716ce9 100644 --- a/compendium_v2/publishers/survey_publisher_v1.py +++ b/compendium_v2/publishers/survey_publisher_v1.py @@ -82,10 +82,23 @@ def db_funding_migration(): session.commit() +def db_charging_structure_migration(): + with db.session_scope() as session: + # Import the data to database + data = parse_excel_data.fetch_charging_structure_excel_data() + + for (nren, year, charging_structure) in data: + charging_structure_entry = model.ChargingStructure( + nren=nren, year=year, fee_type=charging_structure) + session.merge(charging_structure_entry) + session.commit() + + def _cli(config): init_db(config) db_budget_migration() db_funding_migration() + db_charging_structure_migration() @click.command() diff --git a/compendium_v2/routes/api.py b/compendium_v2/routes/api.py index bfb4c1b7..31acbc8d 100644 --- a/compendium_v2/routes/api.py +++ b/compendium_v2/routes/api.py @@ -17,10 +17,12 @@ from flask import Blueprint from compendium_v2.routes import common from compendium_v2.routes.budget import routes as budget_routes from compendium_v2.routes.funding import routes as funding_routes +from compendium_v2.routes.charging import routes as charging_routes routes = Blueprint('compendium-v2-api', __name__) routes.register_blueprint(budget_routes, url_prefix='/budget') routes.register_blueprint(funding_routes, url_prefix='/funding') +routes.register_blueprint(charging_routes, url_prefix='/charging') logger = logging.getLogger(__name__) diff --git a/compendium_v2/routes/charging.py b/compendium_v2/routes/charging.py new file mode 100644 index 00000000..a0583e40 --- /dev/null +++ b/compendium_v2/routes/charging.py @@ -0,0 +1,68 @@ +import logging + +from flask import Blueprint, jsonify, current_app +from compendium_v2 import db +from compendium_v2.routes import common +from compendium_v2.db import model +from typing import Any + +routes = Blueprint('charging', __name__) + + +@routes.before_request +def before_request(): + config = current_app.config['CONFIG_PARAMS'] + dsn_prn = config['SQLALCHEMY_DATABASE_URI'] + db.init_db_model(dsn_prn) + + +logger = logging.getLogger(__name__) + +CHARGING_STRUCTURE_RESPONSE_SCHEMA = { + '$schema': 'http://json-schema.org/draft-07/schema#', + + 'definitions': { + 'charging': { + 'type': 'object', + 'properties': { + 'NREN': {'type': 'string'}, + 'YEAR': {'type': 'integer'}, + 'FEE_TYPE': {'type': ["string", "null"]}, + }, + 'required': ['NREN', 'YEAR'], + 'additionalProperties': False + } + }, + + 'type': 'array', + 'items': {'$ref': '#/definitions/charging'} +} + + +@routes.route('/', methods=['GET']) +@common.require_accepts_json +def charging_structure_view() -> Any: + """ + handler for /api/charging/ requests + + response will be formatted as: + + .. asjson:: + compendium_v2.routes.charging.CHARGING_STRUCTURE_RESPONSE_SCHEMA + + :return: + """ + + def _extract_data(entry: model.ChargingStructure): + return { + 'NREN': entry.nren, + 'YEAR': int(entry.year), + 'FEE_TYPE': entry.fee_type, + } + + with db.session_scope() as session: + entries = sorted([_extract_data(entry) + for entry in session.query(model.ChargingStructure) + .all()], + key=lambda d: (d['NREN'], d['YEAR'])) + return jsonify(entries) diff --git a/test/conftest.py b/test/conftest.py index bc36503c..34205939 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -149,3 +149,21 @@ def client(data_config_filename, mocked_db, mocked_survey_db): os.environ['SETTINGS_FILENAME'] = data_config_filename with compendium_v2.create_app().test_client() as c: yield c + + +@pytest.fixture +def test_charging_structure_data(): + with db.session_scope() as session: + data = _test_data_csv("ChargingStructureTestData.csv") + for row in data: + nren = row["nren"] + year = row["year"] + fee_type = row["fee_type"] + if fee_type == "null": + fee_type = None + + session.add( + model.ChargingStructure( + nren=nren, year=year, + fee_type=fee_type) + ) diff --git a/test/data/ChargingStructureTestData.csv b/test/data/ChargingStructureTestData.csv new file mode 100644 index 00000000..077661fe --- /dev/null +++ b/test/data/ChargingStructureTestData.csv @@ -0,0 +1,85 @@ +nren,year,fee_type +ACOnet,2021,usage_based_fee +AMRES,2021,no_charge +ANA,2021,combination +ARNES,2021,no_charge +ASNET-AM,2021,combination +BASNET,2021,combination +Belnet,2021,no_charge +BREN,2021,no_charge +CARNET,2021,no_charge +CESNET,2021,flat_fee +CYNET,2021,flat_fee +DeIC,2021,combination +DFN,2021,flat_fee +EENet,2021,no_charge +FCCN,2021,usage_based_fee +GARR,2021,no_charge +GRENA,2021,flat_fee +GRNET S.A.,2021,no_charge +HEAnet,2021,no_charge +Jisc,2021,usage_based_fee +LITNET,2021,no_charge +MARnet,2021,no_charge +MREN,2021,no_charge +PIONIER,2021,flat_fee +RedIRIS,2021,no_charge +RENAM,2021,combination +RENATER,2021,flat_fee +RESTENA,2021,flat_fee +RoEduNet,2021,no_charge +SANET,2021,flat_fee +SWITCH,2021,combination +ULAKBIM,2021,no_charge +ACOnet,2019,flat_fee +AMRES,2019,no_charge +ANA,2019,combination +ARNES,2019,no_charge +AzScienceNet,2019,no_charge +BASNET,2019,combination +BELNET,2019,flat_fee +BREN,2019,no_charge +CARNet,2019,no_charge +CESNET,2019,no_charge +DFN,2019,no_charge +EENet,2019,no_charge +FCCN,2019,no_charge +Funet,2019,combination +GARR,2019,no_charge +GRENA,2019,flat_fee +GRNET S.A.,2019,no_charge +HEAnet,2019,combination +Jisc,2019,flat_fee +LITNET,2019,no_charge +MARNET,2019,usage_based_fee +MREN,2019,no_charge +PIONIER,2019,flat_fee +RedIRIS,2019,no_charge +RENAM,2019,combination +RENATER,2019,no_charge +RESTENA,2019,no_charge +RoEduNet,2019,no_charge +SANET,2019,flat_fee +SURFnet,2019,combination +SWITCH,2019,combination +ULAKBIM,2019,no_charge +AzScienceNet,2021,null +LAT,2021,null +RHnet,2021,null +SURFnet,2021,null +Uninett,2021,null +UoM/RicerkaNet,2021,null +ASNET,2019,null +CYNET,2019,null +DeIC,2019,null +KIFU (NIIF),2019,null +LANET,2019,null +RhNET,2019,null +UNINETT,2019,null +UoM,2019,null +Funet,2021,other +IUCC,2021,other +KIFU,2021,other +SUNET,2021,other +IUCC,2019,other +SUNET,2019,other diff --git a/test/test_charging_structure.py b/test/test_charging_structure.py new file mode 100644 index 00000000..c530e6df --- /dev/null +++ b/test/test_charging_structure.py @@ -0,0 +1,13 @@ +import json +import jsonschema +from compendium_v2.routes.charging import CHARGING_STRUCTURE_RESPONSE_SCHEMA + + +def test_charging_structure_response(client, test_charging_structure_data): + rv = client.get( + '/api/charging/', + headers={'Accept': ['application/json']}) + assert rv.status_code == 200 + result = json.loads(rv.data.decode('utf-8')) + jsonschema.validate(result, CHARGING_STRUCTURE_RESPONSE_SCHEMA) + assert result diff --git a/test/test_survey_publisher_v1.py b/test/test_survey_publisher_v1.py index 08461f1f..2f669937 100644 --- a/test/test_survey_publisher_v1.py +++ b/test/test_survey_publisher_v1.py @@ -20,3 +20,6 @@ def test_publisher(client, mocker, dummy_config): assert budget_count funding_source_count = session.query(model.FundingSource.year).count() assert funding_source_count + charging_structure_count = session.query(model.ChargingStructure.year)\ + .count() + assert charging_structure_count diff --git a/webapp/src/App.tsx b/webapp/src/App.tsx index 3e2e1b9d..5dbd117b 100644 --- a/webapp/src/App.tsx +++ b/webapp/src/App.tsx @@ -7,6 +7,8 @@ import DataAnalysis from "./pages/DataAnalysis"; import AnnualReport from "./pages/AnnualReport"; import CompendiumData from "./pages/CompendiumData"; import FundingSourcePage from "./pages/FundingSource"; +import ChargingStructurePage from "./pages/ChargingStructure"; + function App(): ReactElement { return ( @@ -19,6 +21,7 @@ function App(): ReactElement { <Route path="/analysis" element={<DataAnalysis />} /> <Route path="/report" element={<AnnualReport />} /> <Route path="/funding" element={<FundingSourcePage />} /> + <Route path="/charging" element={<ChargingStructurePage />} /> <Route path="*" element={<Landing />} /> </Routes> </Router> diff --git a/webapp/src/Schema.tsx b/webapp/src/Schema.tsx index 380237d6..76558fd2 100644 --- a/webapp/src/Schema.tsx +++ b/webapp/src/Schema.tsx @@ -53,6 +53,26 @@ export interface FundingSourceDataset { }[] } +export interface ChargingStructure{ + NREN: string, + YEAR: number, + FEE_TYPE: (string | null), +} + +export interface ChargingStructureDataset { + labels: string[], + datasets: { + label: string, + data: { x: number | null; y: number | null; }[], + backgroundColor: string + borderRadius: number, + borderSkipped: boolean, + barPercentage: number, + borderWidth: number, + categoryPercentage: number, + }[] +} + export interface DataEntrySection { name: string, description: string, diff --git a/webapp/src/helpers/dataconversion.tsx b/webapp/src/helpers/dataconversion.tsx index a3bb36ac..edfb615a 100644 --- a/webapp/src/helpers/dataconversion.tsx +++ b/webapp/src/helpers/dataconversion.tsx @@ -2,7 +2,9 @@ import { cartesianProduct } from 'cartesian-product-multiple-arrays'; import { FundingSource, - FundingSourceDataset + FundingSourceDataset, + ChargingStructure, + ChargingStructureDataset } from "../Schema"; const DEFAULT_FUNDING_SOURCE_DATA = [ @@ -16,6 +18,13 @@ const DEFAULT_FUNDING_SOURCE_DATA = [ "YEAR": 0, "id": 0 }] +const DEFAULT_CHARGING_STRUCTURE_DATA = [ + { + "NREN": "", + "YEAR": 0, + "FEE_TYPE": "", + } +] function getColorMap() { const rgbToHex = (r: number, g: number, b: number) => '#' + [r, g, b].map(x => { @@ -99,4 +108,84 @@ export const createFundingSourceDataset = (fundingSourcesData: FundingSource[]) labels: labelsNREN.map(l => l.toString()) } return dataResponse; -} \ No newline at end of file +} + + +function createChargingStructureDataLookup(data: ChargingStructure[]) { + let dataLookup = new Map<string, (string|null)>(); + data.forEach((item: ChargingStructure) => { + const lookupKey = `${item.NREN}/${item.YEAR}` + dataLookup.set(lookupKey, item.FEE_TYPE) + }) + return dataLookup; +} + +export function coordinateLookupForFeeStructure(feeType:string,nren:string, + yearIndex:number,nrenIndex:number){ + switch(feeType){ + case "flat_fee": + return {x:(0+((yearIndex+1)*2)),y:(nrenIndex+1),feeType:feeType} + case "usage_based_fee": + return {x:(5+((yearIndex+1)*2)),y:(nrenIndex+1),feeType:feeType} + case "combination": + return {x:(10+((yearIndex+1)*2)),y:(nrenIndex+1),feeType:feeType} + case "no_charge": + return {x:(15+((yearIndex+1)*2)),y:(nrenIndex+1),feeType:feeType} + case "other": + return {x:(20+((yearIndex+1)*2)),y:(nrenIndex+1),feeType:feeType} + default: + return {x:0,y:0} + } +} + +export function lookupColorForYearIndex(yearIndex:number){ + switch(yearIndex){ + case 0: + return "rgba(244, 144, 28, 1)" + case 1: + return "rgba(140, 168, 128, 1)" + default: + return "rgba(0, 0, 0, 0)" + } +} + +export function createChargingStructureDataset(chargingStructureData: ChargingStructure[]) { + const data = chargingStructureData ?? DEFAULT_CHARGING_STRUCTURE_DATA; + const dataLookup = createChargingStructureDataLookup(data) + + const labelsNREN = [...new Set(data.map((item: ChargingStructure) => item.NREN))]; + const labelsYear = [...new Set(data.map((item: ChargingStructure) => item.YEAR))]; + const sizeOfYear = labelsYear.length; + const chargingStructureDataset = labelsYear.map(function (year,yearIndex) { + return { + label: "(" + year + ")", + data: labelsNREN.map((nren,nrenIndex) => { + // ensure that the data is in the same order as the labels + const lookupKey = `${nren}/${year}` + const fee_type =dataLookup.get(lookupKey) ?? "" + return coordinateLookupForFeeStructure(fee_type,nren,yearIndex,nrenIndex) + + }), + backgroundColor: lookupColorForYearIndex(yearIndex), + borderColor: lookupColorForYearIndex(yearIndex), + pointRadius:20, + usePointStyle: true, + pointStyle: 'rectRounded', + pointBackgroundColor: lookupColorForYearIndex(yearIndex), + borderRadius: 0, + borderSkipped: false, + barPercentage: 0.5, + borderWidth: 0.5, + categoryPercentage: 0.8 + } + } + ) + + const dataResponse: ChargingStructureDataset = { + datasets: chargingStructureDataset, + labels: labelsNREN.map(l => l.toString()) + } + return dataResponse; + + +} diff --git a/webapp/src/pages/ChargingStructure.tsx b/webapp/src/pages/ChargingStructure.tsx new file mode 100644 index 00000000..a90b3e7c --- /dev/null +++ b/webapp/src/pages/ChargingStructure.tsx @@ -0,0 +1,164 @@ +import React, { useEffect, useState } from "react"; +import { Container, Row, Col } from "react-bootstrap"; +import Chart, { ChartOptions, Plugin, ChartTypeRegistry, ScatterDataPoint } from 'chart.js'; +import { Bubble, Scatter } from "react-chartjs-2"; +import { createChargingStructureDataset } from "../helpers/dataconversion"; +import { ChargingStructure, ChargingStructureDataset } from "../Schema"; +import { + Chart as ChartJS, + CategoryScale, + LinearScale, + BarElement, + Title, + Tooltip, + Legend, + scales, +} from 'chart.js'; +ChartJS.register( + CategoryScale, + LinearScale, + BarElement, + Title, + Tooltip, + Legend +); + +const EMPTY_DATASET = { datasets: [], labels: [] }; + +const DATA_COUNT = 7; +const NUMBER_CFG = {count: DATA_COUNT, rmin: 1, rmax: 1, min: 0, max: 100}; +const test_data = { + labels: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'], + datasets: [{ + label: 'Weekly Sales', + data: [ + { x: 1, y: 5 }, + { x: 3, y: 0 }, + { x: 3, y: 2 }, + { x: 5, y: 3 }, + { x: 7, y: 4 } + ], + backgroundColor: 'rgba(244, 144, 28, 1)', + borderColor: 'rgba(244, 144, 28, 1)', + borderWidth: 0, + pointRadius: 40, + usePointStyle: true, + pointStyle: 'rectRounded', + }, + { + label: 'Monthly Sales', + data: [ + { x: 2, y: 1 }, + { x: 1, y: 2 }, + { x: 6, y: 3 }, + { x: 8, y: 4 }, + { x: 9, y: 5 } + ], + backgroundColor: 'rgba(140, 168, 128, 1)', + borderColor: 'rgba(140, 168, 128, 1)', + borderWidth: 0, + pointRadius: 40, + usePointStyle: true, + pointStyle: 'rectRounded', + }] + }; + +const plugin = { + id: 'customCanvasBackgroundColor', + beforeDraw: (chart:any, args:any, options:any) => { + const {ctx} = chart; + ctx.save(); + ctx.globalCompositeOperation = 'destination-over'; + ctx.fillStyle = options.color || '#99ffff'; + ctx.fillRect(0, 0, chart.width, chart.height); + ctx.restore(); + } + }; + + +const options = { + maintainAspectRatio: false, + layout: { + padding: { + left: 100, + top: 100, + } + }, + plugins: { + legend: { + display: false, + }, + customCanvasBackgroundColor: { + color: 'lightGreen', + } + }, + scales: { + x: { + position: "top" as const, + ticks: { + display: false, + stepSize: 5, + }, + }, + y: { + grid: { + display: false + }, + ticks: { + display: false, + stepSize: 5, + }, + }, + } + } + + +async function getData(): Promise<ChargingStructure[]> { + try { + const response = await fetch('/api/charging/'); + return response.json(); + } catch (error) { + console.error(`Failed to load data: ${error}`); + throw error; + } +} + +function ChargingStructurePage (): React.ReactElement { + const [chargingStructureDataSet,setDataset]= useState<ChargingStructureDataset>(EMPTY_DATASET); + const [chargingStructureData, setChargingStructureData] = useState<ChargingStructure[]>([]); + console.log("ChargingStructurePage"); + + + React.useEffect(() => { + const loadData = async () => { + const _chargingStructureData = await getData(); + setChargingStructureData(_chargingStructureData); + } + loadData(); + }, []); + + useEffect(() => { + if(chargingStructureDataSet !== undefined) { + setDataset(createChargingStructureDataset(chargingStructureData)); + console.log(chargingStructureDataSet); + } + }, [chargingStructureData]); + + return ( + <Container> + <Row> + <Col> + <h1>Charging Structure</h1> + </Col> + </Row> + <Row> + <Col> + <div className="chart-container" style={{ 'minHeight': '300vh', 'width': '80vw', }}> + <Bubble data={chargingStructureDataSet} options={options} /> + </div> + </Col> + </Row> + </Container> + ); +}; +export default ChargingStructurePage; diff --git a/webapp/src/pages/CompendiumData.tsx b/webapp/src/pages/CompendiumData.tsx index 84b42507..b966c857 100644 --- a/webapp/src/pages/CompendiumData.tsx +++ b/webapp/src/pages/CompendiumData.tsx @@ -38,6 +38,12 @@ function CompendiumData(): ReactElement { <span>Income Source of NRENs per Year</span> </Link> </Row> + + <Row> + <Link to="/charging" state={{ graph: 'charging_structure' }}> + <span>Charging Mechanism of NRENs per Year</span> + </Link> + </Row> </div> </CollapsibleBox> </div> diff --git a/webapp/tsconfig.json b/webapp/tsconfig.json index 52b6b1da..a33ff451 100644 --- a/webapp/tsconfig.json +++ b/webapp/tsconfig.json @@ -12,7 +12,9 @@ "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, - "jsx": "react" + "jsx": "react", + "declaration": true, + "declarationDir": "dist/types" }, "include": ["src"] } -- GitLab