From eb94326bb64552dbd7df62ae201ccca37f368467 Mon Sep 17 00:00:00 2001 From: Kristóf Marussy Date: Mon, 5 Sep 2022 01:29:11 +0200 Subject: feat(frontend): show connection status --- subprojects/frontend/src/editor/ConnectButton.tsx | 68 +++++++++++++ .../src/editor/ConnectionStatusNotification.tsx | 108 +++++++++++++++++++++ subprojects/frontend/src/editor/EditorButtons.tsx | 4 +- subprojects/frontend/src/editor/EditorPane.tsx | 2 + subprojects/frontend/src/editor/EditorStore.ts | 20 ++++ subprojects/frontend/src/editor/GenerateButton.tsx | 1 + subprojects/frontend/src/index.tsx | 4 +- .../frontend/src/xtext/ContentAssistService.ts | 7 ++ .../frontend/src/xtext/HighlightingService.ts | 4 + .../frontend/src/xtext/OccurrencesService.ts | 11 ++- subprojects/frontend/src/xtext/UpdateService.ts | 4 + .../frontend/src/xtext/ValidationService.ts | 4 + subprojects/frontend/src/xtext/XtextClient.ts | 16 ++- .../frontend/src/xtext/XtextWebSocketClient.ts | 12 +++ 14 files changed, 260 insertions(+), 5 deletions(-) create mode 100644 subprojects/frontend/src/editor/ConnectButton.tsx create mode 100644 subprojects/frontend/src/editor/ConnectionStatusNotification.tsx diff --git a/subprojects/frontend/src/editor/ConnectButton.tsx b/subprojects/frontend/src/editor/ConnectButton.tsx new file mode 100644 index 00000000..52e7b854 --- /dev/null +++ b/subprojects/frontend/src/editor/ConnectButton.tsx @@ -0,0 +1,68 @@ +import CloudIcon from '@mui/icons-material/Cloud'; +import CloudOffIcon from '@mui/icons-material/CloudOff'; +import SyncIcon from '@mui/icons-material/Sync'; +import SyncProblemIcon from '@mui/icons-material/SyncProblem'; +import IconButton from '@mui/material/IconButton'; +import { keyframes, styled } from '@mui/material/styles'; +import { observer } from 'mobx-react-lite'; +import React from 'react'; + +import type EditorStore from './EditorStore'; + +const rotateKeyframe = keyframes` + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(-360deg); + } +`; + +const AnimatedSyncIcon = styled(SyncIcon)` + animation: ${rotateKeyframe} 1.4s linear infinite; +`; + +export default observer(function ConnectButton({ + editorStore, +}: { + editorStore: EditorStore | undefined; +}): JSX.Element { + if ( + editorStore !== undefined && + (editorStore.opening || editorStore.opened) + ) { + return ( + editorStore.disconnect()} + aria-label="Disconnect" + color="inherit" + > + {editorStore.opening ? ( + + ) : ( + + )} + + ); + } + + let disconnectedIcon: JSX.Element; + if (editorStore === undefined) { + disconnectedIcon = ; + } else if (editorStore.connectionErrors.length > 0) { + disconnectedIcon = ; + } else { + disconnectedIcon = ; + } + + return ( + editorStore?.connect()} + aria-label="Connect" + color="inherit" + > + {disconnectedIcon} + + ); +}); diff --git a/subprojects/frontend/src/editor/ConnectionStatusNotification.tsx b/subprojects/frontend/src/editor/ConnectionStatusNotification.tsx new file mode 100644 index 00000000..e402e296 --- /dev/null +++ b/subprojects/frontend/src/editor/ConnectionStatusNotification.tsx @@ -0,0 +1,108 @@ +import Button from '@mui/material/Button'; +import { observer } from 'mobx-react-lite'; +import { type SnackbarKey, useSnackbar } from 'notistack'; +import React, { useEffect } from 'react'; + +import { ContrastThemeProvider } from '../theme/ThemeProvider'; + +import type EditorStore from './EditorStore'; + +const CONNECTING_DEBOUNCE_TIMEOUT = 250; + +export default observer(function ConnectionStatusNotification({ + editorStore, +}: { + editorStore: EditorStore; +}): null { + const { opened, opening, connectionErrors } = editorStore; + const { enqueueSnackbar, closeSnackbar } = useSnackbar(); + + useEffect(() => { + if (opening) { + let key: SnackbarKey | undefined; + let timeout: number | undefined = setTimeout(() => { + timeout = undefined; + key = enqueueSnackbar('Connecting to Refinery', { + persist: true, + action: ( + + ), + }); + }, CONNECTING_DEBOUNCE_TIMEOUT); + return () => { + if (timeout !== undefined) { + clearTimeout(timeout); + } + if (key !== undefined) { + closeSnackbar(key); + } + }; + } + + if (connectionErrors.length >= 1) { + const key = enqueueSnackbar( +
+ Connection error: {connectionErrors[0]} + {connectionErrors.length >= 2 && ( + <> + {' '} + and {connectionErrors.length - 1} more{' '} + {connectionErrors.length >= 3 ? 'errors' : 'error'} + + )} +
, + { + persist: !opened, + variant: 'error', + action: opened ? ( + + + + ) : ( + + + + + ), + }, + ); + return () => closeSnackbar(key); + } + + if (!opened) { + const key = enqueueSnackbar( +
+ Not connected to Refinery: Some editing features might be + degraded +
, + { + action: ( + + + + ), + }, + ); + return () => closeSnackbar(key); + } + + return () => {}; + }, [ + editorStore, + opened, + opening, + connectionErrors, + closeSnackbar, + enqueueSnackbar, + ]); + + return null; +}); diff --git a/subprojects/frontend/src/editor/EditorButtons.tsx b/subprojects/frontend/src/editor/EditorButtons.tsx index d2273c6c..fd046d46 100644 --- a/subprojects/frontend/src/editor/EditorButtons.tsx +++ b/subprojects/frontend/src/editor/EditorButtons.tsx @@ -15,6 +15,7 @@ import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'; import { observer } from 'mobx-react-lite'; import React from 'react'; +import ConnectButton from './ConnectButton'; import type EditorStore from './EditorStore'; // Exhastive switch as proven by TypeScript. @@ -93,13 +94,14 @@ export default observer(function EditorButtons({ editorStore?.formatText()} aria-label="Automatic format" color="inherit" > + ); }); diff --git a/subprojects/frontend/src/editor/EditorPane.tsx b/subprojects/frontend/src/editor/EditorPane.tsx index 2651726c..079ebcdc 100644 --- a/subprojects/frontend/src/editor/EditorPane.tsx +++ b/subprojects/frontend/src/editor/EditorPane.tsx @@ -7,6 +7,7 @@ import React, { useState } from 'react'; import { useRootStore } from '../RootStore'; +import ConnectionStatusNotification from './ConnectionStatusNotification'; import EditorArea from './EditorArea'; import EditorButtons from './EditorButtons'; import GenerateButton from './GenerateButton'; @@ -43,6 +44,7 @@ export default observer(function EditorPane(): JSX.Element { ) : ( <> + diff --git a/subprojects/frontend/src/editor/EditorStore.ts b/subprojects/frontend/src/editor/EditorStore.ts index 4407376b..3ec33b2c 100644 --- a/subprojects/frontend/src/editor/EditorStore.ts +++ b/subprojects/frontend/src/editor/EditorStore.ts @@ -69,6 +69,26 @@ export default class EditorStore { }); } + get opened(): boolean { + return this.client.webSocketClient.opened; + } + + get opening(): boolean { + return this.client.webSocketClient.opening; + } + + get connectionErrors(): string[] { + return this.client.webSocketClient.errors; + } + + connect(): void { + this.client.webSocketClient.connect(); + } + + disconnect(): void { + this.client.webSocketClient.disconnect(); + } + setDarkMode(darkMode: boolean): void { log.debug('Update editor dark mode', darkMode); this.dispatch({ diff --git a/subprojects/frontend/src/editor/GenerateButton.tsx b/subprojects/frontend/src/editor/GenerateButton.tsx index 5254f6cb..2ffb1a94 100644 --- a/subprojects/frontend/src/editor/GenerateButton.tsx +++ b/subprojects/frontend/src/editor/GenerateButton.tsx @@ -46,6 +46,7 @@ export default observer(function GenerateButton({ return (