From a1d68552031233fd973a73b414490da072549939 Mon Sep 17 00:00:00 2001 From: Michael Wedl Date: Tue, 30 Jul 2024 12:31:10 +0200 Subject: [PATCH 1/2] Hide connection spinner on first reconnection attempt --- frontend/src/components/CollabLoader.vue | 2 +- frontend/src/utils/collab.ts | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/CollabLoader.vue b/frontend/src/components/CollabLoader.vue index d5421859a..a761980dd 100644 --- a/frontend/src/components/CollabLoader.vue +++ b/frontend/src/components/CollabLoader.vue @@ -6,7 +6,7 @@ Promise; disconnect: () => Promise; send: (msg: CollabEvent) => void; @@ -530,6 +531,7 @@ export function useCollab(storeState: CollabStoreState) { storeState.connection = { type: CollabConnectionType.WEBSOCKET, connectionState: CollabConnectionState.CONNECTING, + connectionAttempt: 0, connect: () => Promise.resolve(), disconnect: () => Promise.resolve(), send: () => {}, @@ -540,6 +542,9 @@ export function useCollab(storeState: CollabStoreState) { try { return await connectTo(connectionWebsocket(storeState, onReceiveMessage)); } catch { + // Increment reconnection attempt counter + storeState.connection!.connectionAttempt = (storeState.connection!.connectionAttempt || 0) + 1; + // Backoff time for reconnecting if (i === 0) { await new Promise(resolve => setTimeout(resolve, 500)); From 2d61703385cb13f94c22498457d78760624d87f3 Mon Sep 17 00:00:00 2001 From: Michael Wedl Date: Tue, 30 Jul 2024 13:09:58 +0200 Subject: [PATCH 2/2] Add setting to disable websockets --- CHANGELOG.md | 1 + api/src/reportcreator_api/api_utils/views.py | 1 + api/src/reportcreator_api/conf/settings.py | 3 ++- docs/docs/setup/configuration.md | 10 +++++++++ frontend/src/components/CollabLoader.vue | 4 +++- frontend/src/utils/collab.ts | 23 +++++++++++--------- frontend/src/utils/types.ts | 1 + 7 files changed, 31 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e1d758210..c3d99a1bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Next * Set restart policy for redis docker container +* Add setting to disable websockets and always use HTTP for collaborative editing * Fix error while sorting finding templates by created/updated date * Fix create template from finding changes not saved diff --git a/api/src/reportcreator_api/api_utils/views.py b/api/src/reportcreator_api/api_utils/views.py index 654a2968d..9f99afc5b 100644 --- a/api/src/reportcreator_api/api_utils/views.py +++ b/api/src/reportcreator_api/api_utils/views.py @@ -95,6 +95,7 @@ def settings_endpoint(self, *args, **kwargs): 'archiving': license.is_professional(), 'permissions': license.is_professional(), 'backup': bool(settings.BACKUP_KEY and license.is_professional()), + 'websockets': not settings.DISABLE_WEBSOCKETS, }, 'guest_permissions': { 'import_projects': settings.GUEST_USERS_CAN_IMPORT_PROJECTS, diff --git a/api/src/reportcreator_api/conf/settings.py b/api/src/reportcreator_api/conf/settings.py index eade2cd69..d758147e7 100644 --- a/api/src/reportcreator_api/conf/settings.py +++ b/api/src/reportcreator_api/conf/settings.py @@ -172,7 +172,8 @@ # Websockets REDIS_URL = config('REDIS_URL', default=None) -if REDIS_URL: +DISABLE_WEBSOCKETS = config('DISABLE_WEBSOCKETS', cast=bool, default=False) or (not REDIS_URL) +if not DISABLE_WEBSOCKETS: CHANNEL_LAYERS = { 'default': { 'BACKEND': 'channels_redis.core.RedisChannelLayer', diff --git a/docs/docs/setup/configuration.md b/docs/docs/setup/configuration.md index de8321144..c7aa62af7 100644 --- a/docs/docs/setup/configuration.md +++ b/docs/docs/setup/configuration.md @@ -261,3 +261,13 @@ This flag also enables setting the `Secure` flag for cookies. ``` SECURE_SSL_REDIRECT=on ``` + + +### WebSockets +Disable WebSockets and always use HTTP fallback for collaborative editing. +This is not recommended because some features are only available with WebSockets and HTTP fallback has higher latency. +This setting sould only be activated if WebSockets are blocked by a firewall or not supported by a reverse proxy. + +``` title="Example:" +DISABLE_WEBSOCKETS=true +``` diff --git a/frontend/src/components/CollabLoader.vue b/frontend/src/components/CollabLoader.vue index a761980dd..6b8f7797a 100644 --- a/frontend/src/components/CollabLoader.vue +++ b/frontend/src/components/CollabLoader.vue @@ -36,6 +36,8 @@ defineOptions({ inheritAttrs: false }); +const apiSettings = useApiSettings(); + const props = defineProps<{ collab: { connection: ComputedRef; @@ -53,7 +55,7 @@ watch(connectionState, async (newState, oldState) => { reconnectAttempted.value = 0; // Show warning if HTTP fallback was used - if (props.collab.connection.value?.type === CollabConnectionType.HTTP_FALLBACK && !warningHttpFallbackShown.value) { + if (apiSettings.settings!.features.websockets && props.collab.connection.value?.type === CollabConnectionType.HTTP_FALLBACK && !warningHttpFallbackShown.value) { warningHttpFallbackShown.value = true; warningToast('Could not establish WebSocket connection, falling back to HTTP. Some features may be limited.'); } diff --git a/frontend/src/utils/collab.ts b/frontend/src/utils/collab.ts index ee7a04466..7aebda4d6 100644 --- a/frontend/src/utils/collab.ts +++ b/frontend/src/utils/collab.ts @@ -493,6 +493,7 @@ export function connectionHttpReadonly(storeState: CollabStoreState, } export function useCollab(storeState: CollabStoreState) { + const apiSettings = useApiSettings(); const eventBusBeforeApplyRemoteTextChange = useEventBus('collab:beforeApplyRemoteTextChanges'); const eventBusBeforeApplySetValue = useEventBus('collab:beforeApplySetValue'); @@ -538,16 +539,18 @@ export function useCollab(storeState: CollabStoreState) { }; // Try websocket connection first - for (let i = 0; i < 2; i++) { - try { - return await connectTo(connectionWebsocket(storeState, onReceiveMessage)); - } catch { - // Increment reconnection attempt counter - storeState.connection!.connectionAttempt = (storeState.connection!.connectionAttempt || 0) + 1; - - // Backoff time for reconnecting - if (i === 0) { - await new Promise(resolve => setTimeout(resolve, 500)); + if (apiSettings.settings!.features.websockets) { + for (let i = 0; i < 2; i++) { + try { + return await connectTo(connectionWebsocket(storeState, onReceiveMessage)); + } catch { + // Increment reconnection attempt counter + storeState.connection!.connectionAttempt = (storeState.connection!.connectionAttempt || 0) + 1; + + // Backoff time for reconnecting + if (i === 0) { + await new Promise(resolve => setTimeout(resolve, 500)); + } } } } diff --git a/frontend/src/utils/types.ts b/frontend/src/utils/types.ts index 28cb6813b..9dc1e9ad6 100644 --- a/frontend/src/utils/types.ts +++ b/frontend/src/utils/types.ts @@ -109,6 +109,7 @@ export type ApiSettings = { readonly archiving: boolean; readonly permissions: boolean; readonly backup: boolean; + readonly websockets: boolean; }; readonly guest_permissions: { readonly import_projects: boolean;