diff --git a/webapp/packages/core-bootstrap/src/manifest.ts b/webapp/packages/core-bootstrap/src/manifest.ts index 56933cf17e..0d540b85e8 100644 --- a/webapp/packages/core-bootstrap/src/manifest.ts +++ b/webapp/packages/core-bootstrap/src/manifest.ts @@ -78,6 +78,7 @@ import { LocalizationService } from '@cloudbeaver/core-localization'; import { PluginManagerService } from '@cloudbeaver/core-plugin'; import { ProductManagerService, ProductSettingsService } from '@cloudbeaver/core-product'; import { + NetworkStateService, SessionService, ServerService, PermissionsService, @@ -104,6 +105,7 @@ export const coreManifest: PluginManifest = { providers: [ RouterService, // important, should be first because the router starts in load phase first after all plugins register phase + NetworkStateService, AdministrationLocaleService, AdministrationTopAppBarService, AdministrationScreenService, diff --git a/webapp/packages/core-root/src/NetworkError.ts b/webapp/packages/core-root/src/NetworkError.ts new file mode 100644 index 0000000000..f9ba8896be --- /dev/null +++ b/webapp/packages/core-root/src/NetworkError.ts @@ -0,0 +1,15 @@ + +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +export class NetworkError extends Error { + constructor(message: string) { + super(message); + this.name = 'NetworkError'; + } +} diff --git a/webapp/packages/core-root/src/NetworkStateService.ts b/webapp/packages/core-root/src/NetworkStateService.ts new file mode 100644 index 0000000000..337c85986f --- /dev/null +++ b/webapp/packages/core-root/src/NetworkStateService.ts @@ -0,0 +1,78 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { makeObservable, observable } from 'mobx'; + +import { Bootstrap, injectable } from '@cloudbeaver/core-di'; +import { Executor, IExecutor } from '@cloudbeaver/core-executor'; +import { GraphQLService } from '@cloudbeaver/core-sdk'; + +import { NetworkError } from './NetworkError'; + +@injectable() +export class NetworkStateService extends Bootstrap { + get state(): boolean { + return this.networkState; + } + + readonly networkStateExecutor: IExecutor; + private networkState: boolean; + + constructor( + private graphQLService: GraphQLService + ) { + super(); + makeObservable(this, { + networkState: observable, + }); + + this.networkState = true; + this.networkStateExecutor = new Executor(); + } + + register(): void { + this.networkState = window.navigator.onLine; + + window.addEventListener('online', () => this.setState(true)); + window.addEventListener('offline', () => this.setState(false)); + + this.graphQLService.registerInterceptor(this.sessionExpiredInterceptor.bind(this)); + } + + load(): void {} + + private setState(state: boolean) { + if (this.networkState === state) { + return; + } + + if (state) { + if (this.graphQLService.client.blockReason instanceof NetworkError) { + this.graphQLService.enableRequests(); + } + } else { + this.graphQLService.blockRequests(new NetworkError('Network connection was lost')); + } + this.networkState = state; + this.networkStateExecutor.execute(this.networkState); + } + + private async sessionExpiredInterceptor(request: Promise): Promise { + try { + return await request; + } catch (exception) { + if ( + exception instanceof TypeError + && exception.message === 'Failed to fetch' + ) { + throw new NetworkError('Error while processing request'); + } + throw exception; + } + } +} diff --git a/webapp/packages/core-root/src/index.ts b/webapp/packages/core-root/src/index.ts index 54c1c80c4f..84e2b35431 100644 --- a/webapp/packages/core-root/src/index.ts +++ b/webapp/packages/core-root/src/index.ts @@ -1,4 +1,6 @@ export * from './ConnectionNavigatorViewSettings'; +export * from './NetworkError'; +export * from './NetworkStateService'; export * from './PermissionsResource'; export * from './PermissionsService'; export * from './ServerConfigResource'; diff --git a/webapp/packages/core-sdk/src/CustomGraphQLClient.ts b/webapp/packages/core-sdk/src/CustomGraphQLClient.ts index 65faa8ca28..5390073ebf 100644 --- a/webapp/packages/core-sdk/src/CustomGraphQLClient.ts +++ b/webapp/packages/core-sdk/src/CustomGraphQLClient.ts @@ -13,9 +13,13 @@ import { GQLError } from './GQLError'; import type { IResponseInterceptor } from './IResponseInterceptor'; export class CustomGraphQLClient extends GraphQLClient { + get blockReason(): Error | string | null { + return this.requestsBlockedReason; + } + private interceptors: IResponseInterceptor[] = []; private isRequestsBlocked = false; - private requestsBlockedReason?: Error | string; + private requestsBlockedReason: Error | string | null = null; registerInterceptor(interceptor: IResponseInterceptor): void { this.interceptors.push(interceptor); @@ -28,6 +32,11 @@ export class CustomGraphQLClient extends GraphQLClient { ); } + enableRequests(): void { + this.isRequestsBlocked = false; + this.requestsBlockedReason = null; + } + blockRequests(reason: Error | string): void { this.isRequestsBlocked = true; this.requestsBlockedReason = reason; @@ -38,7 +47,7 @@ export class CustomGraphQLClient extends GraphQLClient { if (this.requestsBlockedReason instanceof Error) { throw this.requestsBlockedReason; } else { - throw new Error(this.requestsBlockedReason); + throw new Error(this.requestsBlockedReason ?? undefined); } } } diff --git a/webapp/packages/core-sdk/src/GraphQLService.ts b/webapp/packages/core-sdk/src/GraphQLService.ts index 1ae8df23ff..60f5413c6e 100644 --- a/webapp/packages/core-sdk/src/GraphQLService.ts +++ b/webapp/packages/core-sdk/src/GraphQLService.ts @@ -29,6 +29,10 @@ export class GraphQLService { this.client.registerInterceptor(interceptor); } + enableRequests(): void { + this.client.enableRequests(); + } + blockRequests(reason: Error | string): void { this.client.blockRequests(reason); }