working graphs and toggles

This commit is contained in:
Sina Blattmann 2023-05-09 13:50:47 +02:00
parent b7c443fc93
commit d722f42d99
9 changed files with 332 additions and 203 deletions

View File

@ -40,7 +40,6 @@ const Login = ({ setToken, setLanguage }: I_LoginProps) => {
.then(() => { .then(() => {
setToken(data.token); setToken(data.token);
saveCurrentUser(data.user); saveCurrentUser(data.user);
console.log(data.user);
setLoading(false); setLoading(false);
setLanguage(data.user.language); setLanguage(data.user.language);
}) })

View File

@ -34,7 +34,6 @@ const ResetPassword = () => {
}) })
.then((res) => { .then((res) => {
saveCurrentUser(res.data); saveCurrentUser(res.data);
console.log(res.data);
setLoading(false); setLoading(false);
}) })
.catch((err) => setError(err)); .catch((err) => setError(err));

View File

@ -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<SetStateAction<ToggleElement | null>>;
}
export const LogContext = createContext<LogContextProviderProps>({
toggles: [],
setToggles: () => {},
checkedToggles: {},
setCheckedToggles: () => {},
});
const LogContextProvider = ({ children }: { children: ReactNode }) => {
const [toggles, setToggles] = useState<TreeElement[] | null>(null);
const [checkedToggles, setCheckedToggles] = useState<ToggleElement | null>(
null
);
console.log("provider", toggles);
return (
<LogContext.Provider
value={{
toggles,
setToggles,
checkedToggles,
setCheckedToggles,
}}
>
{children}
</LogContext.Provider>
);
};
export default LogContextProvider;

View File

@ -90,7 +90,6 @@ const UsersContextProvider = ({ children }: { children: ReactNode }) => {
const fetchAvailableUsers = async (): Promise<void> => { const fetchAvailableUsers = async (): Promise<void> => {
return axiosConfig.get("/GetAllChildUsers").then((res) => { return axiosConfig.get("/GetAllChildUsers").then((res) => {
console.log(res);
setAvailableUsers(res.data); setAvailableUsers(res.data);
}); });
}; };

View File

@ -8,16 +8,20 @@ import InstallationContextProvider from "../Context/InstallationContextProvider"
import SearchSidebar from "../Layout/Search"; import SearchSidebar from "../Layout/Search";
import InstallationList from "./InstallationList"; import InstallationList from "./InstallationList";
import Installation from "./Installation"; import Installation from "./Installation";
import CheckboxTree from "./Log/CheckboxTree";
import LogContextProvider from "../Context/LogContextProvider";
const Installations = () => { const Installations = () => {
return ( return (
<InstallationContextProvider> <InstallationContextProvider>
<LogContextProvider>
<Grid container spacing={2} height="100%"> <Grid container spacing={2} height="100%">
<Grid item xs={3}> <Grid item xs={3}>
<SearchSidebar <SearchSidebar
id="installations-search-sidebar" id="installations-search-sidebar"
listComponent={InstallationList} listComponent={InstallationList}
/> />
<CheckboxTree />
</Grid> </Grid>
<Grid item xs={9}> <Grid item xs={9}>
<InstallationTabs /> <InstallationTabs />
@ -32,6 +36,7 @@ const Installations = () => {
</Routes> </Routes>
</Grid> </Grid>
</Grid> </Grid>
</LogContextProvider>
</InstallationContextProvider> </InstallationContextProvider>
); );
}; };

View File

@ -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<HTMLButtonElement, MouseEvent>,
element: TreeElement,
checked?: boolean
) => {
event.stopPropagation();
handleCheckChildren([element], checked);
};
const handleExpandClick = (
event: React.MouseEvent<HTMLLIElement, MouseEvent>
) => {
event.stopPropagation();
};
const renderTree = (data: TreeElement[]): ReactNode => {
return data.map((element) => {
const checked = checkedToggles?.[element.id];
const splitName = element.name.split("/");
return (
<TreeItem
id={"checkbox-tree-" + element.name}
key={element.name}
nodeId={element.name}
onClick={handleExpandClick}
label={
<>
<Checkbox
checked={checked}
onClick={(e) => handleClick(e, element, checked)}
/>
{splitName[splitName.length - 1]}
</>
}
sx={{
".MuiTreeItem-content": { paddingY: "5px" },
}}
>
{getNodes(element)}
</TreeItem>
);
});
};
return (
<>
<Divider sx={{ mt: 2 }} />
{toggles !== null && routeMatch !== null && (
<TreeView
aria-label="rich object"
defaultCollapseIcon={<ExpandMoreIcon />}
defaultExpandIcon={<ChevronRightIcon />}
sx={{
height: 480,
flexGrow: 1,
overflow: "auto",
overflowX: "hidden",
}}
>
{renderTree(toggles)}
</TreeView>
)}
</>
);
};
export default CheckboxTree;

