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,30 +8,35 @@ 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>
<Grid container spacing={2} height="100%"> <LogContextProvider>
<Grid item xs={3}> <Grid container spacing={2} height="100%">
<SearchSidebar <Grid item xs={3}>
id="installations-search-sidebar" <SearchSidebar
listComponent={InstallationList} id="installations-search-sidebar"
/> listComponent={InstallationList}
</Grid>
<Grid item xs={9}>
<InstallationTabs />
<Routes>
<Route
path={routes.installation + ":id"}
element={<Installation />}
index
/> />
<Route path={routes.alarms + ":id"} element={<Alarms />} /> <CheckboxTree />
<Route path={routes.log + ":id"} element={<Log />} /> </Grid>
</Routes> <Grid item xs={9}>
<InstallationTabs />
<Routes>
<Route
path={routes.installation + ":id"}
element={<Installation />}
index
/>
<Route path={routes.alarms + ":id"} element={<Alarms />} />
<Route path={routes.log + ":id"} element={<Log />} />
</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); }
if (tail.length > 0) {
insert(child.children, tail);
}
return children; 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) => {
.reduce( return path
(children, path) => insert(children, path), .split("/")
[] as TreeElement[] .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(); 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,81 +151,111 @@ const ScalarGraph = () => {
[] []
); );
const renderGraphs = () => { const transformToGraphData = (timeStampData: RecordSeries) => {
const coordinateTimeSeries = transformToGraphData(timeSeries); const graphData = timeStampData.reduce((acc, curr) => {
return Object.keys(coordinateTimeSeries).map((path) => { if (isDefined(curr.value)) {
const data = coordinateTimeSeries[path] ?? { x: [], y: [] }; const timeStampObj = Object.keys(curr.value).reduce(
return ( (pathAcc, currPath) => {
<Plot if (currPath) {
data={[ return {
{ ...pathAcc,
...data, [currPath]: {
type: "scatter", x: [new Date(curr.time.ticks * 1000)],
mode: "lines+markers", y: [curr.value ? curr.value[currPath] : 0],
marker: { color: "red" }, },
}, };
]}
layout={{
width: 1000,
height: 500,
title: path,
uirevision: uiRevision,
xaxis: {
autorange: false,
range: range,
type: "date",
},
}}
config={{
modeBarButtonsToRemove: [
"lasso2d",
"select2d",
"pan2d",
"autoScale2d",
],
}}
onRelayout={(params) => {
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);
} }
}} 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 ( const renderGraphs = () => {
<> if (checkedToggles) {
{renderGraphs()} const coordinateTimeSeries = transformToGraphData(timeSeries);
{toggles !== null && ( console.log("coordinateTimeSeries", coordinateTimeSeries, checkedToggles);
<TreeView return Object.keys(coordinateTimeSeries)
aria-label="rich object" .filter((path) => {
defaultCollapseIcon={<ExpandMoreIcon />} return checkedToggles[path];
defaultExpandIcon={<ChevronRightIcon />} })
sx={{ .map((path) => {
height: 480, const data = coordinateTimeSeries[path] ?? { x: [], y: [] };
flexGrow: 1, return (
overflow: "auto", <Plot
overflowX: "hidden", key={path}
}} data={[
> {
{renderTree(toggles)} ...data,
</TreeView> type: "scatter",
)} mode: "lines+markers",
</> marker: { color: "red" },
); },
]}
layout={{
width: 1000,
height: 500,
title: path,
uirevision: uiRevision,
xaxis: {
autorange: false,
range: range,
type: "date",
},
}}
config={{
modeBarButtonsToRemove: [
"lasso2d",
"select2d",
"pan2d",
"autoScale2d",
],
}}
onRelayout={(params) => {
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; 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;