From d722f42d992db502e2f470f3e0a4ba6310da58bc Mon Sep 17 00:00:00 2001 From: Sina Blattmann Date: Tue, 9 May 2023 13:50:47 +0200 Subject: [PATCH] working graphs and toggles --- typescript/Frontend/src/Login.tsx | 1 - typescript/Frontend/src/ResetPassword.tsx | 1 - .../components/Context/LogContextProvider.tsx | 39 +++ .../Context/UsersContextProvider.tsx | 1 - .../Installations/Installations.tsx | 41 ++- .../Installations/Log/CheckboxTree.tsx | 110 ++++++ .../Installations/Log/ScalarGraph.tsx | 316 +++++++++--------- .../Frontend/src/components/Layout/Search.tsx | 2 +- typescript/Frontend/src/util/graph.util.tsx | 24 -- 9 files changed, 332 insertions(+), 203 deletions(-) create mode 100644 typescript/Frontend/src/components/Context/LogContextProvider.tsx create mode 100644 typescript/Frontend/src/components/Installations/Log/CheckboxTree.tsx diff --git a/typescript/Frontend/src/Login.tsx b/typescript/Frontend/src/Login.tsx index 11069fd2d..7d0b5104f 100644 --- a/typescript/Frontend/src/Login.tsx +++ b/typescript/Frontend/src/Login.tsx @@ -40,7 +40,6 @@ const Login = ({ setToken, setLanguage }: I_LoginProps) => { .then(() => { setToken(data.token); saveCurrentUser(data.user); - console.log(data.user); setLoading(false); setLanguage(data.user.language); }) diff --git a/typescript/Frontend/src/ResetPassword.tsx b/typescript/Frontend/src/ResetPassword.tsx index 3b07db745..ea893289a 100644 --- a/typescript/Frontend/src/ResetPassword.tsx +++ b/typescript/Frontend/src/ResetPassword.tsx @@ -34,7 +34,6 @@ const ResetPassword = () => { }) .then((res) => { saveCurrentUser(res.data); - console.log(res.data); setLoading(false); }) .catch((err) => setError(err)); diff --git a/typescript/Frontend/src/components/Context/LogContextProvider.tsx b/typescript/Frontend/src/components/Context/LogContextProvider.tsx new file mode 100644 index 000000000..8bbe6d2df --- /dev/null +++ b/typescript/Frontend/src/components/Context/LogContextProvider.tsx @@ -0,0 +1,39 @@ +import { createContext, ReactNode, SetStateAction, useState } from "react"; +import { TreeElement, ToggleElement } from "../Installations/Log/CheckboxTree"; +import React from "react"; + +interface LogContextProviderProps { + toggles: TreeElement[] | null; + setToggles: (value: TreeElement[]) => void; + checkedToggles: ToggleElement | null; + setCheckedToggles: React.Dispatch>; +} + +export const LogContext = createContext({ + toggles: [], + setToggles: () => {}, + checkedToggles: {}, + setCheckedToggles: () => {}, +}); + +const LogContextProvider = ({ children }: { children: ReactNode }) => { + const [toggles, setToggles] = useState(null); + const [checkedToggles, setCheckedToggles] = useState( + null + ); + console.log("provider", toggles); + return ( + + {children} + + ); +}; + +export default LogContextProvider; diff --git a/typescript/Frontend/src/components/Context/UsersContextProvider.tsx b/typescript/Frontend/src/components/Context/UsersContextProvider.tsx index af77b5e9c..9952b2b7d 100644 --- a/typescript/Frontend/src/components/Context/UsersContextProvider.tsx +++ b/typescript/Frontend/src/components/Context/UsersContextProvider.tsx @@ -90,7 +90,6 @@ const UsersContextProvider = ({ children }: { children: ReactNode }) => { const fetchAvailableUsers = async (): Promise => { return axiosConfig.get("/GetAllChildUsers").then((res) => { - console.log(res); setAvailableUsers(res.data); }); }; diff --git a/typescript/Frontend/src/components/Installations/Installations.tsx b/typescript/Frontend/src/components/Installations/Installations.tsx index b79f34d9b..16602a401 100644 --- a/typescript/Frontend/src/components/Installations/Installations.tsx +++ b/typescript/Frontend/src/components/Installations/Installations.tsx @@ -8,30 +8,35 @@ import InstallationContextProvider from "../Context/InstallationContextProvider" import SearchSidebar from "../Layout/Search"; import InstallationList from "./InstallationList"; import Installation from "./Installation"; +import CheckboxTree from "./Log/CheckboxTree"; +import LogContextProvider from "../Context/LogContextProvider"; const Installations = () => { return ( - - - - - - - - } - index + + + + - } /> - } /> - + + + + + + } + index + /> + } /> + } /> + + - + ); }; diff --git a/typescript/Frontend/src/components/Installations/Log/CheckboxTree.tsx b/typescript/Frontend/src/components/Installations/Log/CheckboxTree.tsx new file mode 100644 index 000000000..f7d266c39 --- /dev/null +++ b/typescript/Frontend/src/components/Installations/Log/CheckboxTree.tsx @@ -0,0 +1,110 @@ +import { TreeItem, TreeView } from "@mui/lab"; +import { Checkbox, Divider } from "@mui/material"; +import { useContext, ReactNode } from "react"; +import { LogContext } from "../../Context/LogContextProvider"; +import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; +import ChevronRightIcon from "@mui/icons-material/ChevronRight"; +import useRouteMatch from "../../../hooks/useRouteMatch"; +import routes from "../../../routes.json"; + +export interface ToggleElement { + [key: string]: boolean; +} + +export interface TreeElement { + id: string; + name: string; + children: TreeElement[]; + checked?: boolean; +} + +const CheckboxTree = () => { + const { toggles, setCheckedToggles, checkedToggles } = useContext(LogContext); + const routeMatch = useRouteMatch([ + routes.installations + routes.list + routes.log + ":id", + ]); + + const getNodes = (element: TreeElement): null | ReactNode => { + return element.children.length > 0 ? renderTree(element.children) : null; + }; + + const handleCheckChildren = (children: TreeElement[], checked?: boolean) => { + if (children.length > 0) { + children.forEach((child) => { + setCheckedToggles((prevState) => ({ + ...prevState, + [child.id]: !checked, + })); + if (child.children.length > 0) { + handleCheckChildren(child.children, checked); + } + }); + } + }; + + const handleClick = ( + event: React.MouseEvent, + element: TreeElement, + checked?: boolean + ) => { + event.stopPropagation(); + handleCheckChildren([element], checked); + }; + + const handleExpandClick = ( + event: React.MouseEvent + ) => { + event.stopPropagation(); + }; + + const renderTree = (data: TreeElement[]): ReactNode => { + return data.map((element) => { + const checked = checkedToggles?.[element.id]; + const splitName = element.name.split("/"); + return ( + + handleClick(e, element, checked)} + /> + {splitName[splitName.length - 1]} + + } + sx={{ + ".MuiTreeItem-content": { paddingY: "5px" }, + }} + > + {getNodes(element)} + + ); + }); + }; + return ( + <> + + {toggles !== null && routeMatch !== null && ( + } + defaultExpandIcon={} + sx={{ + height: 480, + flexGrow: 1, + overflow: "auto", + overflowX: "hidden", + }} + > + {renderTree(toggles)} + + )} + + ); +}; + +export default CheckboxTree; diff --git a/typescript/Frontend/src/components/Installations/Log/ScalarGraph.tsx b/typescript/Frontend/src/components/Installations/Log/ScalarGraph.tsx index 95c5c57cf..05248d03a 100644 --- a/typescript/Frontend/src/components/Installations/Log/ScalarGraph.tsx +++ b/typescript/Frontend/src/components/Installations/Log/ScalarGraph.tsx @@ -1,15 +1,15 @@ import Plot from "react-plotly.js"; import { RecordSeries } from "../../../dataCache/data"; -import { parseCsv, transformToGraphData } from "../../../util/graph.util"; +import { GraphData, mergeDeep, parseCsv } from "../../../util/graph.util"; import { TimeRange, TimeSpan, UnixTime } from "../../../dataCache/time"; -import { ReactNode, useEffect, useMemo, useState } from "react"; +import { useContext, useEffect, useMemo, useState } from "react"; import { BehaviorSubject, startWith, throttleTime, withLatestFrom } from "rxjs"; import { S3Access } from "../../../dataCache/S3/S3Access"; import DataCache, { FetchResult } from "../../../dataCache/dataCache"; -import { TreeItem, TreeView } from "@mui/lab"; -import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; -import ChevronRightIcon from "@mui/icons-material/ChevronRight"; -import { Checkbox } from "@mui/material"; + +import { LogContext } from "../../Context/LogContextProvider"; +import { TreeElement, ToggleElement } from "./CheckboxTree"; +import { isDefined } from "../../../dataCache/utils/maybe"; export const createTimes = ( range: TimeRange, @@ -27,16 +27,19 @@ const NUMBER_OF_NODES = 100; const ScalarGraph = () => { const timeRange = createTimes( - UnixTime.fromTicks(1682085650).rangeBefore(TimeSpan.fromDays(1)), + UnixTime.fromTicks(1682085650).rangeBefore(TimeSpan.fromDays(4)), NUMBER_OF_NODES ); const [timeSeries, setTimeSeries] = useState([]); - const [uiRevision, setUiRevision] = useState(Math.random()); const [range, setRange] = useState([ timeRange[0].toDate(), timeRange[timeRange.length - 1].toDate(), ]); - const [toggles, setToggles] = useState(null); + const [uiRevision, setUiRevision] = useState(Math.random()); + const [plotTitles, setPlotTitles] = useState([]); + + const { toggles, setToggles, setCheckedToggles, checkedToggles } = + useContext(LogContext); const times$ = useMemo(() => new BehaviorSubject(timeRange), []); @@ -49,23 +52,37 @@ const ScalarGraph = () => { "" ); - function insert( + const insert = ( children: TreeElement[] = [], [head, ...tail]: string[] - ): TreeElement[] { + ): TreeElement[] => { let child = children.find((child) => child.name === head); - if (!child) + + if (!child) { children.push( (child = { - id: head + tail.join("_"), + id: head, name: head, - checked: false, children: [], }) ); - if (tail.length > 0) insert(child.children, tail); + } + if (tail.length > 0) { + insert(child.children, tail); + } return children; - } + }; + + const flattenToggles = (toggles: TreeElement[]): ToggleElement => { + return toggles.reduce((acc, current) => { + if (current.children.length > 0) { + acc[current.id] = false; + return { ...acc, ...flattenToggles(current.children) }; + } + acc[current.id] = false; + return acc; + }, {} as ToggleElement); + }; useEffect(() => { const subscription = cache.gotData @@ -78,79 +95,34 @@ const ScalarGraph = () => { const timeSeries = cache.getSeries(times); setTimeSeries(timeSeries); const toggleValues = timeSeries.find((timeStamp) => timeStamp.value); - if (toggles === null && toggleValues && toggleValues.value) { - setToggles( - Object.keys(toggleValues.value) - .map((path) => path.split("/").slice(1)) - .reduce( - (children, path) => insert(children, path), - [] as TreeElement[] - ) - ); + console.log("toggles inside", toggles); + const treeElements = Object.keys(toggleValues.value) + .map((path) => { + return path + .split("/") + .map( + (split, i) => + "/" + + path + .split("/") + .slice(1, i + 1) + .join("/") + ) + .slice(1); + }) + .reduce( + (children, path) => insert(children, path), + [] as TreeElement[] + ); + console.log("elements", treeElements); + setToggles(treeElements); + setCheckedToggles(flattenToggles(treeElements)); } }); return () => subscription.unsubscribe(); - }, []); - - interface TreeElement { - id: string; - name: string; - children: TreeElement[]; - checked: boolean; - } - - const getNodes = (element: TreeElement): null | ReactNode => { - return element.children.length > 0 ? renderTree(element.children) : null; - }; - - const renderTree = (data: TreeElement[]): ReactNode => { - return data.map((element) => { - return ( - - { - if (toggles) { - const togglesValue = toggles; - const index = toggles?.findIndex((toggle) => { - return element.id === toggle.id; - }); - if (index > 0) { - togglesValue[index] = { - ...element, - checked: !element.checked, - }; - setToggles(togglesValue); - } - } - }} - /> - {element.name} - - } - sx={{ - ".MuiTreeItem-content": { paddingY: "12px" }, - }} - > - {getNodes(element)} - - ); - }); - }; - - const round = (date: Date) => { - const minutes = 30; - const ms = 1000 * 60 * minutes; - - return new Date(Math.round(date.getTime() / ms) * ms); - }; + }, [toggles]); const fetchData = ( timestamp: UnixTime @@ -179,81 +151,111 @@ const ScalarGraph = () => { [] ); - const renderGraphs = () => { - const coordinateTimeSeries = transformToGraphData(timeSeries); - return Object.keys(coordinateTimeSeries).map((path) => { - const data = coordinateTimeSeries[path] ?? { x: [], y: [] }; - return ( - { - const xaxisRange0 = params["xaxis.range[0]"]; - const xaxisRange1 = params["xaxis.range[1]"]; - - if (xaxisRange0 && xaxisRange1) { - setRange([new Date(xaxisRange0), new Date(xaxisRange1)]); - setUiRevision(Math.random()); - const times = createTimes( - TimeRange.fromTimes( - UnixTime.fromDate(new Date(xaxisRange0)), - UnixTime.fromDate(new Date(xaxisRange1)) - ), - NUMBER_OF_NODES - ); - cache.getSeries(times); - times$.next(times); + const transformToGraphData = (timeStampData: RecordSeries) => { + const graphData = timeStampData.reduce((acc, curr) => { + if (isDefined(curr.value)) { + const timeStampObj = Object.keys(curr.value).reduce( + (pathAcc, currPath) => { + if (currPath) { + return { + ...pathAcc, + [currPath]: { + x: [new Date(curr.time.ticks * 1000)], + y: [curr.value ? curr.value[currPath] : 0], + }, + }; } - }} - /> - ); - }); + return pathAcc; + }, + {} as GraphData + ); + if (plotTitles.length === 0) { + setPlotTitles(Object.keys(curr.value)); + } + return mergeDeep(acc, timeStampObj); + } + return acc; + }, {} as GraphData); + + if (Object.keys(graphData).length > 0) { + return graphData; + } + return plotTitles.reduce( + (acc, curr) => ({ + ...acc, + [curr]: { + x: [], + y: [], + }, + }), + {} as GraphData + ); }; - return ( - <> - {renderGraphs()} - {toggles !== null && ( - } - defaultExpandIcon={} - sx={{ - height: 480, - flexGrow: 1, - overflow: "auto", - overflowX: "hidden", - }} - > - {renderTree(toggles)} - - )} - - ); + const renderGraphs = () => { + if (checkedToggles) { + const coordinateTimeSeries = transformToGraphData(timeSeries); + console.log("coordinateTimeSeries", coordinateTimeSeries, checkedToggles); + return Object.keys(coordinateTimeSeries) + .filter((path) => { + return checkedToggles[path]; + }) + .map((path) => { + const data = coordinateTimeSeries[path] ?? { x: [], y: [] }; + return ( + { + const xaxisRange0 = params["xaxis.range[0]"]; + const xaxisRange1 = params["xaxis.range[1]"]; + + if (xaxisRange0 && xaxisRange1) { + setRange([new Date(xaxisRange0), new Date(xaxisRange1)]); + setUiRevision(Math.random()); + const times = createTimes( + TimeRange.fromTimes( + UnixTime.fromDate(new Date(xaxisRange0)), + UnixTime.fromDate(new Date(xaxisRange1)) + ), + NUMBER_OF_NODES + ); + console.log("times", times); + cache.getSeries(times); + times$.next(times); + } + }} + /> + ); + }); + } + }; + return <>{renderGraphs()}; }; export default ScalarGraph; diff --git a/typescript/Frontend/src/components/Layout/Search.tsx b/typescript/Frontend/src/components/Layout/Search.tsx index 52bc74d1f..c787d54e4 100644 --- a/typescript/Frontend/src/components/Layout/Search.tsx +++ b/typescript/Frontend/src/components/Layout/Search.tsx @@ -12,7 +12,7 @@ const SearchSidebar = (props: SearchSidebarProps) => { const intl = useIntl(); return ( -
+
{ }, {} as GraphData); }; -export const transformToGraphData = (timeStampData: RecordSeries) => { - return timeStampData.reduce((acc, curr) => { - if (isDefined(curr.value)) { - const timeStampObj = Object.keys(curr.value).reduce( - (pathAcc, currPath) => { - if (currPath) { - return { - ...pathAcc, - [currPath]: { - x: [new Date(curr.time.ticks * 1000)], - y: [curr.value ? curr.value[currPath] : 0], - }, - }; - } - return pathAcc; - }, - {} as GraphData - ); - return mergeDeep(acc, timeStampObj); - } - return acc; - }, {} as GraphData); -}; - export interface GraphCoordinates { x: Datum[] | Datum[][] | TypedArray; y: Datum[] | Datum[][] | TypedArray;