From aae63e4e2c8f2f9fbfa2e5810b09426e283b82a2 Mon Sep 17 00:00:00 2001 From: Gabe Kangas Date: Fri, 27 May 2022 22:27:20 -0700 Subject: [PATCH] Display global error if server is unreachable --- web/components/layouts/Main.tsx | 8 +++++ web/components/modals/FatalErrorModal.tsx | 31 ++++++++++++++++++++ web/components/stores/ClientConfigStore.tsx | 25 +++++++++++++++- web/stories/FatalErrorStateModal.stories.tsx | 21 +++++++++++++ web/types/displayable-error.ts | 4 +++ 5 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 web/components/modals/FatalErrorModal.tsx create mode 100644 web/stories/FatalErrorStateModal.stories.tsx create mode 100644 web/types/displayable-error.ts diff --git a/web/components/layouts/Main.tsx b/web/components/layouts/Main.tsx index f36e291f7..900e2a41f 100644 --- a/web/components/layouts/Main.tsx +++ b/web/components/layouts/Main.tsx @@ -4,20 +4,28 @@ import { ClientConfigStore, isChatAvailableSelector, clientConfigStateAtom, + fatalErrorStateAtom, } from '../stores/ClientConfigStore'; import { Content, Header } from '../ui'; import { ClientConfig } from '../../interfaces/client-config.model'; +import { DisplayableError } from '../../types/displayable-error'; +import FatalErrorStateModal from '../modals/FatalErrorModal'; function Main() { const clientConfig = useRecoilValue(clientConfigStateAtom); const { name, title } = clientConfig; const isChatAvailable = useRecoilValue(isChatAvailableSelector); + const fatalError = useRecoilValue(fatalErrorStateAtom); + return ( <>
+ {fatalError && ( + + )} ); diff --git a/web/components/modals/FatalErrorModal.tsx b/web/components/modals/FatalErrorModal.tsx new file mode 100644 index 000000000..ff1c859b0 --- /dev/null +++ b/web/components/modals/FatalErrorModal.tsx @@ -0,0 +1,31 @@ +import { Modal } from 'antd'; + +interface Props { + title: string; + message: string; +} + +export default function FatalErrorStateModal(props: Props) { + const { title, message } = props; + + return ( + + +

{message}

+
+ ); +} diff --git a/web/components/stores/ClientConfigStore.tsx b/web/components/stores/ClientConfigStore.tsx index c3c825917..615b2b85e 100644 --- a/web/components/stores/ClientConfigStore.tsx +++ b/web/components/stores/ClientConfigStore.tsx @@ -24,6 +24,7 @@ import handleChatMessage from './eventhandlers/handleChatMessage'; import handleConnectedClientInfoMessage from './eventhandlers/connected-client-info-handler'; import ServerStatusService from '../../services/status-service'; import handleNameChangeEvent from './eventhandlers/handleNameChangeEvent'; +import { DisplayableError } from '../../types/displayable-error'; const SERVER_STATUS_POLL_DURATION = 5000; const ACCESS_TOKEN_KEY = 'accessToken'; @@ -76,6 +77,11 @@ export const isVideoPlayingAtom = atom({ default: false, }); +export const fatalErrorStateAtom = atom({ + key: 'fatalErrorStateAtom', + default: null, +}); + // Chat is visible if the user wishes it to be visible AND the required // chat state is set. export const isChatVisibleSelector = selector({ @@ -129,10 +135,17 @@ export function ClientConfigStore() { const [chatMessages, setChatMessages] = useRecoilState(chatMessagesAtom); const [accessToken, setAccessToken] = useRecoilState(accessTokenAtom); const setAppState = useSetRecoilState(appStateAtom); - + const setGlobalFatalErrorMessage = useSetRecoilState(fatalErrorStateAtom); const setWebsocketService = useSetRecoilState(websocketServiceAtom); + let ws: WebsocketService; + const setGlobalFatalError = (title: string, message: string) => { + setGlobalFatalErrorMessage({ + title, + message, + }); + }; const sendEvent = (event: string) => { // console.log('---- sending event:', event); appStateSend({ type: event }); @@ -143,7 +156,12 @@ export function ClientConfigStore() { const config = await ClientConfigService.getConfig(); setClientConfig(config); sendEvent('LOADED'); + setGlobalFatalErrorMessage(null); } catch (error) { + setGlobalFatalError( + 'Unable to reach Owncast server', + `Owncast cannot launch. Please make sure the Owncast server is running. ${error}`, + ); console.error(`ClientConfigService -> getConfig() ERROR: \n${error}`); } }; @@ -158,8 +176,13 @@ export function ClientConfigStore() { } else if (!status.online) { sendEvent(AppStateEvent.Offline); } + setGlobalFatalErrorMessage(null); } catch (error) { sendEvent(AppStateEvent.Fail); + setGlobalFatalError( + 'Unable to reach Owncast server', + `Owncast cannot launch. Please make sure the Owncast server is running. ${error}`, + ); console.error(`serverStatusState -> getStatus() ERROR: \n${error}`); } return null; diff --git a/web/stories/FatalErrorStateModal.stories.tsx b/web/stories/FatalErrorStateModal.stories.tsx new file mode 100644 index 000000000..679524188 --- /dev/null +++ b/web/stories/FatalErrorStateModal.stories.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { ComponentStory, ComponentMeta } from '@storybook/react'; +import FatalErrorStateModal from '../components/modals/FatalErrorModal'; + +export default { + title: 'owncast/Modals/Global error state', + component: FatalErrorStateModal, + parameters: {}, +} as ComponentMeta; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const Template: ComponentStory = args => ( + +); + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export const Example = Template.bind({}); +Example.args = { + title: 'Example error title', + message: 'Example error message', +}; diff --git a/web/types/displayable-error.ts b/web/types/displayable-error.ts new file mode 100644 index 000000000..b7164a691 --- /dev/null +++ b/web/types/displayable-error.ts @@ -0,0 +1,4 @@ +export interface DisplayableError { + title: string; + message: string; +}