View File

@ -1,15 +1,15 @@
import Plot from "react-plotly.js"; import Plot from "react-plotly.js";
import { RecordSeries } from "../../../dataCache/data"; 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 { 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 { BehaviorSubject, startWith, throttleTime, withLatestFrom } from "rxjs";
import { S3Access } from "../../../dataCache/S3/S3Access"; import { S3Access } from "../../../dataCache/S3/S3Access";
import DataCache, { FetchResult } from "../../../dataCache/dataCache"; import DataCache, { FetchResult } from "../../../dataCache/dataCache";
import { TreeItem, TreeView } from "@mui/lab";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; import { LogContext } from "../../Context/LogContextProvider";
import ChevronRightIcon from "@mui/icons-material/ChevronRight"; import { TreeElement, ToggleElement } from "./CheckboxTree";
import { Checkbox } from "@mui/material"; import { isDefined } from "../../../dataCache/utils/maybe";
export const createTimes = ( export const createTimes = (
range: TimeRange, range: TimeRange,
@ -27,16 +27,19 @@ const NUMBER_OF_NODES = 100;
const ScalarGraph = () => { const ScalarGraph = () => {
const timeRange = createTimes( const timeRange = createTimes(
UnixTime.fromTicks(1682085650).rangeBefore(TimeSpan.fromDays(1)), UnixTime.fromTicks(1682085650).rangeBefore(TimeSpan.fromDays(4)),
NUMBER_OF_NODES NUMBER_OF_NODES
); );
const [timeSeries, setTimeSeries] = useState<RecordSeries>([]); const [timeSeries, setTimeSeries] = useState<RecordSeries>([]);
const [uiRevision, setUiRevision] = useState(Math.random());
const [range, setRange] = useState([ const [range, setRange] = useState([
timeRange[0].toDate(), timeRange[0].toDate(),
timeRange[timeRange.length - 1].toDate(), timeRange[timeRange.length - 1].toDate(),
]); ]);
const [toggles, setToggles] = useState<TreeElement[] | null>(null); const [uiRevision, setUiRevision] = useState(Math.random());
const [plotTitles, setPlotTitles] = useState<string[]>([]);
const { toggles, setToggles, setCheckedToggles, checkedToggles } =
useContext(LogContext);
const times$ = useMemo(() => new BehaviorSubject(timeRange), []); const times$ = useMemo(() => new BehaviorSubject(timeRange), []);
@ -49,23 +52,37 @@ const ScalarGraph = () => {
"" ""
); );
function insert( const insert = (
children: TreeElement[] = [], children: TreeElement[] = [],
[head, ...tail]: string[] [head, ...tail]: string[]
): TreeElement[] { ): TreeElement[] => {
let child = children.find((child) => child.name === head); let child = children.find((child) => child.name === head);
if (!child)
if (!child) {
children.push( children.push(
(child = { (child = {
id: head + tail.join("_"), id: head,
name: head, name: head,
checked: false,
children: [], children: [],
}) })
); );
if (tail.length > 0) insert(child.children, tail);
return children;
} }
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(() => { useEffect(() => {
const subscription = cache.gotData const subscription = cache.gotData
@ -78,79 +95,34 @@ const ScalarGraph = () => {
const timeSeries = cache.getSeries(times); const timeSeries = cache.getSeries(times);
setTimeSeries(timeSeries); setTimeSeries(timeSeries);
const toggleValues = timeSeries.find((timeStamp) => timeStamp.value); const toggleValues = timeSeries.find((timeStamp) => timeStamp.value);
if (toggles === null && toggleValues && toggleValues.value) { if (toggles === null && toggleValues && toggleValues.value) {
setToggles( console.log("toggles inside", toggles);
Object.keys(toggleValues.value) const treeElements = Object.keys(toggleValues.value)
.map((path) => path.split("/").slice(1)) .map((path) => {
return path
.split("/")
.map(
(split, i) =>
"/" +
path
.split("/")
.slice(1, i + 1)
.join("/")
)
.slice(1);
})
.reduce( .reduce(
(children, path) => insert(children, path), (children, path) => insert(children, path),
[] as TreeElement[] [] as TreeElement[]
)
); );
console.log("elements", treeElements);
setToggles(treeElements);
setCheckedToggles(flattenToggles(treeElements));
} }
}); });
return () => subscription.unsubscribe(); return () => subscription.unsubscribe();
}, []); }, [toggles]);
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 (
<TreeItem
id={"checkbox-tree-" + element.name}
key={element.name}
nodeId={element.name}
label={
<>
<Checkbox
checked={element.checked}
onClick={() => {
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)}
</TreeItem>
);
});
};
const round = (date: Date) => {
const minutes = 30;
const ms = 1000 * 60 * minutes;
return new Date(Math.round(date.getTime() / ms) * ms);
};
const fetchData = ( const fetchData = (
timestamp: UnixTime timestamp: UnixTime
@ -179,12 +151,60 @@ const ScalarGraph = () => {
[] []
); );
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
);
};
const renderGraphs = () => { const renderGraphs = () => {
if (checkedToggles) {
const coordinateTimeSeries = transformToGraphData(timeSeries); const coordinateTimeSeries = transformToGraphData(timeSeries);
return Object.keys(coordinateTimeSeries).map((path) => { console.log("coordinateTimeSeries", coordinateTimeSeries, checkedToggles);
return Object.keys(coordinateTimeSeries)
.filter((path) => {
return checkedToggles[path];
})
.map((path) => {
const data = coordinateTimeSeries[path] ?? { x: [], y: [] }; const data = coordinateTimeSeries[path] ?? { x: [], y: [] };
return ( return (
<Plot <Plot
key={path}
data={[ data={[
{ {
...data, ...data,
@ -226,6 +246,7 @@ const ScalarGraph = () => {
), ),
NUMBER_OF_NODES NUMBER_OF_NODES
); );
console.log("times", times);
cache.getSeries(times); cache.getSeries(times);
times$.next(times); times$.next(times);
} }
@ -233,27 +254,8 @@ const ScalarGraph = () => {
/> />
); );
}); });
}
}; };
return <>{renderGraphs()}</>;
return (
<>
{renderGraphs()}
{toggles !== null && (
<TreeView
aria-label="rich object"
defaultCollapseIcon={<ExpandMoreIcon />}
defaultExpandIcon={<ChevronRightIcon />}
sx={{
height: 480,
flexGrow: 1,
overflow: "auto",
overflowX: "hidden",
}}
>
{renderTree(toggles)}
</TreeView>
)}
</>
);
}; };
export default ScalarGraph; export default ScalarGraph;

View File

@ -12,7 +12,7 @@ const SearchSidebar = (props: SearchSidebarProps) => {
const intl = useIntl(); const intl = useIntl();
return ( return (
<div style={{ height: "100%" }}> <div style={{ height: "500px", overflow: "hidden" }}>
<TextField <TextField
id={id} id={id}
label={intl.formatMessage({ label={intl.formatMessage({

View File

@ -22,30 +22,6 @@ export const mergeDeep = (...objects: any[]) => {
}, {} as GraphData); }, {} 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 { export interface GraphCoordinates {
x: Datum[] | Datum[][] | TypedArray; x: Datum[] | Datum[][] | TypedArray;
y: Datum[] | Datum[][] | TypedArray; y: Datum[] | Datum[][] | TypedArray;