From d071f81d27de98dac49ce9d7d378d711aa7b3f26 Mon Sep 17 00:00:00 2001 From: Noe Date: Mon, 9 Oct 2023 15:03:27 +0200 Subject: [PATCH] updated frontend --- typescript/frontend-marios2/src/App.css | 10 + .../src/Resources/formatPower.tsx | 49 ++ .../src/Resources/images/ac-current.png | Bin 0 -> 4519 bytes .../src/Resources/images/converter.png | Bin 0 -> 24586 bytes .../src/Resources/images/inverter.png | Bin 0 -> 21238 bytes .../src/Resources/images/inverter.svg | 11 + .../src/Resources/routes.json | 14 + .../Configuration/Configuration.tsx | 99 ++++ .../dashboards/Installations/fetchData.tsx | 37 ++ .../dashboards/Installations/flatView.tsx | 19 + .../src/content/dashboards/Log/fetchData.tsx | 37 ++ .../dashboards/Overview/chartOptions.tsx | 97 ++++ .../content/dashboards/Overview/overview.tsx | 509 ++++++++++++++++++ .../dashboards/Topology/dotsAnimation.css | 73 +++ .../dashboards/Topology/topologyBox.tsx | 237 ++++++++ .../dashboards/Topology/topologyColumn.tsx | 45 ++ .../dashboards/Topology/topologyFlow.tsx | 190 +++++++ .../src/content/dashboards/Tree/treeView.tsx | 19 + .../frontend-marios2/src/interfaces/Chart.tsx | 26 + 19 files changed, 1472 insertions(+) create mode 100644 typescript/frontend-marios2/src/App.css create mode 100644 typescript/frontend-marios2/src/Resources/formatPower.tsx create mode 100644 typescript/frontend-marios2/src/Resources/images/ac-current.png create mode 100644 typescript/frontend-marios2/src/Resources/images/converter.png create mode 100644 typescript/frontend-marios2/src/Resources/images/inverter.png create mode 100644 typescript/frontend-marios2/src/Resources/images/inverter.svg create mode 100644 typescript/frontend-marios2/src/Resources/routes.json create mode 100644 typescript/frontend-marios2/src/content/dashboards/Configuration/Configuration.tsx create mode 100644 typescript/frontend-marios2/src/content/dashboards/Installations/fetchData.tsx create mode 100644 typescript/frontend-marios2/src/content/dashboards/Installations/flatView.tsx create mode 100644 typescript/frontend-marios2/src/content/dashboards/Log/fetchData.tsx create mode 100644 typescript/frontend-marios2/src/content/dashboards/Overview/chartOptions.tsx create mode 100644 typescript/frontend-marios2/src/content/dashboards/Overview/overview.tsx create mode 100644 typescript/frontend-marios2/src/content/dashboards/Topology/dotsAnimation.css create mode 100644 typescript/frontend-marios2/src/content/dashboards/Topology/topologyBox.tsx create mode 100644 typescript/frontend-marios2/src/content/dashboards/Topology/topologyColumn.tsx create mode 100644 typescript/frontend-marios2/src/content/dashboards/Topology/topologyFlow.tsx create mode 100644 typescript/frontend-marios2/src/content/dashboards/Tree/treeView.tsx create mode 100644 typescript/frontend-marios2/src/interfaces/Chart.tsx diff --git a/typescript/frontend-marios2/src/App.css b/typescript/frontend-marios2/src/App.css new file mode 100644 index 000000000..e0070014d --- /dev/null +++ b/typescript/frontend-marios2/src/App.css @@ -0,0 +1,10 @@ +/* Disable text selection for the entire page */ +body { + user-select: none; +} + +/* Style the selected text */ +::selection { + background-color: transparent; /* Set the background color to transparent */ + color: #000; /* Set the text color for selected text */ +} \ No newline at end of file diff --git a/typescript/frontend-marios2/src/Resources/formatPower.tsx b/typescript/frontend-marios2/src/Resources/formatPower.tsx new file mode 100644 index 000000000..6ac38ca6c --- /dev/null +++ b/typescript/frontend-marios2/src/Resources/formatPower.tsx @@ -0,0 +1,49 @@ +export function formatPowerForGraph(value, max): { value: number } { + if (isNaN(value)) { + return null; + } + let negative = false; + if (max > 1000) { + value = parseFloat(value); + + if (value < 0) { + value = -value; + negative = true; + } + + value = value / 1000; + + while (value >= 1000) { + value /= 1000; + } + } + + return { + value: negative === false ? value : -value + }; +} + +export function findPower(value) { + value = parseFloat(value); + + if (value === 0) { + return { value: 0 }; + } + + // Determine the sign of the input value + const sign = Math.sign(value); + + // Take the absolute value for calculations + value = Math.abs(value); + + // Calculate the power of 10 that's greater or equal to the absolute value + let exponent = Math.floor(Math.log10(value)); + + // Compute the nearest power of 10 + const nearestPowerOf10 = Math.pow(10, exponent); + + // Restore the sign to the result + const result = nearestPowerOf10 * sign; + + return { value: result }; +} diff --git a/typescript/frontend-marios2/src/Resources/images/ac-current.png b/typescript/frontend-marios2/src/Resources/images/ac-current.png new file mode 100644 index 0000000000000000000000000000000000000000..1220dcc59d83d50a1810d459a3224c51c8a74f47 GIT binary patch literal 4519 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 literal 0 HcmV?d00001 diff --git a/typescript/frontend-marios2/src/Resources/images/converter.png b/typescript/frontend-marios2/src/Resources/images/converter.png new file mode 100644 index 0000000000000000000000000000000000000000..f030ef85df23fb016a9004dab2dd7e81711c3a76 GIT binary patch literal 24586 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%0Dfu1>m4 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

R@&XTSH4#oW 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 literal 0 HcmV?d00001 diff --git a/typescript/frontend-marios2/src/Resources/images/inverter.svg b/typescript/frontend-marios2/src/Resources/images/inverter.svg new file mode 100644 index 000000000..90fe34d8b --- /dev/null +++ b/typescript/frontend-marios2/src/Resources/images/inverter.svg @@ -0,0 +1,11 @@ + + + + + + + diff --git a/typescript/frontend-marios2/src/Resources/routes.json b/typescript/frontend-marios2/src/Resources/routes.json new file mode 100644 index 000000000..d33a51dfc --- /dev/null +++ b/typescript/frontend-marios2/src/Resources/routes.json @@ -0,0 +1,14 @@ +{ + "installation": "installation/", + "liveView": "liveView/", + "users": "/users/", + "log": "log/", + "installations": "/installations/", + "groups": "/groups/", + "group": "group/", + "folder": "folder/", + "manageAccess": "manageAccess/", + "user": "user/", + "tree": "tree", + "list": "list" +} diff --git a/typescript/frontend-marios2/src/content/dashboards/Configuration/Configuration.tsx b/typescript/frontend-marios2/src/content/dashboards/Configuration/Configuration.tsx new file mode 100644 index 000000000..1d8f5e03e --- /dev/null +++ b/typescript/frontend-marios2/src/content/dashboards/Configuration/Configuration.tsx @@ -0,0 +1,99 @@ +import { TopologyValues } from '../Log/graph.util'; +import { Box, CardContent, Container, Grid, TextField } from '@mui/material'; +import React from 'react'; + +interface ConfigurationProps { + values: TopologyValues; +} + +function Configuration(props: ConfigurationProps) { + if (props.values === null) { + return null; + } + + return ( + + + + + +

+ +
+ +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ + + + + + ); +} + +export default Configuration; diff --git a/typescript/frontend-marios2/src/content/dashboards/Installations/fetchData.tsx b/typescript/frontend-marios2/src/content/dashboards/Installations/fetchData.tsx new file mode 100644 index 000000000..c8abf209b --- /dev/null +++ b/typescript/frontend-marios2/src/content/dashboards/Installations/fetchData.tsx @@ -0,0 +1,37 @@ +import { UnixTime } from 'src/dataCache/time'; +import { I_S3Credentials } from 'src/interfaces/S3Types'; +import { FetchResult } from 'src/dataCache/dataCache'; +import { DataRecord } from 'src/dataCache/data'; +import { S3Access } from 'src/dataCache/S3/S3Access'; +import { parseCsv } from '../Log/graph.util'; + +export const fetchData = ( + timestamp: UnixTime, + s3Credentials?: I_S3Credentials +): Promise> => { + const s3Path = `${timestamp.ticks}.csv`; + if (s3Credentials && s3Credentials.s3Bucket) { + const s3Access = new S3Access( + s3Credentials.s3Bucket, + s3Credentials.s3Region, + s3Credentials.s3Provider, + s3Credentials.s3Key, + s3Credentials.s3Secret + ); + return s3Access + .get(s3Path) + .then(async (r) => { + if (r.status === 404) { + return Promise.resolve(FetchResult.notAvailable); + } else if (r.status === 200) { + const text = await r.text(); + return parseCsv(text); + } else { + return Promise.resolve(FetchResult.notAvailable); + } + }) + .catch((e) => { + return Promise.resolve(FetchResult.tryLater); + }); + } +}; diff --git a/typescript/frontend-marios2/src/content/dashboards/Installations/flatView.tsx b/typescript/frontend-marios2/src/content/dashboards/Installations/flatView.tsx new file mode 100644 index 000000000..03998cea6 --- /dev/null +++ b/typescript/frontend-marios2/src/content/dashboards/Installations/flatView.tsx @@ -0,0 +1,19 @@ +import { Box, Grid, useTheme } from '@mui/material'; +import InstallationsContextProvider from 'src/contexts/InstallationsContextProvider'; +import InstallationSearch from './InstallationSearch'; + +function FlatView() { + const theme = useTheme(); + + return ( + + + + + + + + ); +} + +export default FlatView; diff --git a/typescript/frontend-marios2/src/content/dashboards/Log/fetchData.tsx b/typescript/frontend-marios2/src/content/dashboards/Log/fetchData.tsx new file mode 100644 index 000000000..0bc6f455d --- /dev/null +++ b/typescript/frontend-marios2/src/content/dashboards/Log/fetchData.tsx @@ -0,0 +1,37 @@ +import { UnixTime } from '../../../dataCache/time'; +import { I_S3Credentials } from '../../../interfaces/S3Types'; +import { FetchResult } from '../../../dataCache/dataCache'; +import { DataRecord } from '../../../dataCache/data'; +import { S3Access } from '../../../dataCache/S3/S3Access'; +import { parseCsv } from './graph.util'; + +export const fetchData = ( + timestamp: UnixTime, + s3Credentials?: I_S3Credentials +): Promise> => { + const s3Path = `${timestamp.ticks}.csv`; + if (s3Credentials && s3Credentials.s3Bucket) { + const s3Access = new S3Access( + s3Credentials.s3Bucket, + s3Credentials.s3Region, + s3Credentials.s3Provider, + s3Credentials.s3Key, + s3Credentials.s3Secret + ); + return s3Access + .get(s3Path) + .then(async (r) => { + if (r.status === 404) { + return Promise.resolve(FetchResult.notAvailable); + } else if (r.status === 200) { + const text = await r.text(); + return parseCsv(text); + } else { + return Promise.resolve(FetchResult.notAvailable); + } + }) + .catch((e) => { + return Promise.resolve(FetchResult.tryLater); + }); + } +}; diff --git a/typescript/frontend-marios2/src/content/dashboards/Overview/chartOptions.tsx b/typescript/frontend-marios2/src/content/dashboards/Overview/chartOptions.tsx new file mode 100644 index 000000000..71e1540c3 --- /dev/null +++ b/typescript/frontend-marios2/src/content/dashboards/Overview/chartOptions.tsx @@ -0,0 +1,97 @@ +// chartOptions.ts + +import { ApexOptions } from 'apexcharts'; +import { chartInfoInterface } from 'src/interfaces/Chart'; +import { findPower, formatPowerForGraph } from '../../../Resources/formatPower'; + +export const getChartOptions = (chartInfo: chartInfoInterface): ApexOptions => { + const chartOptions: ApexOptions = { + chart: { + id: 'area-datetime', + type: 'area', + height: 350, + zoom: { + autoScaleYaxis: false + } + }, + dataLabels: { + enabled: false + }, + fill: { + type: 'gradient', + gradient: { + shadeIntensity: 1, + opacityFrom: 0.7, + opacityTo: 0.9, + stops: [0, 100] + } + }, + //colors: ['#FF5733', '#3498db'], + colors: ['#3498db', '#2ecc71'], + //colors: ['#1abc9c', '#e91e63'], + xaxis: { + type: 'datetime', + labels: { + datetimeFormatter: { + year: 'yyyy', + month: "MMM 'yy", + day: 'dd MMM', + hour: 'HH:mm' + } + } + }, + stroke: { + curve: 'smooth', + width: 2 + }, + yaxis: { + min: chartInfo.min > 0 ? 0 : undefined, + max: + chartInfo.min > 0 + ? Math.round(chartInfo.max / findPower(chartInfo.max).value) * + findPower(chartInfo.max).value + : undefined, + title: { + text: chartInfo.unit, + style: { + fontSize: '12px' + }, + offsetY: -160, + offsetX: 25, + rotate: 0 + }, + labels: { + formatter: + chartInfo.min > 0 + ? function (value: number) { + return formatPowerForGraph( + value, + chartInfo.max + ).value.toString(); + } + : function (value: number) { + return formatPowerForGraph( + value, + chartInfo.max + ).value.toString(); + } + } + }, + tooltip: { + x: { + format: 'dd MMM HH:mm' + }, + y: { + formatter: function (val, opts) { + return ( + formatPowerForGraph(val, chartInfo.max).value.toFixed(2) + + ' ' + + chartInfo.unit + ); + } + } + } + }; + + return chartOptions; +}; diff --git a/typescript/frontend-marios2/src/content/dashboards/Overview/overview.tsx b/typescript/frontend-marios2/src/content/dashboards/Overview/overview.tsx new file mode 100644 index 000000000..d7821dad9 --- /dev/null +++ b/typescript/frontend-marios2/src/content/dashboards/Overview/overview.tsx @@ -0,0 +1,509 @@ +import { + Box, + Card, + Container, + Grid, + Typography, + useTheme +} from '@mui/material'; +import ReactApexChart from 'react-apexcharts'; +import { useEffect, useMemo, useState } from 'react'; +import DataCache from 'src/dataCache/dataCache'; +import { TimeSpan, UnixTime } from 'src/dataCache/time'; +import { BehaviorSubject, startWith, throttleTime, withLatestFrom } from 'rxjs'; +import { RecordSeries } from 'src/dataCache/data'; +import { createTimes } from '../Log/graph.util'; +import { I_S3Credentials } from 'src/interfaces/S3Types'; +import { getChartOptions } from './chartOptions'; +import { chartDataInterface, overviewInterface } from 'src/interfaces/Chart'; +import { fetchData } from 'src/content/dashboards/Installations/fetchData'; + +const prefixes = ['', 'k', 'M', 'G', 'T']; +const MAX_NUMBER = 9999999; + +interface OverviewProps { + s3Credentials: I_S3Credentials; +} + +function Overview(props: OverviewProps) { + const theme = useTheme(); + const timeRange = createTimes( + UnixTime.now().rangeBefore(TimeSpan.fromDays(1)), + 200 + ); + + const [chartData, setChartData] = useState({ + soc: [], + temperature: [], + dcPower: [], + gridPower: [], + pvProduction: [], + dcBusVoltage: [] + }); + + const [chartOverview, setChartOverview] = useState({ + soc: { magnitude: 0, unit: '', min: 0, max: 0 }, + temperature: { magnitude: 0, unit: '', min: 0, max: 0 }, + dcPower: { magnitude: 0, unit: '', min: 0, max: 0 }, + gridPower: { magnitude: 0, unit: '', min: 0, max: 0 }, + pvProduction: { magnitude: 0, unit: '', min: 0, max: 0 }, + dcBusVoltage: { magnitude: 0, unit: '', min: 0, max: 0 } + }); + + const pathsToSearch = [ + '/Battery/Soc', + '/Battery/Temperature', + '/Battery/Dc/Power', + '/GridMeter/Ac/Power/Active', + '/PvOnDc/Dc/Power', + '/DcDc/Dc/Link/Voltage' + ]; + + const cache = useMemo(() => { + return new DataCache( + fetchData, + TimeSpan.fromSeconds(2), + props.s3Credentials + ); + }, []); + + const times$ = useMemo(() => new BehaviorSubject(timeRange), []); + + const transformToGraphData = ( + input: RecordSeries + ): { + chartData: chartDataInterface; + chartOverview: overviewInterface; + } => { + const data = {}; + const overviewData = {}; + + const chartData: chartDataInterface = { + soc: [], + temperature: [], + dcPower: [], + gridPower: [], + pvProduction: [], + dcBusVoltage: [] + }; + + const chartOverview: overviewInterface = { + soc: { magnitude: 0, unit: '', min: 0, max: 0 }, + temperature: { magnitude: 0, unit: '', min: 0, max: 0 }, + dcPower: { magnitude: 0, unit: '', min: 0, max: 0 }, + gridPower: { magnitude: 0, unit: '', min: 0, max: 0 }, + pvProduction: { magnitude: 0, unit: '', min: 0, max: 0 }, + dcBusVoltage: { magnitude: 0, unit: '', min: 0, max: 0 } + }; + + pathsToSearch.forEach((path) => { + data[path] = []; + overviewData[path] = { + magnitude: 0, + unit: '', + min: MAX_NUMBER, + max: 0 + }; + }); + + input.forEach((item) => { + const csvContent = item.value; + + pathsToSearch.forEach((path) => { + if (csvContent && csvContent[path]) { + const timestamp = item.time.ticks * 1000; + const value = csvContent[path]; + // const result: { magnitude: number; value: number } = formatPower( + // value.value + // ); + if (value.value < overviewData[path].min) { + overviewData[path].min = value.value; + } + + if (value.value > overviewData[path].max) { + overviewData[path].max = value.value; + } + + // if (result.magnitude > result.magnitude) { + // overviewData[path].magnitude = result.magnitude; + // } + data[path].push([timestamp, value.value]); + } + }); + }); + + pathsToSearch.forEach((path) => { + let value = overviewData[path].max; + let negative = false; + let magnitude = 0; + + if (value < 0) { + value = -value; + negative = true; + } + while (value >= 1000) { + value /= 1000; + magnitude++; + } + console.log(path, magnitude); + + overviewData[path].magnitude = prefixes[magnitude]; + }); + + let path = '/Battery/Soc'; + chartData.soc = [{ name: 'State of Charge', data: data[path] }]; + + chartOverview.soc = { + unit: '(%)', + magnitude: overviewData[path].magnitude, + min: 0, + max: 100 + }; + + path = '/Battery/Temperature'; + chartData.temperature = [ + { name: 'Battery Temperature:', data: data[path] } + ]; + + chartOverview.temperature = { + unit: '(°C)', + magnitude: overviewData[path].magnitude, + min: overviewData[path].min, + max: overviewData[path].max + }; + + path = '/Battery/Dc/Power'; + chartData.dcPower = [{ name: 'Battery Power', data: data[path] }]; + + //console.log(overviewData[path]); + //console.log(data[path]); + chartOverview.dcPower = { + magnitude: overviewData[path].magnitude, + unit: '(' + overviewData[path].magnitude + 'W' + ')', + min: overviewData[path].min, + max: overviewData[path].max + }; + + path = '/GridMeter/Ac/Power/Active'; + chartData.gridPower = [{ name: 'Grid Power', data: data[path] }]; + + chartOverview.gridPower = { + magnitude: overviewData[path].magnitude, + unit: '(' + overviewData[path].magnitude + 'W' + ')', + min: overviewData[path].min, + max: overviewData[path].max + }; + + path = '/PvOnDc/Dc/Power'; + chartData.pvProduction = [{ name: 'Pv Production', data: data[path] }]; + + chartOverview.pvProduction = { + magnitude: overviewData[path].magnitude, + unit: '(' + overviewData[path].magnitude + 'W' + ')', + min: overviewData[path].min, + max: overviewData[path].max + }; + + path = '/DcDc/Dc/Link/Voltage'; + chartData.dcBusVoltage = [{ name: 'DC Bus Voltage', data: data[path] }]; + + chartOverview.dcBusVoltage = { + magnitude: overviewData[path].magnitude, + unit: '(' + overviewData[path].magnitude + 'V' + ')', + min: overviewData[path].min, + max: overviewData[path].max + }; + + return { + chartData: chartData, + chartOverview: chartOverview + }; + + //return chartData; + }; + + useEffect(() => { + const subscription = cache.gotData + .pipe( + startWith(0), + throttleTime(200, undefined, { leading: true, trailing: true }), + withLatestFrom(times$) + ) + .subscribe(([_, times]) => { + const timeSeries = cache.getSeries(times); + + console.log(timeSeries); + + const result: { + chartData: chartDataInterface; + chartOverview: overviewInterface; + } = transformToGraphData(timeSeries); + + setChartData(result.chartData); + setChartOverview(result.chartOverview); + }); + return () => subscription.unsubscribe(); + }, []); + + const renderGraphs = () => { + return ( + + + + + + + + + + + Battery SOC (State Of Charge) + + + + + + + + + + + + + + + Battery Temperature + + + + + + + + + + + + + + + + + + Battery Power + + + + + + + + + + + + + + + Grid Power + + + + + + + + + + + + + + + + + PV Production + + + + + + + + + + + + + + + DC Bus Voltage + + + + + + + + + + + + + ); + }; + + return <>{renderGraphs()}; +} + +export default Overview; diff --git a/typescript/frontend-marios2/src/content/dashboards/Topology/dotsAnimation.css b/typescript/frontend-marios2/src/content/dashboards/Topology/dotsAnimation.css new file mode 100644 index 000000000..20a7be336 --- /dev/null +++ b/typescript/frontend-marios2/src/content/dashboards/Topology/dotsAnimation.css @@ -0,0 +1,73 @@ + +.line { + overflow: hidden; + border-top: none; + border-bottom: none; +} + +.horizontalLine { + position: absolute; + overflow: hidden; + border-left: none; + margin-left: 173px; + border-right: none; +} + +.dotRight { + position: absolute; + margin-left: 35px; + width: 3px; + height: 3px; + border-radius: 50%; + background-color: #f7b34d; + animation: rightflow 2s linear infinite; +} + +.dotLeft { + position: absolute; + margin-left: 35px; + width: 3px; + height: 3px; + border-radius: 50%; + background-color: #f7b34d; + animation: leftflow 2s linear infinite; +} + +.verticalDotDown { + position: absolute; + margin-top: 35px; + width: 3px; + height: 3px; + border-radius: 50%; + background-color: #f7b34d; + animation: verticalDownFlow 2s linear infinite; +} + + +@keyframes rightflow { + 0% { + left: -35px; + } + 100% { + left: 110%; + } +} + +@keyframes leftflow { + 0% { + left: 110px; + } + 100% { + left: -35px; + } +} + +@keyframes verticalDownFlow { + 0% { + top: -35px; + } + 100% { + top: 100%; + } +} + diff --git a/typescript/frontend-marios2/src/content/dashboards/Topology/topologyBox.tsx b/typescript/frontend-marios2/src/content/dashboards/Topology/topologyBox.tsx new file mode 100644 index 000000000..f1c11e47a --- /dev/null +++ b/typescript/frontend-marios2/src/content/dashboards/Topology/topologyBox.tsx @@ -0,0 +1,237 @@ +import React from 'react'; +import { + Box, + Card, + CardContent, + Divider, + Typography, + useTheme +} from '@mui/material'; +import { AvatarWrapper } from 'src/layouts/TabsContainerWrapper'; +import BatteryCharging60Icon from '@mui/icons-material/BatteryCharging60'; +import OutletIcon from '@mui/icons-material/Outlet'; +import SolarPowerIcon from '@mui/icons-material/SolarPower'; +import PowerInputIcon from '@mui/icons-material/PowerInput'; +import BoltIcon from '@mui/icons-material/Bolt'; +import { BoxData } from '../Log/graph.util'; +import inverterImage from 'src/Resources/images/inverter.png'; +import acCurrentImage from 'src/Resources/images/ac-current.png'; +import converterImage from 'src/Resources/images/converter.png'; + +export interface TopologyBoxProps { + title: string; + data?: BoxData; +} + +const isInt = (value: number) => { + return value % 1 === 0; +}; + +function formatPower(value) { + if (isNaN(value)) { + return 'Invalid'; + } + + const prefixes = ['', 'k', 'M', 'G', 'T']; + let magnitude = 0; + let negative = false; + + if (value < 0) { + value = -value; + negative = true; + } + while (value >= 1000) { + value /= 1000; + magnitude++; + } + + const roundedValue = value.toFixed(2); + + return negative === false + ? `${roundedValue} ${prefixes[magnitude]}` + : `-${roundedValue} ${prefixes[magnitude]}`; +} + +function TopologyBox(props: TopologyBoxProps) { + const theme = useTheme(); + + return ( + + + + {props.title === 'Battery' && ( + + {props.title} (x{props.data.values.length - 4}) + + )} + {props.title != 'Battery' && ( + + {props.title} + + )} + + {props.title === 'AC-DC' && ( + + )} + + {props.title === 'DC Link' && ( + + )} + + {props.title === 'DC-DC' && ( + + )} + + {(props.title === 'Grid Bus' || props.title === 'Island Bus') && ( + + )} + + {props.title != 'AC-DC' && + props.title != 'DC Link' && + props.title != 'DC-DC' && ( + + {(props.title === 'Pv Inverter' || + props.title === 'Pv DcDc') && ( + + )} + + {props.title === 'Battery' && ( + + )} + {(props.title === 'AC Loads' || props.title === 'DC Loads') && ( + + )} + {props.title === 'Grid' && ( + + )} + + )} + + {props.data && } + {props.data && ( + + {props.data.values.map((boxData, index) => { + return ( + + {formatPower(boxData.value)} + {boxData.unit} + + ); + })} + + )} + + + + ); +} + +export default TopologyBox; diff --git a/typescript/frontend-marios2/src/content/dashboards/Topology/topologyColumn.tsx b/typescript/frontend-marios2/src/content/dashboards/Topology/topologyColumn.tsx new file mode 100644 index 000000000..88a1e9ff0 --- /dev/null +++ b/typescript/frontend-marios2/src/content/dashboards/Topology/topologyColumn.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import TopologyBox, { TopologyBoxProps } from './topologyBox'; +import { Box } from '@mui/material'; +import './dotsAnimation.css'; +import TopologyFlow, { TopologyFlowProps } from './topologyFlow'; + +interface TopologyColumnProps { + topBox?: TopologyBoxProps; + centerBox?: TopologyBoxProps; + bottomBox?: TopologyBoxProps; + topConnection?: TopologyFlowProps; + centerConnection?: TopologyFlowProps; + bottomConnection?: TopologyFlowProps; + isLast: boolean; + isFirst: boolean; +} + +function TopologyColumn(props: TopologyColumnProps) { + return ( + + + + {props.topBox && props.centerBox && ( + + )} + + {!props.isLast && } + + {props.centerBox && props.bottomBox && ( + + )} + + + ); +} + +export default TopologyColumn; diff --git a/typescript/frontend-marios2/src/content/dashboards/Topology/topologyFlow.tsx b/typescript/frontend-marios2/src/content/dashboards/Topology/topologyFlow.tsx new file mode 100644 index 000000000..d35130288 --- /dev/null +++ b/typescript/frontend-marios2/src/content/dashboards/Topology/topologyFlow.tsx @@ -0,0 +1,190 @@ +import React, { useEffect, useState } from 'react'; +import './dotsAnimation.css'; +import { BoxData } from '../Log/graph.util'; +import { Box, Typography } from '@mui/material'; + +export interface TopologyFlowProps { + orientation: string; + position?: string; + data?: BoxData; + amount?: number; + showValues: boolean; +} + +function getRandomStyle() { + const animationDelay = `${Math.random() * 2}s`; // Random delay between 0 and 2 seconds + const top = `${Math.random() * 100}%`; // Random top position within the container + return { animationDelay, top }; +} + +function getRandomStyleVertical() { + const animationDelay = `${Math.random() * 2}s`; // Random delay between 0 and 2 seconds + const left = `${Math.random() * 100}%`; // Random top position within the container + return { animationDelay, left }; +} + +const isInt = (value: number) => { + return value % 1 === 0; +}; + +function formatPower(value) { + if (isNaN(value)) { + return 'Invalid'; + } + + const prefixes = ['', 'k', 'M', 'G', 'T']; + let magnitude = 0; + let negative = false; + + if (value < 0) { + value = -value; + negative = true; + } + while (value >= 1000) { + value /= 1000; + magnitude++; + } + + const roundedValue = value.toFixed(2); + + return negative === false + ? `${roundedValue} ${prefixes[magnitude]}` + : `-${roundedValue} ${prefixes[magnitude]}`; +} + +function TopologyFlow(props: TopologyFlowProps) { + const [dotStyles, setDotStyle] = useState< + { animationDelay: string; top: string }[] + >([]); + + const [dotStylesVertical, setDotStyleVertical] = useState< + { animationDelay: string; left: string }[] + >([]); + + const numOfDots = 400; + const minNumberOfDots = 100; + const minHeight = 2; + const maxHeight = 70; + const minWidth = 2; + const maxWidth = 68; + + const desiredNumOfDots = + numOfDots * props.amount < minNumberOfDots + ? minNumberOfDots + : numOfDots * props.amount > numOfDots + ? numOfDots + : numOfDots * props.amount; + + useEffect(() => { + setDotStyle( + Array.from({ length: desiredNumOfDots }, () => getRandomStyle()) + ); + + setDotStyleVertical( + Array.from({ length: desiredNumOfDots }, () => getRandomStyleVertical()) + ); + }, []); + + return ( + <> + {props.data && props.orientation === 'horizontal' && ( + + {props.showValues && props.data.values[0].value != 0 && ( + + {formatPower(props.data.values[0].value)} + {props.data.values[0].unit} + + )} + + )} + +
+ {props.orientation === 'horizontal' && + props.data && + props.data.values[0].value != 0 && ( + <> +
+ {dotStyles.map((style, index) => ( +
= 0 + ? 'dotRight' + : 'dotLeft' + } + key={index} + style={{ + animationDelay: style.animationDelay, + top: style.top + }} + >
+ ))} +
+ + )} + + {props.orientation === 'vertical' && + props.data && + props.data.values[0].value != 0 && ( +
+ {dotStylesVertical.map((style, index) => ( +
+ ))} +
+ )} +
+ + ); +} + +export default TopologyFlow; diff --git a/typescript/frontend-marios2/src/content/dashboards/Tree/treeView.tsx b/typescript/frontend-marios2/src/content/dashboards/Tree/treeView.tsx new file mode 100644 index 000000000..82e8c737d --- /dev/null +++ b/typescript/frontend-marios2/src/content/dashboards/Tree/treeView.tsx @@ -0,0 +1,19 @@ +import { Box, Grid, useTheme } from '@mui/material'; +import InstallationTree from './InstallationTree'; +import InstallationsContextProvider from 'src/contexts/InstallationsContextProvider'; + +function TreeView() { + const theme = useTheme(); + + return ( + + + + + + + + ); +} + +export default TreeView; diff --git a/typescript/frontend-marios2/src/interfaces/Chart.tsx b/typescript/frontend-marios2/src/interfaces/Chart.tsx new file mode 100644 index 000000000..fd97b0c70 --- /dev/null +++ b/typescript/frontend-marios2/src/interfaces/Chart.tsx @@ -0,0 +1,26 @@ +import { ApexOptions } from 'apexcharts'; + +export interface overviewInterface { + soc: chartInfoInterface; + temperature: chartInfoInterface; + dcPower: chartInfoInterface; + gridPower: chartInfoInterface; + pvProduction: chartInfoInterface; + dcBusVoltage: chartInfoInterface; +} + +export interface chartInfoInterface { + magnitude: number; + unit: string; + min: number; + max: number; +} + +export interface chartDataInterface { + soc: ApexOptions['series']; + temperature: ApexOptions['series']; + dcPower: ApexOptions['series']; + gridPower: ApexOptions['series']; + pvProduction: ApexOptions['series']; + dcBusVoltage: ApexOptions['series']; +}