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 000000000..1220dcc59 Binary files /dev/null and b/typescript/frontend-marios2/src/Resources/images/ac-current.png differ 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 000000000..f030ef85d Binary files /dev/null and b/typescript/frontend-marios2/src/Resources/images/converter.png differ diff --git a/typescript/frontend-marios2/src/Resources/images/inverter.png b/typescript/frontend-marios2/src/Resources/images/inverter.png new file mode 100644 index 000000000..b6947af3b Binary files /dev/null and b/typescript/frontend-marios2/src/Resources/images/inverter.png differ 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']; +}