Skip to content

Commit

Permalink
feat(ui): Display MQTTClient status
Browse files Browse the repository at this point in the history
  • Loading branch information
Hypfer committed Feb 12, 2022
1 parent dcc1db4 commit c54ebc8
Show file tree
Hide file tree
Showing 8 changed files with 270 additions and 29 deletions.
9 changes: 9 additions & 0 deletions frontend/src/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
MapSegmentRenameRequestParameters,
MQTTConfiguration,
MQTTProperties,
MQTTStatus,
NTPClientConfiguration,
NTPClientState,
Point,
Expand Down Expand Up @@ -486,6 +487,14 @@ export const sendMQTTConfiguration = async (mqttConfiguration: MQTTConfiguration
});
};

export const fetchMQTTStatus = async (): Promise<MQTTStatus> => {
return valetudoAPI
.get<MQTTStatus>("/mqtt/status")
.then(({data}) => {
return data;
});
};

export const fetchMQTTProperties = async (): Promise<MQTTProperties> => {
return valetudoAPI
.get<MQTTProperties>("/mqtt/properties")
Expand Down
9 changes: 9 additions & 0 deletions frontend/src/api/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ import {
fetchQuirks,
sendSetQuirkValueCommand,
fetchRobotProperties,
fetchMQTTStatus,
} from "./client";
import {
PresetSelectionState,
Expand Down Expand Up @@ -144,6 +145,7 @@ enum CacheKey {
SystemHostInfo = "system_host_info",
SystemRuntimeInfo = "system_runtime_info",
MQTTConfiguration = "mqtt_configuration",
MQTTStatus = "mqtt_status",
MQTTProperties = "mqtt_properties",
HTTPBasicAuth = "http_basic_auth",
NTPClientState = "ntp_client_state",
Expand Down Expand Up @@ -647,6 +649,13 @@ export const useMQTTConfigurationMutation = () => {
);
};

export const useMQTTStatusQuery = () => {
return useQuery(CacheKey.MQTTStatus, fetchMQTTStatus, {
staleTime: 5_000,
refetchInterval: 5_000
});
};

export const useMQTTPropertiesQuery = () => {
return useQuery(CacheKey.MQTTProperties, fetchMQTTProperties, {
staleTime: Infinity,
Expand Down
22 changes: 22 additions & 0 deletions frontend/src/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,28 @@ export interface MQTTConfiguration {
};
}

export interface MQTTStatus {
state: "init" | "ready" | "disconnected" | "lost" | "alert",
stats: {
messages: {
count: {
received: number;
sent: number;
},
bytes: {
received: number;
sent: number;
}
},
connection: {
connects: number;
disconnects: number;
reconnects: number;
errors: number;
}
}
}

export interface MQTTProperties {
defaults: {
identity: {
Expand Down
27 changes: 27 additions & 0 deletions frontend/src/components/TextInformationGrid.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React from "react";
import {Grid, Typography} from "@mui/material";

const TextInformationGrid: React.FunctionComponent<{ items: Array<{ header: string, body: string }> }> = ({
items
}): JSX.Element => {
return (
<Grid
container
spacing={2}
style={{wordBreak: "break-all"}}
>
{items.map((item) => {
return (
<Grid item key={item.header}>
<Typography variant="caption" color="textSecondary">
{item.header}
</Typography>
<Typography variant="body2">{item.body}</Typography>
</Grid>
);
})}
</Grid>
);
};

export default TextInformationGrid;
190 changes: 187 additions & 3 deletions frontend/src/settings/connectivity/MQTTConnectivity.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import {
Box,
Card,
CardContent,
Checkbox,
Collapse,
Container,
Expand All @@ -23,23 +25,193 @@ import {
import {
ArrowUpward,
Visibility as VisibilityIcon,
VisibilityOff as VisibilityOffIcon
VisibilityOff as VisibilityOffIcon,

LinkOff as MQTTDisconnectedIcon,
Link as MQTTConnectedIcon,
Sync as MQTTConnectingIcon,
Warning as MQTTErrorIcon
} from "@mui/icons-material";
import React from "react";
import {
MQTTConfiguration,
MQTTStatus,
useMQTTConfigurationMutation,
useMQTTConfigurationQuery,
useMQTTPropertiesQuery
useMQTTPropertiesQuery,
useMQTTStatusQuery
} from "../../api";
import {getIn, setIn} from "../../api/utils";
import {deepCopy} from "../../utils";
import {convertBytesToHumans, deepCopy} from "../../utils";
import {InputProps} from "@mui/material/Input/Input";
import LoadingFade from "../../components/LoadingFade";
import InfoBox from "../../components/InfoBox";
import PaperContainer from "../../components/PaperContainer";
import {MQTTIcon} from "../../components/CustomIcons";
import {LoadingButton} from "@mui/lab";
import TextInformationGrid from "../../components/TextInformationGrid";

const MQTTStatusComponent : React.FunctionComponent<{ status: MQTTStatus | undefined, statusLoading: boolean, statusError: boolean }> = ({
status,
statusLoading,
statusError
}) => {

if (statusLoading || !status) {
return (
<LoadingFade/>
);
}

if (statusError) {
return <Typography color="error">Error loading MQTT status</Typography>;
}

const getIconForState = () : JSX.Element => {
switch (status.state) {
case "disconnected":
return <MQTTDisconnectedIcon sx={{ fontSize: "4rem" }}/>;
case "ready":
return <MQTTConnectedIcon sx={{ fontSize: "4rem" }}/>;
case "init":
return <MQTTConnectingIcon sx={{ fontSize: "4rem" }}/>;
case "lost":
case "alert":
return <MQTTErrorIcon sx={{fontSize: "4rem"}}/>;
}
};

const getContentForState = () : JSX.Element => {
switch (status.state) {
case "disconnected":
return (
<Typography variant="h5">Disconnected</Typography>
);
case "ready":
return (
<Typography variant="h5">Connected</Typography>
);
case "init":
return (
<Typography variant="h5">Connecting/Reconfiguring</Typography>
);
case "lost":
case "alert":
return (
<Typography variant="h5">Connection error</Typography>
);
}
};

const getMessageStats = () : JSX.Element => {
const items = [
{
header: "Messages Sent",
body: status.stats.messages.count.sent.toString()
},
{
header: "Bytes Sent",
body: convertBytesToHumans(status.stats.messages.bytes.sent)
},
{
header: "Messages Received",
body: status.stats.messages.count.received.toString()
},
{
header: "Bytes Received",
body: convertBytesToHumans(status.stats.messages.bytes.received)
},
];

return <TextInformationGrid items={items}/>;
};

const getConnectionStats = () : JSX.Element => {
const items = [
{
header: "Connects",
body: status.stats.connection.connects.toString()
},
{
header: "Disconnects",
body: status.stats.connection.disconnects.toString()
},
{
header: "Reconnects",
body: status.stats.connection.reconnects.toString()
},
{
header: "Errors",
body: status.stats.connection.errors.toString()
},
];

return <TextInformationGrid items={items}/>;
};


return (
<>
<Grid container alignItems="center" direction="column" style={{paddingBottom:"1rem"}}>
<Grid item style={{marginTop:"1rem"}}>
{getIconForState()}
</Grid>
<Grid
item
sx={{
maxWidth: "100% !important", //Why, MUI? Why?
wordWrap: "break-word",
textAlign: "center",
userSelect: "none"
}}
>
{getContentForState()}
</Grid>
<Grid
item
container
direction="row"
style={{marginTop: "1rem"}}
>
<Grid
item
style={{flexGrow: 1}}
p={1}
>
<Card
sx={{boxShadow: 3}}
>
<CardContent>
<Typography variant="h6" gutterBottom>
Message Statistics
</Typography>
<Divider/>
{getMessageStats()}
</CardContent>
</Card>
</Grid>
<Grid
item
style={{flexGrow: 1}}
p={1}
>
<Card
sx={{boxShadow: 3}}
>
<CardContent>
<Typography variant="h6" gutterBottom>
Connection Statistics
</Typography>
<Divider/>
{getConnectionStats()}
</CardContent>
</Card>
</Grid>
</Grid>
</Grid>
</>
);
};


const GroupBox = (props: { title: string, children: React.ReactNode, checked?: boolean, disabled?: boolean, onChange?: ((event: React.ChangeEvent<HTMLInputElement>) => void) }): JSX.Element => {
Expand Down Expand Up @@ -185,6 +357,12 @@ const MQTTConnectivity = (): JSX.Element => {
isError: mqttConfigurationError,
} = useMQTTConfigurationQuery();

const {
data: mqttStatus,
isLoading: mqttStatusLoading,
isError: mqttStatusError
} = useMQTTStatusQuery();

const {
data: mqttProperties,
isLoading: mqttPropertiesLoading,
Expand Down Expand Up @@ -243,6 +421,12 @@ const MQTTConnectivity = (): JSX.Element => {
</Grid>
</Grid>
<Divider sx={{mt: 1}}/>
<MQTTStatusComponent
status={mqttStatus}
statusLoading={mqttStatusLoading}
statusError={mqttStatusError}
/>
<Divider sx={{mt: 1}} style={{marginBottom: "1rem"}}/>

<FormControlLabel control={<Checkbox checked={mqttConfiguration.enabled} onChange={e => {
modifyMQTTConfig(e.target.checked, ["enabled"]);
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/settings/connectivity/NTPConnectivity.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ const NTPClientStateComponent : React.FunctionComponent<{ state: NTPClientState
}

if (stateError) {
return <Typography color="error">Error loading Updater state</Typography>;
return <Typography color="error">Error loading NTPClient state</Typography>;
}

const getIconForState = () : JSX.Element => {
Expand Down
12 changes: 12 additions & 0 deletions frontend/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,18 @@ export function convertSecondsToHumans(seconds: number, showSeconds = true, show
return humanReadableTimespan.trim();
}

export function convertBytesToHumans(bytes: number): string {
if (bytes >= 1024*1024*1024) {
return `${(((bytes/1024)/1024)/1024).toFixed(2)} GiB`;
} else if (bytes >= 1024*1024) {
return `${((bytes/1024)/1024).toFixed(2)} MiB`;
} else if (bytes >= 1024) {
return `${(bytes/1024).toFixed(2)} KiB`;
} else {
return `${bytes} bytes`;
}
}

// Adapted from https://gist.github.com/erikvullings/ada7af09925082cbb89f40ed962d475e
export const deepCopy = <T>(target: T): T => {
if (target === null) {
Expand Down
Loading

0 comments on commit c54ebc8

Please sign in to comment.