Display global error if server is unreachable

This commit is contained in:
Gabe Kangas 2022-05-27 22:27:20 -07:00
parent ce9d403269
commit aae63e4e2c
No known key found for this signature in database
GPG Key ID: 9A56337728BC81EA
5 changed files with 88 additions and 1 deletions

View File

@ -4,20 +4,28 @@ import {
ClientConfigStore, ClientConfigStore,
isChatAvailableSelector, isChatAvailableSelector,
clientConfigStateAtom, clientConfigStateAtom,
fatalErrorStateAtom,
} from '../stores/ClientConfigStore'; } from '../stores/ClientConfigStore';
import { Content, Header } from '../ui'; import { Content, Header } from '../ui';
import { ClientConfig } from '../../interfaces/client-config.model'; import { ClientConfig } from '../../interfaces/client-config.model';
import { DisplayableError } from '../../types/displayable-error';
import FatalErrorStateModal from '../modals/FatalErrorModal';
function Main() { function Main() {
const clientConfig = useRecoilValue<ClientConfig>(clientConfigStateAtom); const clientConfig = useRecoilValue<ClientConfig>(clientConfigStateAtom);
const { name, title } = clientConfig; const { name, title } = clientConfig;
const isChatAvailable = useRecoilValue<boolean>(isChatAvailableSelector); const isChatAvailable = useRecoilValue<boolean>(isChatAvailableSelector);
const fatalError = useRecoilValue<DisplayableError>(fatalErrorStateAtom);
return ( return (
<> <>
<ClientConfigStore /> <ClientConfigStore />
<Layout> <Layout>
<Header name={title || name} chatAvailable={isChatAvailable} /> <Header name={title || name} chatAvailable={isChatAvailable} />
<Content /> <Content />
{fatalError && (
<FatalErrorStateModal title={fatalError.title} message={fatalError.message} />
)}
</Layout> </Layout>
</> </>
); );

View File

@ -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 (
<Modal
title={title}
visible
footer={null}
closable={false}
keyboard={false}
width={900}
centered
className="modal"
>
<style global jsx>{`
.modal .ant-modal-content,
.modal .ant-modal-header {
background-color: var(--warning-color);
}
`}</style>
<p>{message}</p>
</Modal>
);
}

View File

@ -24,6 +24,7 @@ import handleChatMessage from './eventhandlers/handleChatMessage';
import handleConnectedClientInfoMessage from './eventhandlers/connected-client-info-handler'; import handleConnectedClientInfoMessage from './eventhandlers/connected-client-info-handler';
import ServerStatusService from '../../services/status-service'; import ServerStatusService from '../../services/status-service';
import handleNameChangeEvent from './eventhandlers/handleNameChangeEvent'; import handleNameChangeEvent from './eventhandlers/handleNameChangeEvent';
import { DisplayableError } from '../../types/displayable-error';
const SERVER_STATUS_POLL_DURATION = 5000; const SERVER_STATUS_POLL_DURATION = 5000;
const ACCESS_TOKEN_KEY = 'accessToken'; const ACCESS_TOKEN_KEY = 'accessToken';
@ -76,6 +77,11 @@ export const isVideoPlayingAtom = atom<boolean>({
default: false, default: false,
}); });
export const fatalErrorStateAtom = atom<DisplayableError>({
key: 'fatalErrorStateAtom',
default: null,
});
// Chat is visible if the user wishes it to be visible AND the required // Chat is visible if the user wishes it to be visible AND the required
// chat state is set. // chat state is set.
export const isChatVisibleSelector = selector({ export const isChatVisibleSelector = selector({
@ -129,10 +135,17 @@ export function ClientConfigStore() {
const [chatMessages, setChatMessages] = useRecoilState<ChatMessage[]>(chatMessagesAtom); const [chatMessages, setChatMessages] = useRecoilState<ChatMessage[]>(chatMessagesAtom);
const [accessToken, setAccessToken] = useRecoilState<string>(accessTokenAtom); const [accessToken, setAccessToken] = useRecoilState<string>(accessTokenAtom);
const setAppState = useSetRecoilState<AppStateOptions>(appStateAtom); const setAppState = useSetRecoilState<AppStateOptions>(appStateAtom);
const setGlobalFatalErrorMessage = useSetRecoilState<DisplayableError>(fatalErrorStateAtom);
const setWebsocketService = useSetRecoilState<WebsocketService>(websocketServiceAtom); const setWebsocketService = useSetRecoilState<WebsocketService>(websocketServiceAtom);
let ws: WebsocketService; let ws: WebsocketService;
const setGlobalFatalError = (title: string, message: string) => {
setGlobalFatalErrorMessage({
title,
message,
});
};
const sendEvent = (event: string) => { const sendEvent = (event: string) => {
// console.log('---- sending event:', event); // console.log('---- sending event:', event);
appStateSend({ type: event }); appStateSend({ type: event });
@ -143,7 +156,12 @@ export function ClientConfigStore() {
const config = await ClientConfigService.getConfig(); const config = await ClientConfigService.getConfig();
setClientConfig(config); setClientConfig(config);
sendEvent('LOADED'); sendEvent('LOADED');
setGlobalFatalErrorMessage(null);
} catch (error) { } 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}`); console.error(`ClientConfigService -> getConfig() ERROR: \n${error}`);
} }
}; };
@ -158,8 +176,13 @@ export function ClientConfigStore() {
} else if (!status.online) { } else if (!status.online) {
sendEvent(AppStateEvent.Offline); sendEvent(AppStateEvent.Offline);
} }
setGlobalFatalErrorMessage(null);
} catch (error) { } catch (error) {
sendEvent(AppStateEvent.Fail); 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}`); console.error(`serverStatusState -> getStatus() ERROR: \n${error}`);
} }
return null; return null;

View File

@ -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<typeof FatalErrorStateModal>;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const Template: ComponentStory<typeof FatalErrorStateModal> = args => (
<FatalErrorStateModal {...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',
};

View File

@ -0,0 +1,4 @@
export interface DisplayableError {
title: string;
message: string;
}