diff options
author | Kristóf Marussy <kristof@marussy.com> | 2022-09-05 01:29:11 +0200 |
---|---|---|
committer | Kristóf Marussy <kristof@marussy.com> | 2022-09-06 01:05:24 +0200 |
commit | eb94326bb64552dbd7df62ae201ccca37f368467 (patch) | |
tree | b810f0230ace058cac8a6343455ca60113925221 /subprojects/frontend/src/editor | |
parent | refactor(frontend): more readable indentation (diff) | |
download | refinery-eb94326bb64552dbd7df62ae201ccca37f368467.tar.gz refinery-eb94326bb64552dbd7df62ae201ccca37f368467.tar.zst refinery-eb94326bb64552dbd7df62ae201ccca37f368467.zip |
feat(frontend): show connection status
Diffstat (limited to 'subprojects/frontend/src/editor')
6 files changed, 202 insertions, 1 deletions
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 @@ | |||
1 | import CloudIcon from '@mui/icons-material/Cloud'; | ||
2 | import CloudOffIcon from '@mui/icons-material/CloudOff'; | ||
3 | import SyncIcon from '@mui/icons-material/Sync'; | ||
4 | import SyncProblemIcon from '@mui/icons-material/SyncProblem'; | ||
5 | import IconButton from '@mui/material/IconButton'; | ||
6 | import { keyframes, styled } from '@mui/material/styles'; | ||
7 | import { observer } from 'mobx-react-lite'; | ||
8 | import React from 'react'; | ||
9 | |||
10 | import type EditorStore from './EditorStore'; | ||
11 | |||
12 | const rotateKeyframe = keyframes` | ||
13 | 0% { | ||
14 | transform: rotate(0deg); | ||
15 | } | ||
16 | 100% { | ||
17 | transform: rotate(-360deg); | ||
18 | } | ||
19 | `; | ||
20 | |||
21 | const AnimatedSyncIcon = styled(SyncIcon)` | ||
22 | animation: ${rotateKeyframe} 1.4s linear infinite; | ||
23 | `; | ||
24 | |||
25 | export default observer(function ConnectButton({ | ||
26 | editorStore, | ||
27 | }: { | ||
28 | editorStore: EditorStore | undefined; | ||
29 | }): JSX.Element { | ||
30 | if ( | ||
31 | editorStore !== undefined && | ||
32 | (editorStore.opening || editorStore.opened) | ||
33 | ) { | ||
34 | return ( | ||
35 | <IconButton | ||
36 | onClick={() => editorStore.disconnect()} | ||
37 | aria-label="Disconnect" | ||
38 | color="inherit" | ||
39 | > | ||
40 | {editorStore.opening ? ( | ||
41 | <AnimatedSyncIcon fontSize="small" /> | ||
42 | ) : ( | ||
43 | <CloudIcon fontSize="small" /> | ||
44 | )} | ||
45 | </IconButton> | ||
46 | ); | ||
47 | } | ||
48 | |||
49 | let disconnectedIcon: JSX.Element; | ||
50 | if (editorStore === undefined) { | ||
51 | disconnectedIcon = <SyncIcon fontSize="small" />; | ||
52 | } else if (editorStore.connectionErrors.length > 0) { | ||
53 | disconnectedIcon = <SyncProblemIcon fontSize="small" />; | ||
54 | } else { | ||
55 | disconnectedIcon = <CloudOffIcon fontSize="small" />; | ||
56 | } | ||
57 | |||
58 | return ( | ||
59 | <IconButton | ||
60 | disabled={editorStore === undefined} | ||
61 | onClick={() => editorStore?.connect()} | ||
62 | aria-label="Connect" | ||
63 | color="inherit" | ||
64 | > | ||
65 | {disconnectedIcon} | ||
66 | </IconButton> | ||
67 | ); | ||
68 | }); | ||
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 @@ | |||
1 | import Button from '@mui/material/Button'; | ||
2 | import { observer } from 'mobx-react-lite'; | ||
3 | import { type SnackbarKey, useSnackbar } from 'notistack'; | ||
4 | import React, { useEffect } from 'react'; | ||
5 | |||
6 | import { ContrastThemeProvider } from '../theme/ThemeProvider'; | ||
7 | |||
8 | import type EditorStore from './EditorStore'; | ||
9 | |||
10 | const CONNECTING_DEBOUNCE_TIMEOUT = 250; | ||
11 | |||
12 | export default observer(function ConnectionStatusNotification({ | ||
13 | editorStore, | ||
14 | }: { | ||
15 | editorStore: EditorStore; | ||
16 | }): null { | ||
17 | const { opened, opening, connectionErrors } = editorStore; | ||
18 | const { enqueueSnackbar, closeSnackbar } = useSnackbar(); | ||
19 | |||
20 | useEffect(() => { | ||
21 | if (opening) { | ||
22 | let key: SnackbarKey | undefined; | ||
23 | let timeout: number | undefined = setTimeout(() => { | ||
24 | timeout = undefined; | ||
25 | key = enqueueSnackbar('Connecting to Refinery', { | ||
26 | persist: true, | ||
27 | action: ( | ||
28 | <Button onClick={() => editorStore.disconnect()} color="inherit"> | ||
29 | Cancel | ||
30 | </Button> | ||
31 | ), | ||
32 | }); | ||
33 | }, CONNECTING_DEBOUNCE_TIMEOUT); | ||
34 | return () => { | ||
35 | if (timeout !== undefined) { | ||
36 | clearTimeout(timeout); | ||
37 | } | ||
38 | if (key !== undefined) { | ||
39 | closeSnackbar(key); | ||
40 | } | ||
41 | }; | ||
42 | } | ||
43 | |||
44 | if (connectionErrors.length >= 1) { | ||
45 | const key = enqueueSnackbar( | ||
46 | <div> | ||
47 | Connection error: <b>{connectionErrors[0]}</b> | ||
48 | {connectionErrors.length >= 2 && ( | ||
49 | <> | ||
50 | {' '} | ||
51 | and <b>{connectionErrors.length - 1}</b> more{' '} | ||
52 | {connectionErrors.length >= 3 ? 'errors' : 'error'} | ||
53 | </> | ||
54 | )} | ||
55 | </div>, | ||
56 | { | ||
57 | persist: !opened, | ||
58 | variant: 'error', | ||
59 | action: opened ? ( | ||
60 | <ContrastThemeProvider> | ||
61 | <Button onClick={() => editorStore.disconnect()} color="inherit"> | ||
62 | Disconnect | ||
63 | </Button> | ||
64 | </ContrastThemeProvider> | ||
65 | ) : ( | ||
66 | <ContrastThemeProvider> | ||
67 | <Button onClick={() => editorStore.connect()} color="inherit"> | ||
68 | Reconnect | ||
69 | </Button> | ||
70 | <Button onClick={() => editorStore.disconnect()} color="inherit"> | ||
71 | Cancel | ||
72 | </Button> | ||
73 | </ContrastThemeProvider> | ||
74 | ), | ||
75 | }, | ||
76 | ); | ||
77 | return () => closeSnackbar(key); | ||
78 | } | ||
79 | |||
80 | if (!opened) { | ||
81 | const key = enqueueSnackbar( | ||
82 | <div> | ||
83 | <b>Not connected to Refinery:</b> Some editing features might be | ||
84 | degraded | ||
85 | </div>, | ||
86 | { | ||
87 | action: ( | ||
88 | <ContrastThemeProvider> | ||
89 | <Button onClick={() => editorStore.connect()}>Reconnect</Button> | ||
90 | </ContrastThemeProvider> | ||
91 | ), | ||
92 | }, | ||
93 | ); | ||
94 | return () => closeSnackbar(key); | ||
95 | } | ||
96 | |||
97 | return () => {}; | ||
98 | }, [ | ||
99 | editorStore, | ||
100 | opened, | ||
101 | opening, | ||
102 | connectionErrors, | ||
103 | closeSnackbar, | ||
104 | enqueueSnackbar, | ||
105 | ]); | ||
106 | |||
107 | return null; | ||
108 | }); | ||
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'; | |||
15 | import { observer } from 'mobx-react-lite'; | 15 | import { observer } from 'mobx-react-lite'; |
16 | import React from 'react'; | 16 | import React from 'react'; |
17 | 17 | ||
18 | import ConnectButton from './ConnectButton'; | ||
18 | import type EditorStore from './EditorStore'; | 19 | import type EditorStore from './EditorStore'; |
19 | 20 | ||
20 | // Exhastive switch as proven by TypeScript. | 21 | // Exhastive switch as proven by TypeScript. |
@@ -93,13 +94,14 @@ export default observer(function EditorButtons({ | |||
93 | </ToggleButton> | 94 | </ToggleButton> |
94 | </ToggleButtonGroup> | 95 | </ToggleButtonGroup> |
95 | <IconButton | 96 | <IconButton |
96 | disabled={editorStore === undefined} | 97 | disabled={editorStore === undefined || !editorStore.opened} |
97 | onClick={() => editorStore?.formatText()} | 98 | onClick={() => editorStore?.formatText()} |
98 | aria-label="Automatic format" | 99 | aria-label="Automatic format" |
99 | color="inherit" | 100 | color="inherit" |
100 | > | 101 | > |
101 | <FormatPaint fontSize="small" /> | 102 | <FormatPaint fontSize="small" /> |
102 | </IconButton> | 103 | </IconButton> |
104 | <ConnectButton editorStore={editorStore} /> | ||
103 | </Stack> | 105 | </Stack> |
104 | ); | 106 | ); |
105 | }); | 107 | }); |
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'; | |||
7 | 7 | ||
8 | import { useRootStore } from '../RootStore'; | 8 | import { useRootStore } from '../RootStore'; |
9 | 9 | ||
10 | import ConnectionStatusNotification from './ConnectionStatusNotification'; | ||
10 | import EditorArea from './EditorArea'; | 11 | import EditorArea from './EditorArea'; |
11 | import EditorButtons from './EditorButtons'; | 12 | import EditorButtons from './EditorButtons'; |
12 | import GenerateButton from './GenerateButton'; | 13 | import GenerateButton from './GenerateButton'; |
@@ -43,6 +44,7 @@ export default observer(function EditorPane(): JSX.Element { | |||
43 | <EditorLoading /> | 44 | <EditorLoading /> |
44 | ) : ( | 45 | ) : ( |
45 | <> | 46 | <> |
47 | <ConnectionStatusNotification editorStore={editorStore} /> | ||
46 | <SearchPanelPortal editorStore={editorStore} /> | 48 | <SearchPanelPortal editorStore={editorStore} /> |
47 | <EditorArea editorStore={editorStore} /> | 49 | <EditorArea editorStore={editorStore} /> |
48 | </> | 50 | </> |
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 { | |||
69 | }); | 69 | }); |
70 | } | 70 | } |
71 | 71 | ||
72 | get opened(): boolean { | ||
73 | return this.client.webSocketClient.opened; | ||
74 | } | ||
75 | |||
76 | get opening(): boolean { | ||
77 | return this.client.webSocketClient.opening; | ||
78 | } | ||
79 | |||
80 | get connectionErrors(): string[] { | ||
81 | return this.client.webSocketClient.errors; | ||
82 | } | ||
83 | |||
84 | connect(): void { | ||
85 | this.client.webSocketClient.connect(); | ||
86 | } | ||
87 | |||
88 | disconnect(): void { | ||
89 | this.client.webSocketClient.disconnect(); | ||
90 | } | ||
91 | |||
72 | setDarkMode(darkMode: boolean): void { | 92 | setDarkMode(darkMode: boolean): void { |
73 | log.debug('Update editor dark mode', darkMode); | 93 | log.debug('Update editor dark mode', darkMode); |
74 | this.dispatch({ | 94 | 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({ | |||
46 | 46 | ||
47 | return ( | 47 | return ( |
48 | <Button | 48 | <Button |
49 | disabled={!editorStore.opened} | ||
49 | color={warningCount > 0 ? 'warning' : 'primary'} | 50 | color={warningCount > 0 ? 'warning' : 'primary'} |
50 | className="rounded" | 51 | className="rounded" |
51 | startIcon={<PlayArrowIcon />} | 52 | startIcon={<PlayArrowIcon />} |