From bb8f3f754e57f8d7ef0284ff25e1d735a467c2de Mon Sep 17 00:00:00 2001 From: Ash Keel Date: Thu, 24 Nov 2022 01:54:56 +0100 Subject: [PATCH] feat: rework loggers and pass data to the UI --- .gitignore | 3 +- app.go | 27 +++++- frontend/src/locale/en/translation.json | 9 +- frontend/src/store/index.ts | 2 + frontend/src/store/logging/reducer.ts | 53 ++++++++++++ frontend/src/ui/App.tsx | 94 +++++++++++++------- frontend/src/ui/pages/AuthDialog.tsx | 6 +- frontend/src/ui/pages/ServerSettings.tsx | 3 +- frontend/src/ui/pages/TwitchSettings.tsx | 17 ++-- frontend/wailsjs/go/main/App.d.ts | 3 + frontend/wailsjs/go/main/App.js | 4 + frontend/wailsjs/go/models.ts | 25 ++++++ go.mod | 1 + go.sum | 3 + logging.go | 104 +++++++++++++++++++++++ main.go | 34 ++------ 16 files changed, 310 insertions(+), 78 deletions(-) create mode 100644 frontend/src/store/logging/reducer.ts create mode 100644 logging.go diff --git a/.gitignore b/.gitignore index 981225e..e30dd0c 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,5 @@ .vscode .idea strimertul_* -build/bin \ No newline at end of file +build/bin +*.log \ No newline at end of file diff --git a/app.go b/app.go index 55f6857..9c26bd0 100644 --- a/app.go +++ b/app.go @@ -4,14 +4,15 @@ import ( "context" "strconv" - "github.com/nicklaw5/helix/v2" - "github.com/strimertul/strimertul/modules" "github.com/strimertul/strimertul/modules/database" "github.com/strimertul/strimertul/modules/http" "github.com/strimertul/strimertul/modules/twitch" + "git.sr.ht/~hamcha/containers" + "github.com/nicklaw5/helix/v2" "github.com/urfave/cli/v2" + "github.com/wailsapp/wails/v2/pkg/runtime" "go.uber.org/zap" ) @@ -21,12 +22,14 @@ type App struct { cliParams *cli.Context driver DatabaseDriver manager *modules.Manager + ready *containers.RWSync[bool] } // NewApp creates a new App application struct func NewApp(cliParams *cli.Context) *App { return &App{ cliParams: cliParams, + ready: containers.NewRWSync(false), } } @@ -84,8 +87,19 @@ func (a *App) startup(ctx context.Context) { } }() + a.ready.Set(true) + runtime.EventsEmit(ctx, "ready", true) + logger.Info("app is ready") + + // Start redirecting logs to UI + go func() { + for entry := range incomingLogs { + runtime.EventsEmit(ctx, "log-event", entry) + } + }() + // Run HTTP server - go failOnError(httpServer.Listen(), "HTTP server stopped") + failOnError(httpServer.Listen(), "HTTP server stopped") } func (a *App) stop(context.Context) { @@ -101,6 +115,9 @@ func (a *App) AuthenticateKVClient(id string) { } func (a *App) IsServerReady() bool { + if !a.ready.Get() { + return false + } return a.manager.Modules[modules.ModuleHTTP].Status().Working } @@ -121,3 +138,7 @@ func (a *App) GetTwitchAuthURL() string { func (a *App) GetTwitchLoggedUser() (helix.User, error) { return a.manager.Modules[modules.ModuleTwitch].(*twitch.Client).GetLoggedUser() } + +func (a *App) GetLastLogs() []LogEntry { + return lastLogs +} diff --git a/frontend/src/locale/en/translation.json b/frontend/src/locale/en/translation.json index 0952f86..fdcf21a 100644 --- a/frontend/src/locale/en/translation.json +++ b/frontend/src/locale/en/translation.json @@ -36,7 +36,7 @@ "bind": "Webserver Bind", "kilovolt-password": "Kilovolt password", "kilovolt-placeholder": "Leave empty to disable authentication", - "bind-help": "Every application that uses strimertul will need to be updated, your browser will be redirected to the new URL automatically.", + "bind-help": "Every application that uses {{APPNAME}} will need to be updated!", "static-path": "Static assets (leave empty to disable)", "static-placeholder": "Absolute path to static assets", "static-help": "Will be served at the following URL: {{url}}", @@ -82,7 +82,7 @@ }, "sim-events": "Send test event", "auth-button": "Authenticate with Twitch", - "auth-message": "Click the following button to authenticate strimertul with your Twitch account:", + "auth-message": "Click the following button to authenticate {{APPNAME}} with your Twitch account:", "current-status": "Current status" } }, @@ -130,7 +130,7 @@ }, "auth": { "title": "Authentication required", - "desc": "The installation of strimertül you are trying to reach is protected with a password. Please write the password below to access the control panel.", + "desc": "The installation of {{APPNAME}} you are trying to reach is protected with a password. Please write the password below to access the control panel.", "no-pwd-note": " If the database has no password (for example, it was recently changed from having one to none), leave the field empty.", "password": "Password", "submit": "Authenticate" @@ -284,6 +284,7 @@ "wip": { "header": "WIP - Page under development", "text": "This page is still under construction, apologies for the lackluster view :(" - } + }, + "loading": "{{APPNAME}} is starting up, please wait!" } } diff --git a/frontend/src/store/index.ts b/frontend/src/store/index.ts index fe8a7e9..9c9c7f2 100644 --- a/frontend/src/store/index.ts +++ b/frontend/src/store/index.ts @@ -2,10 +2,12 @@ import { configureStore } from '@reduxjs/toolkit'; import { useDispatch } from 'react-redux'; import thunkMiddleware from 'redux-thunk'; import apiReducer from './api/reducer'; +import loggingReducer from './logging/reducer'; const store = configureStore({ reducer: { api: apiReducer.reducer, + logging: loggingReducer.reducer, }, middleware: (getDefaultMiddleware) => getDefaultMiddleware({ diff --git a/frontend/src/store/logging/reducer.ts b/frontend/src/store/logging/reducer.ts new file mode 100644 index 0000000..dfadd42 --- /dev/null +++ b/frontend/src/store/logging/reducer.ts @@ -0,0 +1,53 @@ +/* eslint-disable no-param-reassign */ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { main } from '@wailsapp/go/models'; + +interface ProcessedLogEntry { + time: Date; + caller: string; + level: string; + message: string; + data: object; +} + +function processEntry({ + time, + caller, + level, + message, + data, +}: main.LogEntry): ProcessedLogEntry { + return { + time: new Date(time), + caller, + level, + message, + data: JSON.parse(data) as object, + }; +} + +interface LoggingState { + messages: ProcessedLogEntry[]; +} + +const initialState: LoggingState = { + messages: [], +}; + +const loggingReducer = createSlice({ + name: 'logging', + initialState, + reducers: { + loadedLogData(state, { payload }: PayloadAction) { + state.messages = payload.map(processEntry); + }, + receivedEvent(state, { payload }: PayloadAction) { + state.messages = [...state.messages, processEntry(payload)]; + }, + clearedEvents(state) { + state.messages = []; + }, + }, +}); + +export default loggingReducer; diff --git a/frontend/src/ui/App.tsx b/frontend/src/ui/App.tsx index ba9ecc9..2be610d 100644 --- a/frontend/src/ui/App.tsx +++ b/frontend/src/ui/App.tsx @@ -1,5 +1,3 @@ -import React, { useEffect } from 'react'; -import { useSelector } from 'react-redux'; import { ChatBubbleIcon, DashboardIcon, @@ -10,34 +8,45 @@ import { TableIcon, TimerIcon, } from '@radix-ui/react-icons'; +import { EventsOff, EventsOn } from '@wailsapp/runtime/runtime'; +import { t } from 'i18next'; +import React, { useEffect, useState } from 'react'; +import { useSelector } from 'react-redux'; import { Route, Routes } from 'react-router-dom'; import { ToastContainer } from 'react-toastify'; -import { GetKilovoltBind } from '@wailsapp/go/main/App'; -import { delay } from 'src/lib/time-utils'; -import Dashboard from './pages/Dashboard'; -import Sidebar, { RouteSection } from './components/Sidebar'; -import ServerSettingsPage from './pages/ServerSettings'; +import { + GetKilovoltBind, + GetLastLogs, + IsServerReady, +} from '@wailsapp/go/main/App'; +import { main } from '@wailsapp/go/models'; + import { RootState, useAppDispatch } from '../store'; import { createWSClient, useAuthBypass } from '../store/api/reducer'; import { ConnectionStatus } from '../store/api/types'; -import { styled } from './theme'; +import loggingReducer from '../store/logging/reducer'; +import Sidebar, { RouteSection } from './components/Sidebar'; +import AuthDialog from './pages/AuthDialog'; +import TwitchBotCommandsPage from './pages/BotCommands'; +import TwitchBotTimersPage from './pages/BotTimers'; +import ChatAlertsPage from './pages/ChatAlerts'; +import Dashboard from './pages/Dashboard'; +import DebugPage from './pages/Debug'; +import LoyaltyConfigPage from './pages/LoyaltyConfig'; +import LoyaltyQueuePage from './pages/LoyaltyQueue'; +import LoyaltyRewardsPage from './pages/LoyaltyRewards'; +import ServerSettingsPage from './pages/ServerSettings'; +import StrimertulPage from './pages/Strimertul'; +import TwitchSettingsPage from './pages/TwitchSettings'; +import { APPNAME, styled } from './theme'; // @ts-expect-error Asset import import spinner from '../assets/icon-loading.svg'; -import TwitchSettingsPage from './pages/TwitchSettings'; -import TwitchBotCommandsPage from './pages/BotCommands'; -import TwitchBotTimersPage from './pages/BotTimers'; -import AuthDialog from './pages/AuthDialog'; -import ChatAlertsPage from './pages/ChatAlerts'; -import LoyaltyConfigPage from './pages/LoyaltyConfig'; -import LoyaltyQueuePage from './pages/LoyaltyQueue'; -import StrimertulPage from './pages/Strimertul'; -import DebugPage from './pages/Debug'; -import LoyaltyRewardsPage from './pages/LoyaltyRewards'; const LoadingDiv = styled('div', { display: 'flex', + flexDirection: 'column', justifyContent: 'center', alignItems: 'center', minHeight: '100vh', @@ -47,10 +56,15 @@ const Spinner = styled('img', { maxWidth: '100px', }); -function Loading() { +interface LoadingProps { + message: string; +} + +function Loading({ message }: React.PropsWithChildren) { return ( +

