Merge remote-tracking branch 'origin/main'
This commit is contained in:
commit
69c105296f
|
@ -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);
|
||||||
})
|
})
|
||||||
|
|
|
@ -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));
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
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
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<LogContext.Provider
|
||||||
|
value={{
|
||||||
|
toggles,
|
||||||
|
setToggles,
|
||||||
|
checkedToggles,
|
||||||
|
setCheckedToggles,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</LogContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LogContextProvider;
|
|
@ -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);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,109 @@
|
||||||
|
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;
|
|
@ -1,65 +1,10 @@
|
||||||
import React, { useState } from "react";
|
import React from "react";
|
||||||
import ScalarGraph from "./ScalarGraph";
|
import ScalarGraph from "./ScalarGraph";
|
||||||
import DataCache, { FetchResult } from "../../../dataCache/dataCache";
|
|
||||||
import { TimeSpan, UnixTime } from "../../../dataCache/time";
|
|
||||||
import { S3Access } from "../../../dataCache/S3/S3Access";
|
|
||||||
import { map, debounceTime } from "rxjs/operators";
|
|
||||||
import { RecordSeries } from "../../../dataCache/data";
|
|
||||||
import { parseCsv } from "../../../util/graph.util";
|
|
||||||
|
|
||||||
const Log = () => {
|
const Log = () => {
|
||||||
const [timeSeries, setTimeSeries] = useState<RecordSeries>([]);
|
|
||||||
const s3Access = new S3Access(
|
|
||||||
"saliomameiringen",
|
|
||||||
"sos-ch-dk-2",
|
|
||||||
"exo.io",
|
|
||||||
"EXO18e7ae9e53fae71ee55cf35b",
|
|
||||||
"3Cyonq8gMQ0a3elTH2vP7Yv-czcCj8iE2lBcPB9XhSc",
|
|
||||||
""
|
|
||||||
);
|
|
||||||
|
|
||||||
const fetchData = (
|
|
||||||
timestamp: UnixTime
|
|
||||||
): Promise<FetchResult<Record<string, number>>> => {
|
|
||||||
const s3Path = `${timestamp.ticks}.csv`;
|
|
||||||
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 {
|
|
||||||
console.error("unexpected status code");
|
|
||||||
return Promise.resolve(FetchResult.notAvailable);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
console.log(e);
|
|
||||||
return Promise.resolve(FetchResult.tryLater);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const cache = new DataCache(fetchData, TimeSpan.fromSeconds(2));
|
|
||||||
|
|
||||||
const sampleTimes = UnixTime.fromTicks(1682085650)
|
|
||||||
.earlier(TimeSpan.fromMinutes(1))
|
|
||||||
.rangeBefore(TimeSpan.fromMinutes(1))
|
|
||||||
.sample(TimeSpan.fromSeconds(2));
|
|
||||||
|
|
||||||
cache.getSeries(sampleTimes);
|
|
||||||
|
|
||||||
const update = cache.gotData.pipe(
|
|
||||||
debounceTime(2000),
|
|
||||||
map((_) => setTimeSeries(cache.getSeries(sampleTimes)))
|
|
||||||
);
|
|
||||||
|
|
||||||
update.subscribe();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ScalarGraph data={timeSeries} />
|
<ScalarGraph />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,26 +1,243 @@
|
||||||
import Plot from "react-plotly.js";
|
import Plot from "react-plotly.js";
|
||||||
import { RecordSeries } from "../../../dataCache/data";
|
import { RecordSeries } from "../../../dataCache/data";
|
||||||
import { transformToGraphData } from "../../../util/graph.util";
|
import {
|
||||||
|
GraphCoordinates,
|
||||||
|
GraphData,
|
||||||
|
mergeDeep,
|
||||||
|
parseCsv,
|
||||||
|
} from "../../../util/graph.util";
|
||||||
|
import { TimeRange, TimeSpan, UnixTime } from "../../../dataCache/time";
|
||||||
|
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";
|
||||||
|
|
||||||
interface I_ScalarGraphProps {
|
import { LogContext } from "../../Context/LogContextProvider";
|
||||||
data: RecordSeries;
|
import { TreeElement, ToggleElement } from "./CheckboxTree";
|
||||||
}
|
import { isDefined } from "../../../dataCache/utils/maybe";
|
||||||
|
import { timeStamp } from "console";
|
||||||
|
|
||||||
|
export const createTimes = (
|
||||||
|
range: TimeRange,
|
||||||
|
numberOfNodes: number
|
||||||
|
): UnixTime[] => {
|
||||||
|
const oneSpan = range.duration.divide(numberOfNodes);
|
||||||
|
const roundedRange = TimeRange.fromTimes(
|
||||||
|
range.start.round(oneSpan),
|
||||||
|
range.end.round(oneSpan)
|
||||||
|
);
|
||||||
|
return roundedRange.sample(oneSpan);
|
||||||
|
};
|
||||||
|
|
||||||
|
const NUMBER_OF_NODES = 100;
|
||||||
|
|
||||||
|
const ScalarGraph = () => {
|
||||||
|
const timeRange = createTimes(
|
||||||
|
UnixTime.now() /* .fromTicks(1682085650) */
|
||||||
|
.rangeBefore(TimeSpan.fromDays(4)),
|
||||||
|
NUMBER_OF_NODES
|
||||||
|
);
|
||||||
|
const [timeSeries, setTimeSeries] = useState<RecordSeries>([]);
|
||||||
|
const [range, setRange] = useState([
|
||||||
|
timeRange[0].toDate().getTime(),
|
||||||
|
timeRange[timeRange.length - 1].toDate().getTime(),
|
||||||
|
]);
|
||||||
|
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 s3Access = new S3Access(
|
||||||
|
"saliomameiringen",
|
||||||
|
"sos-ch-dk-2",
|
||||||
|
"exo.io",
|
||||||
|
"EXO18e7ae9e53fae71ee55cf35b",
|
||||||
|
"3Cyonq8gMQ0a3elTH2vP7Yv-czcCj8iE2lBcPB9XhSc",
|
||||||
|
""
|
||||||
|
);
|
||||||
|
|
||||||
|
const insert = (
|
||||||
|
children: TreeElement[] = [],
|
||||||
|
[head, ...tail]: string[]
|
||||||
|
): TreeElement[] => {
|
||||||
|
let child = children.find((child) => child.name === head);
|
||||||
|
|
||||||
|
if (!child) {
|
||||||
|
children.push(
|
||||||
|
(child = {
|
||||||
|
id: head,
|
||||||
|
name: head,
|
||||||
|
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(() => {
|
||||||
|
const subscription = cache.gotData
|
||||||
|
.pipe(
|
||||||
|
startWith(0),
|
||||||
|
throttleTime(200, undefined, { leading: true, trailing: true }),
|
||||||
|
withLatestFrom(times$)
|
||||||
|
)
|
||||||
|
.subscribe(([_, times]) => {
|
||||||
|
const timeSeries = cache.getSeries(times);
|
||||||
|
setTimeSeries(timeSeries);
|
||||||
|
const toggleValues = timeSeries.find((timeStamp) => timeStamp.value);
|
||||||
|
if (toggles === null && toggleValues && toggleValues.value) {
|
||||||
|
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[]
|
||||||
|
);
|
||||||
|
setToggles(treeElements);
|
||||||
|
setCheckedToggles(flattenToggles(treeElements));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => subscription.unsubscribe();
|
||||||
|
}, [toggles]);
|
||||||
|
|
||||||
|
const fetchData = (
|
||||||
|
timestamp: UnixTime
|
||||||
|
): Promise<FetchResult<Record<string, number>>> => {
|
||||||
|
const s3Path = `${timestamp.ticks}.csv`;
|
||||||
|
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 {
|
||||||
|
console.error("unexpected status code");
|
||||||
|
return Promise.resolve(FetchResult.notAvailable);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
return Promise.resolve(FetchResult.tryLater);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const cache = useMemo(
|
||||||
|
() => new DataCache(fetchData, TimeSpan.fromSeconds(2)),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
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 ScalarGraph = (props: I_ScalarGraphProps) => {
|
|
||||||
const renderGraphs = () => {
|
const renderGraphs = () => {
|
||||||
const coordinateTimeSeries = transformToGraphData(props.data);
|
const coordinateTimeSeries = transformToGraphData(timeSeries);
|
||||||
return Object.keys(coordinateTimeSeries).map((path) => {
|
console.log("coordinates", coordinateTimeSeries);
|
||||||
|
const graphCoordinates: GraphCoordinates[] = Object.keys(
|
||||||
|
coordinateTimeSeries
|
||||||
|
)
|
||||||
|
.filter((path) => {
|
||||||
|
return checkedToggles?.[path];
|
||||||
|
})
|
||||||
|
.map((path, i) => {
|
||||||
|
return {
|
||||||
|
...coordinateTimeSeries[path],
|
||||||
|
xaxis: "x",
|
||||||
|
yaxis: i === 0 ? "y" : "y" + (i + 1),
|
||||||
|
type: "scatter",
|
||||||
|
};
|
||||||
|
});
|
||||||
|
if (checkedToggles && graphCoordinates.length > 0) {
|
||||||
|
const subplots = graphCoordinates.map((coordinate) => [
|
||||||
|
(coordinate?.xaxis || "") + (coordinate.yaxis || ""),
|
||||||
|
]);
|
||||||
return (
|
return (
|
||||||
<Plot
|
<Plot
|
||||||
data={[
|
data={graphCoordinates}
|
||||||
{
|
layout={{
|
||||||
...coordinateTimeSeries[path],
|
title: "Graphs",
|
||||||
type: "scatter",
|
uirevision: uiRevision,
|
||||||
mode: "lines+markers",
|
showlegend: false,
|
||||||
marker: { color: "red" },
|
xaxis: {
|
||||||
|
autorange: false,
|
||||||
|
range: range,
|
||||||
|
type: "date",
|
||||||
|
showticklabels: true,
|
||||||
},
|
},
|
||||||
]}
|
grid: {
|
||||||
layout={{ width: 1000, height: 500, title: path }}
|
subplots: subplots as any,
|
||||||
|
xside: "top",
|
||||||
|
ygap: 0.1,
|
||||||
|
},
|
||||||
|
height: graphCoordinates.length * 300,
|
||||||
|
}}
|
||||||
config={{
|
config={{
|
||||||
modeBarButtonsToRemove: [
|
modeBarButtonsToRemove: [
|
||||||
"lasso2d",
|
"lasso2d",
|
||||||
|
@ -29,14 +246,32 @@ const ScalarGraph = (props: I_ScalarGraphProps) => {
|
||||||
"autoScale2d",
|
"autoScale2d",
|
||||||
],
|
],
|
||||||
}}
|
}}
|
||||||
onUpdate={(figure) => {
|
onRelayout={(params) => {
|
||||||
//console.log(figure);
|
const xaxisRange0 = params["xaxis.range[0]"];
|
||||||
|
const xaxisRange1 = params["xaxis.range[1]"];
|
||||||
|
|
||||||
|
if (xaxisRange0 && xaxisRange1) {
|
||||||
|
setRange([
|
||||||
|
new Date(xaxisRange0).getTime(),
|
||||||
|
new Date(xaxisRange1).getTime(),
|
||||||
|
]);
|
||||||
|
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()}</>;
|
return <>{renderGraphs()}</>;
|
||||||
};
|
};
|
||||||
export default ScalarGraph;
|
export default ScalarGraph;
|
||||||
|
|
|
@ -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({
|
||||||
|
|
|
@ -2,12 +2,12 @@ import { Maybe } from "yup";
|
||||||
import {Timestamped} from "./types";
|
import {Timestamped} from "./types";
|
||||||
import { isDefined } from "./utils/maybe";
|
import { isDefined } from "./utils/maybe";
|
||||||
|
|
||||||
export type DataRecord = Record<string, number>
|
export type DataRecord = Record<string, number | string>
|
||||||
|
|
||||||
export type DataPoint = Timestamped<Maybe<DataRecord>>
|
export type DataPoint = Timestamped<Maybe<DataRecord>>
|
||||||
export type RecordSeries = Array<DataPoint>
|
export type RecordSeries = Array<DataPoint>
|
||||||
export type PointSeries = Array<Timestamped<Maybe<number>>>
|
export type PointSeries = Array<Timestamped<Maybe<number| string>>>
|
||||||
export type DataSeries = Array<Maybe<number>>
|
export type DataSeries = Array<Maybe<number| string>>
|
||||||
|
|
||||||
export function getPoints(recordSeries: RecordSeries, series: keyof DataRecord): PointSeries
|
export function getPoints(recordSeries: RecordSeries, series: keyof DataRecord): PointSeries
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,169 +1,158 @@
|
||||||
/* eslint-disable no-mixed-operators */
|
/* eslint-disable no-mixed-operators */
|
||||||
import {TimeSpan, UnixTime} from "./time";
|
import { TimeSpan, UnixTime } from "./time";
|
||||||
import {Observable, Subject} from "rxjs";
|
import { Observable, Subject } from "rxjs";
|
||||||
import {SkipList} from "./skipList/skipList";
|
import { SkipList } from "./skipList/skipList";
|
||||||
import {createDispatchQueue} from "./promiseQueue";
|
import { createDispatchQueue } from "./promiseQueue";
|
||||||
import {SkipListNode} from "./skipList/skipListNode";
|
import { SkipListNode } from "./skipList/skipListNode";
|
||||||
import {RecordSeries} from "./data";
|
import { RecordSeries } from "./data";
|
||||||
import { Maybe, isUndefined } from "./utils/maybe";
|
import { Maybe, isUndefined } from "./utils/maybe";
|
||||||
|
import { isNumber, isString } from "./utils/runtimeTypeChecking";
|
||||||
|
|
||||||
|
export const FetchResult = {
|
||||||
export const FetchResult =
|
notAvailable: "N/A",
|
||||||
{
|
tryLater: "Try Later",
|
||||||
notAvailable : "N/A",
|
} as const;
|
||||||
tryLater : "Try Later"
|
|
||||||
} as const
|
|
||||||
|
|
||||||
export type FetchResult<T> =
|
export type FetchResult<T> =
|
||||||
| T
|
| T
|
||||||
| typeof FetchResult.notAvailable
|
| typeof FetchResult.notAvailable
|
||||||
| typeof FetchResult.tryLater
|
| typeof FetchResult.tryLater;
|
||||||
|
|
||||||
function reverseBits(x : number): number
|
function reverseBits(x: number): number {
|
||||||
{
|
// https://stackoverflow.com/a/60227327/141397
|
||||||
// https://stackoverflow.com/a/60227327/141397
|
|
||||||
|
|
||||||
x = (x & 0x55555555) << 1 | (x & 0xAAAAAAAA) >> 1;
|
x = ((x & 0x55555555) << 1) | ((x & 0xaaaaaaaa) >> 1);
|
||||||
x = (x & 0x33333333) << 2 | (x & 0xCCCCCCCC) >> 2;
|
x = ((x & 0x33333333) << 2) | ((x & 0xcccccccc) >> 2);
|
||||||
x = (x & 0x0F0F0F0F) << 4 | (x & 0xF0F0F0F0) >> 4;
|
x = ((x & 0x0f0f0f0f) << 4) | ((x & 0xf0f0f0f0) >> 4);
|
||||||
x = (x & 0x00FF00FF) << 8 | (x & 0xFF00FF00) >> 8;
|
x = ((x & 0x00ff00ff) << 8) | ((x & 0xff00ff00) >> 8);
|
||||||
x = (x & 0x0000FFFF) << 16 | (x & 0xFFFF0000) >> 16;
|
x = ((x & 0x0000ffff) << 16) | ((x & 0xffff0000) >> 16);
|
||||||
|
|
||||||
return x >>> 0;
|
return x >>> 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default class DataCache<T extends Record<string, number>> {
|
||||||
|
private readonly cache: SkipList<Maybe<T>> = new SkipList<Maybe<T>>();
|
||||||
|
private readonly resolution: TimeSpan;
|
||||||
|
|
||||||
export default class DataCache<T extends Record<string, number>>
|
readonly _fetch: (t: UnixTime) => Promise<FetchResult<T>>;
|
||||||
{
|
|
||||||
private readonly cache: SkipList<Maybe<T>> = new SkipList<Maybe<T>>()
|
|
||||||
private readonly resolution: TimeSpan;
|
|
||||||
|
|
||||||
readonly _fetch: (t: UnixTime) => Promise<FetchResult<T>>;
|
private readonly fetchQueue = createDispatchQueue(6);
|
||||||
|
private readonly fetching: Set<number> = new Set<number>();
|
||||||
|
|
||||||
private readonly fetchQueue = createDispatchQueue(6)
|
public readonly gotData: Observable<UnixTime>;
|
||||||
private readonly fetching: Set<number> = new Set<number>()
|
|
||||||
|
|
||||||
public readonly gotData: Observable<UnixTime>;
|
constructor(
|
||||||
|
fetch: (t: UnixTime) => Promise<FetchResult<T>>,
|
||||||
|
resolution: TimeSpan
|
||||||
|
) {
|
||||||
|
this._fetch = fetch;
|
||||||
|
this.resolution = resolution;
|
||||||
|
this.gotData = new Subject<UnixTime>();
|
||||||
|
}
|
||||||
|
|
||||||
constructor(fetch: (t: UnixTime) => Promise<FetchResult<T>>, resolution: TimeSpan)
|
public prefetch(times: Array<UnixTime>, clear = true) {
|
||||||
{
|
if (clear) {
|
||||||
this._fetch = fetch;
|
this.fetching.clear();
|
||||||
this.resolution = resolution;
|
this.fetchQueue.clear();
|
||||||
this.gotData = new Subject<UnixTime>()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public prefetch(times: Array<UnixTime>, clear = true)
|
const timesWithPriority = times.map((time, index) => ({
|
||||||
{
|
time,
|
||||||
if (clear)
|
priority: reverseBits(index),
|
||||||
{
|
}));
|
||||||
this.fetching.clear()
|
timesWithPriority.sort((x, y) => x.priority - y.priority);
|
||||||
this.fetchQueue.clear()
|
|
||||||
|
for (let i = 0; i < timesWithPriority.length; i++) {
|
||||||
|
const time = timesWithPriority[i].time.round(this.resolution);
|
||||||
|
const t = time.ticks;
|
||||||
|
|
||||||
|
const node = this.cache.find(t);
|
||||||
|
if (node.index !== t) this.fetchData(time);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public get(timeStamp: UnixTime, fetch = true): Maybe<T> {
|
||||||
|
const time = timeStamp.round(this.resolution);
|
||||||
|
const t = time.ticks;
|
||||||
|
|
||||||
|
const node = this.cache.find(t);
|
||||||
|
if (node.index === t) return node.value;
|
||||||
|
|
||||||
|
if (fetch) this.fetchData(time);
|
||||||
|
|
||||||
|
return this.interpolate(node, t);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getSeries(sampleTimes: UnixTime[]): RecordSeries {
|
||||||
|
this.prefetch(sampleTimes);
|
||||||
|
return sampleTimes.map((time) => ({ time, value: this.get(time, false) }));
|
||||||
|
}
|
||||||
|
|
||||||
|
private interpolate(before: SkipListNode<Maybe<T>>, t: number): Maybe<T> {
|
||||||
|
const dataBefore = before.value;
|
||||||
|
const after = before.next[0];
|
||||||
|
const dataAfter = after.value;
|
||||||
|
|
||||||
|
if (isUndefined(dataBefore) && isUndefined(dataAfter)) return undefined;
|
||||||
|
|
||||||
|
if (isUndefined(dataBefore)) return dataAfter;
|
||||||
|
|
||||||
|
if (isUndefined(dataAfter)) return dataBefore;
|
||||||
|
|
||||||
|
const p = t - before.index;
|
||||||
|
const n = after.index - t;
|
||||||
|
const pn = p + n;
|
||||||
|
|
||||||
|
let interpolated: Partial<Record<string, number>> = {};
|
||||||
|
|
||||||
|
//What about string nodes? like Alarms
|
||||||
|
for (const k of Object.keys(dataBefore)) {
|
||||||
|
interpolated[k] = isNumber(dataBefore[k])
|
||||||
|
? (dataBefore[k] * n + dataAfter[k] * p) / pn
|
||||||
|
: n < p
|
||||||
|
? dataAfter[k]
|
||||||
|
: dataBefore[k];
|
||||||
|
}
|
||||||
|
|
||||||
|
return interpolated as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
private fetchData(time: UnixTime) {
|
||||||
|
const t = time.ticks;
|
||||||
|
|
||||||
|
if (this.fetching.has(t))
|
||||||
|
// we are already fetching t
|
||||||
|
return;
|
||||||
|
|
||||||
|
const fetchTask = () => {
|
||||||
|
const onSuccess = (data: FetchResult<T>) => {
|
||||||
|
if (data === FetchResult.tryLater) {
|
||||||
|
console.warn(FetchResult.tryLater);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const timesWithPriority = times.map((time, index) => ({time, priority: reverseBits(index)}))
|
const value = data === FetchResult.notAvailable ? undefined : data;
|
||||||
timesWithPriority.sort((x, y) => x.priority - y.priority)
|
this.cache.insert(value, t);
|
||||||
|
};
|
||||||
|
|
||||||
for (let i = 0; i < timesWithPriority.length; i++)
|
const onFailure = (_: unknown) => {
|
||||||
{
|
console.error(time.ticks + " FAILED!"); // should not happen
|
||||||
const time = timesWithPriority[i].time.round(this.resolution)
|
};
|
||||||
const t = time.ticks;
|
|
||||||
|
|
||||||
const node = this.cache.find(t);
|
const dispatch = () => {
|
||||||
if (node.index !== t)
|
this.fetching.delete(time.ticks);
|
||||||
this.fetchData(time);
|
(this.gotData as Subject<UnixTime>).next(time);
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
||||||
public get(timeStamp: UnixTime, fetch = true): Maybe<T>
|
return this._fetch(time)
|
||||||
{
|
.then(
|
||||||
const time = timeStamp.round(this.resolution)
|
(d) => onSuccess(d),
|
||||||
const t = time.ticks;
|
(f) => onFailure(f)
|
||||||
|
)
|
||||||
|
.finally(() => dispatch());
|
||||||
|
};
|
||||||
|
|
||||||
const node = this.cache.find(t);
|
this.fetching.add(t);
|
||||||
if (node.index === t)
|
this.fetchQueue.dispatch(() => fetchTask());
|
||||||
return node.value
|
}
|
||||||
|
}
|
||||||
if (fetch)
|
|
||||||
this.fetchData(time);
|
|
||||||
|
|
||||||
return this.interpolate(node, t)
|
|
||||||
}
|
|
||||||
|
|
||||||
public getSeries(sampleTimes: UnixTime[]): RecordSeries
|
|
||||||
{
|
|
||||||
this.prefetch(sampleTimes)
|
|
||||||
return sampleTimes.map(time => ({time, value: this.get(time, false)}))
|
|
||||||
}
|
|
||||||
|
|
||||||
private interpolate(before: SkipListNode<Maybe<T>>, t: number): Maybe<T>
|
|
||||||
{
|
|
||||||
const dataBefore = before.value
|
|
||||||
const after = before.next[0];
|
|
||||||
const dataAfter = after.value
|
|
||||||
|
|
||||||
if (isUndefined(dataBefore) && isUndefined(dataAfter))
|
|
||||||
return undefined
|
|
||||||
|
|
||||||
if (isUndefined(dataBefore))
|
|
||||||
return dataAfter
|
|
||||||
|
|
||||||
if (isUndefined(dataAfter))
|
|
||||||
return dataBefore
|
|
||||||
|
|
||||||
const p = t - before.index
|
|
||||||
const n = after.index - t
|
|
||||||
const pn = p + n
|
|
||||||
|
|
||||||
let interpolated: Partial<Record<string, number>> = {}
|
|
||||||
|
|
||||||
//What about string nodes? like Alarms
|
|
||||||
for (const k of Object.keys(dataBefore))
|
|
||||||
{
|
|
||||||
interpolated[k] = (dataBefore[k] * n + dataAfter[k] * p) / pn
|
|
||||||
}
|
|
||||||
|
|
||||||
return interpolated as T
|
|
||||||
}
|
|
||||||
|
|
||||||
private fetchData(time: UnixTime)
|
|
||||||
{
|
|
||||||
const t = time.ticks;
|
|
||||||
|
|
||||||
if (this.fetching.has(t)) // we are already fetching t
|
|
||||||
return
|
|
||||||
|
|
||||||
const fetchTask = () =>
|
|
||||||
{
|
|
||||||
const onSuccess = (data: FetchResult<T>) =>
|
|
||||||
{
|
|
||||||
if (data === FetchResult.tryLater)
|
|
||||||
{
|
|
||||||
console.warn(FetchResult.tryLater)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const value = data === FetchResult.notAvailable ? undefined : data;
|
|
||||||
this.cache.insert(value, t)
|
|
||||||
}
|
|
||||||
|
|
||||||
const onFailure = (_: unknown) =>
|
|
||||||
{
|
|
||||||
console.error(time.ticks + " FAILED!") // should not happen
|
|
||||||
}
|
|
||||||
|
|
||||||
const dispatch = () =>
|
|
||||||
{
|
|
||||||
this.fetching.delete(time.ticks);
|
|
||||||
(this.gotData as Subject<UnixTime>).next(time);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this._fetch(time)
|
|
||||||
.then(d => onSuccess(d), f => onFailure(f))
|
|
||||||
.finally(() => dispatch())
|
|
||||||
};
|
|
||||||
|
|
||||||
this.fetching.add(t)
|
|
||||||
this.fetchQueue.dispatch(() => fetchTask());
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
import { Datum, TypedArray } from "plotly.js";
|
import { Datum, TypedArray } from "plotly.js";
|
||||||
import { RecordSeries } from "../dataCache/data";
|
|
||||||
import { isDefined } from "../dataCache/utils/maybe";
|
|
||||||
|
|
||||||
export const mergeDeep = (...objects: any[]) => {
|
export const mergeDeep = (...objects: any[]) => {
|
||||||
const isObject = (obj: GraphCoordinates) => obj && typeof obj === "object";
|
const isObject = (obj: GraphCoordinates) => obj && typeof obj === "object";
|
||||||
|
@ -22,33 +20,11 @@ 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: [curr.time.ticks],
|
|
||||||
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;
|
||||||
|
xaxis?: string;
|
||||||
|
yaxis?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GraphData {
|
export interface GraphData {
|
||||||
|
@ -58,11 +34,26 @@ export interface GraphData {
|
||||||
export const parseCsv = (text: string) => {
|
export const parseCsv = (text: string) => {
|
||||||
const y = text
|
const y = text
|
||||||
.split(/\r?\n/)
|
.split(/\r?\n/)
|
||||||
.map((l) => l.split(";"))
|
.filter((split) => split.length > 0)
|
||||||
.filter((fields) => !isNaN(parseFloat(fields[1])));
|
.map((l) => {
|
||||||
|
if (l.length === 0) {
|
||||||
|
console.log("splitting", l, l.split(";"));
|
||||||
|
}
|
||||||
|
return l.split(";");
|
||||||
|
});
|
||||||
|
console.log("text", y);
|
||||||
|
|
||||||
|
/* .filter((fields) => !isNaN(parseFloat(fields[1])));
|
||||||
|
*/
|
||||||
const x = y
|
const x = y
|
||||||
.map((fields) => ({ [fields[0]]: parseFloat(fields[1]) }))
|
.map((fields) => {
|
||||||
.reduce((acc, current) => ({ ...acc, ...current }), {});
|
if (typeof fields[1] === "string") {
|
||||||
|
console.log("if inside", fields, { [fields[0]]: fields[1] });
|
||||||
|
return { [fields[0]]: fields[1] };
|
||||||
|
}
|
||||||
|
console.log("if outside", fields, { [fields[0]]: parseFloat(fields[1]) });
|
||||||
|
return { [fields[0]]: parseFloat(fields[1]) };
|
||||||
|
})
|
||||||
|
.reduce((acc, current) => ({ ...acc, ...current }), {} as any);
|
||||||
return x;
|
return x;
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue