From 775e64034b59a3680237312add9e70c8769fa8fd Mon Sep 17 00:00:00 2001 From: Zack Date: Tue, 19 Sep 2023 12:52:39 +0200 Subject: [PATCH] Added refresh, a notification, fixes and stale_bot --- .eslintrc.json | 3 + .github/workflows/stale_bot.yml | 22 ++++ .gitignore | 3 +- electron/bridge.ts | 1 - electron/main.handlers.ts | 5 +- electron/utils/shutdown.ts | 5 + src/Pages/Home.tsx | 128 ++++++++++++++++++++---- src/Pages/Settings.tsx | 36 ++++++- src/components/AreaComponent.tsx | 161 +++++++++++++++--------------- src/interfaces/IAreaComponent.tsx | 3 +- src/interfaces/IAreaInfo.tsx | 8 +- src/interfaces/ISettings.tsx | 1 + 12 files changed, 264 insertions(+), 112 deletions(-) create mode 100644 .github/workflows/stale_bot.yml diff --git a/.eslintrc.json b/.eslintrc.json index 1c3233d..fc2e930 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -24,5 +24,8 @@ ], "rules": { "react/prop-types": "off" + }, + "globals": { + "NodeJS": true } } diff --git a/.github/workflows/stale_bot.yml b/.github/workflows/stale_bot.yml new file mode 100644 index 0000000..c6a7bed --- /dev/null +++ b/.github/workflows/stale_bot.yml @@ -0,0 +1,22 @@ +name: Close inactive issues +on: + schedule: + - cron: "0 0 * * *" + +jobs: + close-issues: + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + steps: + - uses: actions/stale@v5 + with: + days-before-issue-stale: 30 + days-before-issue-close: 14 + stale-issue-label: "stale" + stale-issue-message: "This issue is stale because it has been open for 30 days with no activity." + close-issue-message: "This issue was closed because it has been inactive for 14 days since being marked as stale." + days-before-pr-stale: -1 + days-before-pr-close: -1 + repo-token: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index a8a4fba..7706a07 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ packages out yarn-error.log .yarn* -*.log \ No newline at end of file +*.log +.vscode/launch.json diff --git a/electron/bridge.ts b/electron/bridge.ts index 35b6180..60b868f 100644 --- a/electron/bridge.ts +++ b/electron/bridge.ts @@ -1,6 +1,5 @@ import { contextBridge, ipcRenderer } from 'electron'; import ISettings from '../src/interfaces/ISettings'; -import { Dayjs } from 'dayjs'; const api = { getSettings: async () => { diff --git a/electron/main.handlers.ts b/electron/main.handlers.ts index 855c0f2..84e750d 100644 --- a/electron/main.handlers.ts +++ b/electron/main.handlers.ts @@ -6,9 +6,8 @@ import ISettings from '../src/interfaces/ISettings' import { CronJob } from 'cron' import { shutdown } from './utils/shutdown' import logger from './utils/logger' -import { Dayjs } from 'dayjs' -const store = new Store() +const store = new Store(); export const registerHandlers = (ipcMain: IpcMain, tray: Tray | null) => { let job: CronJob @@ -29,7 +28,7 @@ export const registerHandlers = (ipcMain: IpcMain, tray: Tray | null) => { ipcMain.handle('saveSettings', (event: any, settings: ISettings) => { store.set('settings', settings); app.setLoginItemSettings({ - openAtLogin: settings.runAtStartup + openAtLogin: settings.runAtStartup, }); }); diff --git a/electron/utils/shutdown.ts b/electron/utils/shutdown.ts index 5978af7..4e583d1 100644 --- a/electron/utils/shutdown.ts +++ b/electron/utils/shutdown.ts @@ -1,4 +1,5 @@ import cp from 'child_process'; +import { Notification } from 'electron'; export function shutdown() { const cmdarguments = ['shutdown']; @@ -15,5 +16,9 @@ export function shutdown() { } const executeCmd = (cmd: string[]) => { + new Notification({ + title: 'ShedShield shutdown', + body: 'ShedShield will now shut down your PC' + }).show(); cp.exec(cmd.join(' ')); } \ No newline at end of file diff --git a/src/Pages/Home.tsx b/src/Pages/Home.tsx index c06abe3..44eee7a 100644 --- a/src/Pages/Home.tsx +++ b/src/Pages/Home.tsx @@ -1,24 +1,85 @@ import { FC, useEffect, useState } from 'react'; -import { Settings } from '@mui/icons-material'; -import { Box, Button, IconButton, Stack, Typography } from '@mui/material'; +import { Settings, Refresh } from '@mui/icons-material'; +import { Box, Button, IconButton, Skeleton, Stack, Tooltip, Typography } from '@mui/material'; import { Link } from 'react-router-dom'; import IHomePage from '../interfaces/IHomePage'; import AreaComponent from '../components/AreaComponent'; import dayjs, { Dayjs } from 'dayjs'; import ISettings from '../interfaces/ISettings'; +import IAreaInfo from '../interfaces/IAreaInfo'; const HomePage: FC = () => { const [settings, setSettings] = useState(); const [firstTimeSetup, setFirstTimeSetup] = useState(false); + const [areaData, setAreaData] = useState([]); + const [refreshing, setRefreshing] = useState(false); + const intervals: NodeJS.Timeout[] = []; + + const updateInfo = (id: string): Promise => { + const updatedArea: IAreaInfo = { + id, + events: null, + info: null, + schedule: null, + error: "", + }; + + return window.Main.getAreaInfo(id) + .then((response: IAreaInfo) => { + updatedArea.events = response.events; + updatedArea.info = response.info; + updatedArea.schedule = response.schedule; + return updatedArea; + }) + .catch((error: Error) => { + updatedArea.error = error.message; + window.Main.error(error); + return updatedArea; + }); + }; + + const updateAreaInfo = (id: string, blah?: string ) => { + updateInfo(id).then((updatedArea) => { + + setAreaData(prevAreaData => { + const index = prevAreaData.findIndex(arrArea => arrArea.id === id); + + if (index > -1) { + const newData = [...prevAreaData]; + newData[index] = updatedArea; + return newData; + } else { + return [...prevAreaData, updatedArea]; + } + }); + }); + } useEffect(() => { window.Main.getSettings() .then((res: ISettings) => { setSettings(res); setFirstTimeSetup(!res || res?.espAreas.length <= 0 || !res?.apiKey) + res?.espAreas.forEach(area => { + updateAreaInfo(area.id); + }); }) .catch((error: Error) => window.Main.error(error)); }, []); + + useEffect(() => { + areaData?.forEach((area, index) => { + const intervalId = setInterval(() => { + updateAreaInfo(area.id); + }, (settings?.updates || 60) * 60000); + intervals.push(intervalId); + }); + + return () => { + intervals?.forEach((intervalId) => clearInterval(intervalId)); + } + }, [areaData]); + const [currentEvent, setCurrentEvent] = useState(); const handleNextEvent = (date: Dayjs) => { @@ -28,9 +89,42 @@ const HomePage: FC = () => { } }; + const renderNextEvent = () => { + if (currentEvent) { + return ( + + {`Shutting down at ${currentEvent?.format('HH:mm, ddd D MMM')}.`} + + ) + } else if (!refreshing && areaData.length > 0) { + return ( + + No upcoming loadshedding for today! 🎉 + + ) + } else { + return ( + + Loading... + + ) + } + }; + return ( + Get latest data from ESP
P.S. It uses quota}> + { + setRefreshing(true); + settings?.espAreas.forEach(area => { + updateAreaInfo(area.id, "testing"); + }); + setRefreshing(false); + }}> + + +
@@ -73,21 +167,21 @@ const HomePage: FC = () => { )} {!firstTimeSetup && ( - {currentEvent && ( - - {`Shutting down at ${currentEvent?.format('HH:mm, ddd D MMM')}.`} - - )} - {settings?.espAreas.map(area => { - return ( - - ); - })} + {renderNextEvent()} + {(!refreshing && areaData.length > 0) ? areaData.map(area => { + return ( + + ); + }) : + + + + } )}
diff --git a/src/Pages/Settings.tsx b/src/Pages/Settings.tsx index c7b16be..b471515 100644 --- a/src/Pages/Settings.tsx +++ b/src/Pages/Settings.tsx @@ -31,6 +31,8 @@ import { Link, useNavigate } from 'react-router-dom'; import ISettingsPage from '../interfaces/ISettingsPage'; const intervals = [15, 10, 5, 2]; +const updates = [30, 60, 120, 300]; +const updateText = ['30 minutes', '1 hour', '2 hours', '3 hours']; const SettingsPage: FC = ({ themeState, setThemeState }) => { useEffect(() => { @@ -43,6 +45,7 @@ const SettingsPage: FC = ({ themeState, setThemeState }) => { const checkedAreas = settings?.espAreas.map(area => area.id); setAreas(checkedAreas); setInterval(settings?.interval || 15); + setUpdate(settings?.updates || 120); setRunAtStartup(settings?.runAtStartup); }) .catch((error: Error) => { @@ -65,6 +68,7 @@ const SettingsPage: FC = ({ themeState, setThemeState }) => { const [areasError, setAreasError] = useState(""); const [interval, setInterval] = useState(15); + const [update, setUpdate] = useState(120); const [runAtStartup, setRunAtStartup] = useState(false); @@ -136,6 +140,7 @@ const SettingsPage: FC = ({ themeState, setThemeState }) => { espAreas: checkedAreas, theme: themeState, interval, + updates: update, runAtStartup, } as ISettings) .then(() => { @@ -268,8 +273,8 @@ const SettingsPage: FC = ({ themeState, setThemeState }) => { {areasError && {areasError}}
- - Interval + + Duration - How early should your PC be switched off? + {`How long before loadshedding \n should your PC be switched off?`} + + + + Update Interval + + + How often should ShedShield check for updates? P.S. This will affect your ESP quota. diff --git a/src/components/AreaComponent.tsx b/src/components/AreaComponent.tsx index 9050a76..94cfb35 100644 --- a/src/components/AreaComponent.tsx +++ b/src/components/AreaComponent.tsx @@ -1,107 +1,102 @@ import React, { useEffect, useState } from 'react'; import IAreaComponent from '../interfaces/IAreaComponent'; -import IAreaInfo from '../interfaces/IAreaInfo'; -import { Alert, Chip, Grid, Skeleton, Stack, Typography } from '@mui/material'; +import { Alert, Chip, Grid, Stack, Typography } from '@mui/material'; import dayjs from 'dayjs'; const AreaComponent: React.FC = ({ - id, + areaInfo, interval, handleNextEvent, }) => { - const [areaInfo, setAreaInfo] = useState(null); - const [areaInfoError, setAreaInfoError] = useState(""); const [timeSlots, setTimeSlots] = useState( new Array(48).fill(false), ); + const setSlots = () => { + if (!areaInfo?.events) { + return; + } + const currentStage = +areaInfo?.events[0]?.note?.split(' ')[1] || 0; + const stageStart = dayjs(areaInfo?.events[0]?.start); + const stageEnd = dayjs(areaInfo?.events[0]?.end); + const slots = areaInfo?.schedule?.days[0]?.stages[currentStage - 1]; + const newSlots = timeSlots; + const today = new Date(); + slots?.forEach(slot => { + const [startA, endA] = slot.split('-'); + const startDateTime = dayjs( + new Date( + today.getFullYear(), + today.getMonth(), + today.getDate(), + +startA.split(':')[0], + +startA.split(':')[1] + ) + ); + const endDateTime = dayjs( + new Date( + today.getFullYear(), + today.getMonth(), + today.getDate(), + +endA.split(':')[0], + +endA.split(':')[1] + ) + ); + const start = startDateTime.hour() * 2; + const end = endDateTime.hour() === 0 ? 48 : endDateTime.hour() * 2 + 2; + if (startDateTime >= stageStart && endDateTime <= stageEnd) { + for (let i = start; i < end; i++) { + newSlots[i] = true; + } + const shutdownTime = startDateTime.subtract(interval, 'minutes'); + shutdownTime > dayjs() && handleNextEvent(shutdownTime); + } + }); + setTimeSlots(newSlots); + }; + useEffect(() => { - window.Main.getAreaInfo(id) - .then((response: IAreaInfo) => { - setAreaInfo(response); - const currentStage = +response?.events[0]?.note?.split(' ')[1] || 0; - const slots = response?.schedule?.days[0]?.stages[currentStage - 1]; - const newSlots = timeSlots; - const today = new Date(); - slots.forEach(slot => { - const [startA, endA] = slot.split('-'); - const startDateTime = dayjs( - new Date( - today.getFullYear(), - today.getMonth(), - today.getDate(), - +startA.split(':')[0], - +startA.split(':')[1], - ), - ); - const endDateTime = dayjs( - new Date( - today.getFullYear(), - today.getMonth(), - today.getDate(), - +endA.split(':')[0], - +endA.split(':')[1], - ), - ); - const start = startDateTime.hour() * 2; - const end = - endDateTime.hour() === 0 ? 48 : endDateTime.hour() * 2 + 2; - for (let i = start; i < end; i++) { - newSlots[i] = true; - } - const shutdownTime = startDateTime.subtract(interval, 'minutes'); - shutdownTime > dayjs() && handleNextEvent(shutdownTime); - }); - setTimeSlots(newSlots); - }) - .catch((error: Error) => { - setAreaInfo(null); - setAreaInfoError("Oops! Couldn't load area info. Please try again later") - window.Main.error(error); - }); - }, []); + setSlots(); + },[areaInfo]); return ( - {areaInfo ? - areaInfoError ? + {areaInfo?.error ? ( - {areaInfoError} + {areaInfo.error} ) : ( - <> - - - - {areaInfo?.info?.name} - - + + - {areaInfo?.info?.region} - + + {areaInfo?.info?.name} + + + {areaInfo?.info?.region} + + - - - - {timeSlots.map((slot, idx) => ( - - ))} + + + {timeSlots.map((slot, idx) => ( + + ))} + - - - ) : ( - - )} + + )} ); }; diff --git a/src/interfaces/IAreaComponent.tsx b/src/interfaces/IAreaComponent.tsx index 0bdbd98..f6ec757 100644 --- a/src/interfaces/IAreaComponent.tsx +++ b/src/interfaces/IAreaComponent.tsx @@ -1,7 +1,8 @@ import { Dayjs } from 'dayjs'; +import IAreaInfo from './IAreaInfo'; export default interface IAreaComponent { - id: string; + areaInfo: IAreaInfo | undefined; interval: number; handleNextEvent: (date: Dayjs) => void; } diff --git a/src/interfaces/IAreaInfo.tsx b/src/interfaces/IAreaInfo.tsx index ed433c9..64b4b6f 100644 --- a/src/interfaces/IAreaInfo.tsx +++ b/src/interfaces/IAreaInfo.tsx @@ -21,7 +21,9 @@ interface ISchedule { } export default interface IAreaInfo { - events: IEvent[]; - info: IInfo; - schedule: ISchedule; + id: string; + events: IEvent[] | null; + info: IInfo | null; + schedule: ISchedule | null; + error: string; } diff --git a/src/interfaces/ISettings.tsx b/src/interfaces/ISettings.tsx index 1df6ef5..185097c 100644 --- a/src/interfaces/ISettings.tsx +++ b/src/interfaces/ISettings.tsx @@ -5,5 +5,6 @@ export default interface ISettings { espAreas: IAreaResult[]; theme: string; interval: number; + updates: number; runAtStartup: boolean; }