{message}

); } @@ -143,6 +157,7 @@ const PageWrapper = styled('div', { }); export default function App(): JSX.Element { + const [ready, setReady] = useState(false); const client = useSelector((state: RootState) => state.api.client); const connected = useSelector( (state: RootState) => state.api.connectionStatus, @@ -150,16 +165,7 @@ export default function App(): JSX.Element { const dispatch = useAppDispatch(); const connectToKV = async () => { - let address = ''; - while (address === '') { - // eslint-disable-next-line no-await-in-loop - address = await GetKilovoltBind(); - if (address === '') { - // Server not ready yet, wait a second - // eslint-disable-next-line no-await-in-loop - await delay(1000); - } - } + const address = await GetKilovoltBind(); await dispatch( createWSClient({ address: `ws://${address}/ws`, @@ -167,17 +173,43 @@ export default function App(): JSX.Element { ); }; + // Get application logs useEffect(() => { + void GetLastLogs().then((logs) => { + dispatch(loggingReducer.actions.loadedLogData(logs)); + }); + EventsOn('log-event', (event: main.LogEntry) => { + dispatch(loggingReducer.actions.receivedEvent(event)); + }); + return () => { + EventsOff('log-event'); + }; + }, []); + + useEffect(() => { + void IsServerReady().then(setReady); + EventsOn('ready', (newValue: boolean) => { + setReady(newValue); + }); + return () => { + EventsOff('ready'); + }; + }, []); + + useEffect(() => { + if (!ready) { + return; + } if (!client) { void connectToKV(); } if (connected === ConnectionStatus.AuthenticationNeeded) { void dispatch(useAuthBypass()); } - }); + }, [ready, connected]); if (connected === ConnectionStatus.NotConnected) { - return ; + return ; } if (connected === ConnectionStatus.AuthenticationNeeded) { diff --git a/frontend/src/ui/pages/AuthDialog.tsx b/frontend/src/ui/pages/AuthDialog.tsx index e55bb72..730339f 100644 --- a/frontend/src/ui/pages/AuthDialog.tsx +++ b/frontend/src/ui/pages/AuthDialog.tsx @@ -1,7 +1,7 @@ import { styled } from '@stitches/react'; import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { Button, InputBox, TextBlock } from '../theme'; +import { APPNAME, Button, InputBox, TextBlock } from '../theme'; const AuthWrapper = styled('div', { alignItems: 'center', @@ -59,7 +59,9 @@ export default function AuthDialog(): React.ReactElement { > {t('pages.auth.title')} - {t('pages.auth.desc')} + + {t('pages.auth.desc', { APPNAME })} + {t('pages.auth.no-pwd-note')} diff --git a/frontend/src/ui/pages/ServerSettings.tsx b/frontend/src/ui/pages/ServerSettings.tsx index 070b373..27d57ac 100644 --- a/frontend/src/ui/pages/ServerSettings.tsx +++ b/frontend/src/ui/pages/ServerSettings.tsx @@ -5,6 +5,7 @@ import { useAppDispatch } from '../../store'; import apiReducer, { modules } from '../../store/api/reducer'; import SaveButton from '../components/utils/SaveButton'; import { + APPNAME, Field, FieldNote, InputBox, @@ -53,7 +54,7 @@ export default function ServerSettingsPage(): React.ReactElement { ) } /> - {t('pages.http.bind-help')} + {t('pages.http.bind-help', { APPNAME })}