Skip to content

Commit

Permalink
Add Healtcheck, OTP improvements, and Event Timeline view #patch (#370)
Browse files Browse the repository at this point in the history
## v0.3.24
**Improvements**
- Service healthcheck 
- Events view with multiple event types
- Timeline view for events
- WiFi AP Widget now shows frequency
**Fixes**
- OTP Code required for backups, feature setting
  • Loading branch information
lts-rad committed Sep 13, 2024
1 parent fb8030c commit 0eb2e41
Show file tree
Hide file tree
Showing 21 changed files with 908 additions and 410 deletions.
10 changes: 10 additions & 0 deletions RELEASE-NOTES.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
# Secure Programmable Router (SPR) Release Notes

## v0.3.24
**Improvements**
- Service healthcheck
- Events view with multiple event types
- Timeline view for events
- WiFi AP Widget now shows frequency
**Fixes**
- OTP Code required for backups, feature setting

## v0.3.23
**Fixes**
- Various fixes for Setup
- Show up to date subnet info when restoring from backup
- Devices list will now only show authorized as green, and associated only as Yellow
Expand Down
5 changes: 3 additions & 2 deletions api/code/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -2905,7 +2905,7 @@ func main() {
external_router_authenticated.HandleFunc("/restart", restart).Methods("PUT")
external_router_authenticated.HandleFunc("/dockerPS", dockerPS).Methods("GET")
external_router_authenticated.HandleFunc("/backup", doConfigsBackup).Methods("PUT", "OPTIONS")
external_router_authenticated.HandleFunc("/backup/{name}", getConfigsBackup).Methods("GET", "DELETE", "OPTIONS")
external_router_authenticated.HandleFunc("/backup/{name}", applyJwtOtpCheck(getConfigsBackup)).Methods("GET", "DELETE", "OPTIONS")
external_router_authenticated.HandleFunc("/backup", getConfigsBackup).Methods("GET", "OPTIONS")
external_router_authenticated.HandleFunc("/info/{name}", getInfo).Methods("GET", "OPTIONS", "PUT")
external_router_authenticated.HandleFunc("/subnetConfig", getSetDhcpConfig).Methods("GET", "PUT", "OPTIONS")
Expand All @@ -2915,7 +2915,8 @@ func main() {
external_router_authenticated.HandleFunc("/multicastSettings", multicastSettings).Methods("GET", "PUT")

//updates, version, feature info
external_router_authenticated.HandleFunc("/release", releaseInfo).Methods("GET", "PUT", "DELETE", "OPTIONS")
external_router_authenticated.HandleFunc("/release", releaseInfo).Methods("GET", "OPTIONS")
external_router_authenticated.HandleFunc("/releaseSet", applyJwtOtpCheck(releaseInfo)).Methods("PUT", "DELETE", "OPTIONS")
external_router_authenticated.HandleFunc("/releaseChannels", releaseChannels).Methods("GET", "OPTIONS")
external_router_authenticated.HandleFunc("/releasesAvailable", releasesAvailable).Methods("GET", "OPTIONS")
external_router_authenticated.HandleFunc("/update", update).Methods("PUT", "OPTIONS")
Expand Down
1 change: 1 addition & 0 deletions frontend/src/AppContext.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export const AppContext = createContext({
isWifiDisabled: false,
isPlusDisabled: true,
isMeshNode: false,
isFeaturesInitialized: false,
features: [],
devices: [],
getDevices: () => {},
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/api/API.js
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ class API {
async request(method = 'GET', url, body) {
// if forced to not return data
let skipReturnValue = method == 'DELETE'

return this.fetch(method, url, body)
.then((response) => {
if (!response.ok) {
Expand All @@ -229,6 +229,8 @@ class API {
return response.text()
} else if (contentType.includes('application/x-x509-ca-cert')) {
return response.text()
} else if (contentType.startsWith('application/x-gtar-compressed')) {
return response.blob()
}

return Promise.reject({ message: 'unknown Content-Type' })
Expand Down
8 changes: 8 additions & 0 deletions frontend/src/components/Alerts/EventTimelineChart.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import React, { useContext } from 'react'

//empty for iOS, default
const EventTimelineChart = (props) => {
return <></>
}

export default EventTimelineChart
167 changes: 167 additions & 0 deletions frontend/src/components/Alerts/EventTimelineChart.web.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import React, {useState, useEffect} from 'react';
import { Bar } from 'react-chartjs-2';
import { Chart as ChartJS } from 'chart.js/auto'
import chroma from 'chroma-js'

import {
Button,
ButtonIcon,
Center,
FlatList,
Heading,
HStack,
ScrollView,
Slider,
SliderTrack,
SliderFilledTrack,
SliderThumb,
Text,
Tooltip,
TooltipContent,
View,
VStack,
SettingsIcon,
ChevronDownIcon,
ChevronUpIcon,
} from '@gluestack-ui/themed'

const TimelineChart = ({ topics, data, onBarClick }) => {

const [chartData, setChartData] = useState(null);
const [chartMinTime, setChartMinTime] = useState(null);
const [chartMaxTime, setChartMaxTime] = useState(null);

const [minTime, setMinTime] = useState(null);
const [maxTime, setMaxTime] = useState(null);

useEffect(() => {
const processData = () => {
let eventTimeByTopic = {};
for (let topic of topics) {
eventTimeByTopic[topic] = [];
}
let maxTime = new Date('2023-01-12T00:00:00Z');
let minTime = new Date();

for (let item of data) {
if (!eventTimeByTopic[item.selected]) {
eventTimeByTopic[item.selected] = [];
}
let dt = new Date(item.time);
if (dt < minTime) minTime = dt;
if (dt > maxTime) maxTime = dt;
eventTimeByTopic[item.selected].push({
dt: dt,
data: item
});
}

setMinTime(minTime);
setMaxTime(maxTime);
//setStartTimeRange(maxTime - minTime)

let width = (maxTime.getTime() - minTime.getTime()) / 100;
let cmt = new Date(maxTime.getTime() + width * 2);
let cmmt = new Date(minTime.getTime() - width * 2);
setChartMinTime(cmmt)
setChartMaxTime(cmt)

let colors = chroma
.scale(['seagreen', 'teal', 'lightskyblue', 'royalblue', 'navy'])
.mode('lch')
.colors(Object.keys(eventTimeByTopic).length);

const datasets = topics.map((topic, index) => {
let c = chroma(colors[index]).alpha(0.85).css();
let ban = ['selected','bucket','time']
return {
label: topic + "",
backgroundColor: `rgba(75, 192, ${192 + index * 20}, 0.6)`,
borderColor: `rgba(75, 192, ${192 + index * 20}, 1)`,
borderWidth: 1,
pointStyle: 'rect',
data: eventTimeByTopic[topic].map(event => ({
y: topic,
x: [event.dt.getTime() - minTime.getTime(),
(new Date(event.dt.getTime() + width)).getTime() - minTime.getTime()],
customLabel: Object.entries(event.data).filter(([k]) => !ban.includes(k)).map(([key, value]) => `${key} : ${JSON.stringify(value)}`)
}))
};
});

setChartData({
labels: topics,
datasets: datasets,
});
};

processData();
}, [topics, data]);


const max_end = chartMaxTime ? (chartMaxTime.getTime() - chartMinTime.getTime()) : 0

const options = {
legend: {
show: false,
},
indexAxis: 'y',
scales: {
x: {
min: 0,
max: max_end,
ticks: {
callback: function(value) {
const date = new Date(minTime.getTime() + value);
return date.toLocaleDateString();
}
},
title: {
display: true,
text: 'Time'
}
},
y: {
type: 'category',
labels: topics,
stacked: true,
}
},
plugins: {
tooltip: {
callbacks: {
label: function(context, data) {
const date = new Date(minTime.getTime() + context.parsed.x);
const dataset = chartData.datasets[context.datasetIndex];
const datapoint = dataset.data[context.dataIndex];
return [`${date.toLocaleString()}`, ...datapoint.customLabel];
}
}
}
},
onClick: (event, elements) => {
if (elements.length > 0) {
const index = elements[0].index;
const datasetIndex = elements[0].datasetIndex;
const clickedTopic = chartData.labels[datasetIndex];
const clickedTime = new Date(minTime.getTime() + chartData.datasets[datasetIndex].data[index].x);
if (onBarClick) {
onBarClick(clickedTopic, clickedTime);
}
}
},
responsive: true,
maintainAspectRatio: false,
};


return (
<ScrollView>
{chartData && (
<Bar data={chartData} options={options} />
)}
</ScrollView>
);
};

export default TimelineChart;
15 changes: 4 additions & 11 deletions frontend/src/components/Auth/OTPValidate.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import React, { useEffect, useState } from 'react'
import PropTypes from 'prop-types'
import { useNavigate } from 'react-router-dom'

import { authAPI, setJWTOTPHeader } from 'api'

Expand All @@ -18,8 +17,7 @@ import {
VStack
} from '@gluestack-ui/themed'

const OTPValidate = ({ onSuccess, ...props }) => {
const navigate = useNavigate()
const OTPValidate = ({ onSuccess, onSetup, ...props }) => {
const [code, setCode] = useState('')
const [status, setStatus] = useState('')
const [errors, setErrors] = useState({})
Expand Down Expand Up @@ -59,13 +57,7 @@ const OTPValidate = ({ onSuccess, ...props }) => {
return (
<VStack space="md">
<Text>Need to setup OTP auth for this feature</Text>
<Button
variant="outline"
onPress={() => {
onSuccess() // only to close the modal
navigate('/admin/auth')
}}
>
<Button variant="outline" onPress={onSetup}>
<ButtonText>Setup OTP</ButtonText>
</Button>
</VStack>
Expand Down Expand Up @@ -101,7 +93,8 @@ const OTPValidate = ({ onSuccess, ...props }) => {
}

OTPValidate.propTypes = {
onSuccess: PropTypes.func.isRequired
onSuccess: PropTypes.func.isRequired,
onSetup: PropTypes.func.isRequired
}

export default OTPValidate
4 changes: 2 additions & 2 deletions frontend/src/components/DNS/DNSLogHistoryList.js
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ const ListItem = ({

<HStack>
<Text color="$muted500" onPress={() => triggerAlert(item)}>
{item.FirstAnswer || '0.0.0.0'}
{item.FirstAnswer || ''}
</Text>
</HStack>
</VStack>
Expand Down Expand Up @@ -328,7 +328,7 @@ const DNSLogHistoryList = (props) => {
return match
})
} else {
listFiltered = list
listFiltered = list.filter(i => i.Type != "NODATA" || i.FirstAnswer)
}

if (filterText.length) {
Expand Down
55 changes: 34 additions & 21 deletions frontend/src/components/Dashboard/HealthCheck.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,30 +18,44 @@ export const HealthCheck = () => {
const colorMode = useColorMode()
const alertContext = useContext(AlertContext)
const appContext = useContext(AppContext)
const [criticalToCheck, setCriticalToCheck] = useState([])
const [processed, setIsProcessed] = useState(false)

useEffect(() => {

let criticalToCheck = appContext.isMeshNode ? [] : ['dns', 'dhcp']
if (appContext.isFeaturesInitialized === true) {
let toCheck = appContext.isMeshNode ? [] : ['dns', 'dhcp']
if (!appContext.isWifiDisabled) {
toCheck.push('wifid')
}

if (!appContext.isWifiDisabled) {
criticalToCheck.push('wifid')
}
let counter = 0

const getServicesStatus = () => {
criticalToCheck.forEach(s => {
api.get(`/dockerPS?service=${s}`)
.then(() => {
setCriticalStatus(prev => ({ ...prev, [s]: true }))
})
.catch(() => {
setCriticalStatus(prev => ({ ...prev, [s]: false }))
alertContext.warning(s + " service is not running")
})
})
}
const complete = () => {
counter += 1
if (counter == toCheck.length) {
setIsProcessed(true)
}
}

useEffect(() => {
getServicesStatus()
}, [])
toCheck.forEach((s, idx) => {
api.get(`/dockerPS?service=${s}`)
.then(() => {
setCriticalStatus(prev => ({ ...prev, [s]: true }))
complete()
counter += 1
})
.catch(() => {
setCriticalStatus(prev => ({ ...prev, [s]: false }))
alertContext.warning(s + " service is not running")
complete()
})
})


setCriticalToCheck(toCheck)
}
}, [appContext.isMeshNode, appContext.isFeaturesInitialized, appContext.isWifiDisabled])

const getServiceIcon = (service) => {
switch (service) {
Expand All @@ -67,7 +81,7 @@ export const HealthCheck = () => {
</Heading>
<Divider my="$2" />
<VStack space="md" alignItems="center">
{criticalToCheck.map((service) => (
{processed && criticalToCheck.map((service) => (
<HStack key={service} space="md" alignItems="center">
{/*<Icon
as={getServiceIcon(service)}
Expand All @@ -85,5 +99,4 @@ export const HealthCheck = () => {
)
}

HealthCheck.contextType = AlertContext
export default HealthCheck
2 changes: 1 addition & 1 deletion frontend/src/components/Dashboard/Intro.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ const IntroWidget = ({
onPress={() => navigate('/admin/dnsBlock')}
>
<ButtonIcon mr="$2" as={BanIcon} />
<ButtonText>DNS Blocklists</ButtonText>
<ButtonText>DNS Ad Blocking</ButtonText>
</Button>
<Button
size="xs"
Expand Down
Loading

0 comments on commit 0eb2e41

Please sign in to comment.