Skip to content

Commit

Permalink
GH-1155: Added support to handle the connection issues gracefully.
Browse files Browse the repository at this point in the history
Signed-off-by: Akos Kitta <kittaakos@gmail.com>
  • Loading branch information
kittaakos committed Feb 7, 2018
1 parent 55e431b commit 316241f
Show file tree
Hide file tree
Showing 12 changed files with 829 additions and 2 deletions.
1 change: 1 addition & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
"build": "theiaext build",
"watch": "theiaext watch",
"test": "theiaext test",
"test:watch": "theiaext test:watch",
"docs": "theiaext docs"
},
"devDependencies": {
Expand Down
21 changes: 19 additions & 2 deletions packages/core/src/browser/frontend-application-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
MessageService,
MessageClient
} from "../common";
import { FrontendApplication, FrontendApplicationContribution } from './frontend-application';
import { FrontendApplication, FrontendApplicationContribution, DefaultFrontendApplicationContribution } from './frontend-application';
import { DefaultOpenerService, OpenerService, OpenHandler } from './opener-service';
import { HttpOpenHandler } from './http-open-handler';
import { CommonFrontendContribution } from './common-frontend-contribution';
Expand All @@ -32,14 +32,19 @@ import {
import { StatusBar, StatusBarImpl } from "./status-bar/status-bar";
import { LabelParser } from './label-parser';
import { LabelProvider, LabelProviderContribution, DefaultUriLabelProviderContribution } from "./label-provider";
import { ThemingCommandContribution, ThemeService } from './theming';
import { IdleService } from '../common/idle-service';
import { FrontendIdleService } from './frontend-idle-service';
import { ConnectionStatusService } from '../common/connection-status-service';
import { FrontendConnectionStatusService, ApplicationConnectionStatusContribution, ConnectionStatusStatusBarContribution } from './frontend-connection-status-service';

import '../../src/browser/style/index.css';
import 'font-awesome/css/font-awesome.min.css';
import "file-icons-js/css/style.css";
import { ThemingCommandContribution, ThemeService } from './theming';

export const frontendApplicationModule = new ContainerModule((bind, unbind, isBound, rebind) => {
bind(FrontendApplication).toSelf().inSingletonScope();
bind(DefaultFrontendApplicationContribution).toSelf();
bindContributionProvider(bind, FrontendApplicationContribution);

bind(ApplicationShellOptions).toConstantValue({});
Expand Down Expand Up @@ -108,6 +113,18 @@ export const frontendApplicationModule = new ContainerModule((bind, unbind, isBo
bind(LabelProviderContribution).to(DefaultUriLabelProviderContribution).inSingletonScope();

bind(CommandContribution).to(ThemingCommandContribution).inSingletonScope();

bind(FrontendIdleService).toSelf().inSingletonScope();
bind(IdleService).toDynamicValue(ctx => ctx.container.get(FrontendIdleService)).inSingletonScope();
bind(FrontendApplicationContribution).toDynamicValue(ctx => ctx.container.get(FrontendIdleService)).inSingletonScope();

bind(FrontendConnectionStatusService).toSelf().inSingletonScope();
bind(ConnectionStatusService).toDynamicValue(ctx => ctx.container.get(FrontendConnectionStatusService)).inSingletonScope();
bind(FrontendApplicationContribution).toDynamicValue(ctx => ctx.container.get(FrontendConnectionStatusService)).inSingletonScope();
bind(ApplicationConnectionStatusContribution).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toDynamicValue(ctx => ctx.container.get(ApplicationConnectionStatusContribution)).inSingletonScope();
bind(ConnectionStatusStatusBarContribution).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toDynamicValue(ctx => ctx.container.get(ConnectionStatusStatusBarContribution)).inSingletonScope();
});

const theme = ThemeService.get().getCurrentTheme().id;
Expand Down
13 changes: 13 additions & 0 deletions packages/core/src/browser/frontend-application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,19 @@ export interface FrontendApplicationContribution {
initializeLayout?(app: FrontendApplication): MaybePromise<void>;
}

/**
* Default frontend contribution that can be extended by clients if they do not want to implement any of the
* methods from the interface but still want to contribute to the frontend application.
*/
@injectable()
export abstract class DefaultFrontendApplicationContribution implements FrontendApplicationContribution {

initialize() {
// NOOP
}

}

@injectable()
export class FrontendApplication {

Expand Down
232 changes: 232 additions & 0 deletions packages/core/src/browser/frontend-connection-status-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
/*
* Copyright (C) 2018 TypeFox and others.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*/

import { inject, injectable, optional } from 'inversify';
import { ILogger } from '../common/logger';
import { Endpoint } from './endpoint';
import { IdleService } from '../common/idle-service';
import { AbstractDialog, DialogProps } from './dialogs';
import { StatusBar, StatusBarAlignment } from './status-bar/status-bar';
import { FrontendApplicationContribution, DefaultFrontendApplicationContribution } from './frontend-application';
import { AbstractConnectionStatusService, ConnectionState, ConnectionStatusOptions, ConnectionStatus, ConnectionStatusService } from '../common/connection-status-service';

@injectable()
export class FrontendConnectionStatusOptions extends ConnectionStatusOptions {

static DEFAULT: FrontendConnectionStatusOptions = {
...ConnectionStatusOptions.DEFAULT,
requestTimeout: 1000,
};

/**
* Timeout for the HTTP GET request in milliseconds. Must be a positive integer.
*/
readonly requestTimeout: number;

}

@injectable()
export abstract class FrontendConnectionStatusService extends AbstractConnectionStatusService<number> implements FrontendApplicationContribution {

private readonly endpointUrl: string;

constructor(
@inject(FrontendConnectionStatusOptions) @optional() protected readonly options: FrontendConnectionStatusOptions = FrontendConnectionStatusOptions.DEFAULT,
@inject(IdleService) protected idleService: IdleService,
@inject(ILogger) protected readonly logger: ILogger
) {
super(options, idleService, logger);
this.endpointUrl = new Endpoint().getRestUrl().toString();
}

onStart() {
this.start();
}

onStop() {
this.stop();
}

protected checkAlive(): Promise<boolean> {
return new Promise<boolean>(resolve => {
const handle = (success: boolean) => {
return resolve(success);
};
const xhr = new XMLHttpRequest();
xhr.timeout = this.options.requestTimeout;
xhr.onreadystatechange = () => {
const { readyState, status } = xhr;
if (readyState === XMLHttpRequest.DONE) {
handle(status === 200);
}
};
xhr.onerror = () => handle(false);
xhr.ontimeout = () => handle(false);
xhr.open('GET', `${this.endpointUrl}/alive`);
try {
xhr.send();
} catch {
handle(false);
}
});
}

protected setTimeout(handler: (...args: any[]) => void, timeout: number): number {
return window.setTimeout(handler, timeout);
}

protected clearTimeout(handle: number): void {
window.clearTimeout(handle);
}

}

@injectable()
export class ConnectionStatusStatusBarContribution extends DefaultFrontendApplicationContribution {

constructor(
@inject(ConnectionStatusService) protected readonly connectionStatusService: ConnectionStatusService,
@inject(StatusBar) protected statusBar: StatusBar
) {
super();
this.connectionStatusService.onStatusChange(status => this.onStatusChange(status));
}

protected onStatusChange(status: ConnectionStatus) {
this.statusBar.removeElement('connection-status');
const text = `$(${this.getStatusIcon(status)})`;
const tooltip = this.getStatusTooltip(status);
this.statusBar.setElement('connection-status', {
alignment: StatusBarAlignment.RIGHT,
text,
priority: 0,
tooltip
});
}

protected getStatusIcon(status: ConnectionStatus) {
if (status.state === ConnectionState.IDLE) {
return 'bed';
}
const { health } = status;
if (health === undefined || health === 0) {
return 'exclamation-circle';
}
if (health < 25) {
return 'frown-o';
}
if (health < 50) {
return 'meh-o';
}
return 'smile-o';
}

protected getStatusTooltip(status: ConnectionStatus) {
if (status.state === ConnectionState.IDLE) {
return 'Idle';
}
return status.health ? `Online [Connection health: ${status.health}%]` : 'Offline';
}

}

@injectable()
export class ApplicationConnectionStatusContribution extends DefaultFrontendApplicationContribution {

private dialog: ConnectionStatusDialog | undefined;
private state = ConnectionState.ONLINE;

constructor(
@inject(ConnectionStatusService) protected readonly connectionStatusService: ConnectionStatusService,
@inject(ILogger) protected readonly logger: ILogger
) {
super();
this.connectionStatusService.onStatusChange(status => this.onStatusChange(status));
}

protected onStatusChange(status: ConnectionStatus): void {
if (this.state !== status.state) {
this.state = status.state;
switch (status.state) {
case ConnectionState.OFFLINE: {
this.handleOffline();
break;
}
case ConnectionState.ONLINE: {
this.handleOnline();
break;
}
case ConnectionState.IDLE: {
this.handleIdle();
break;
}
}
}
}

protected getOrCreateDialog(content: string): ConnectionStatusDialog {
if (this.dialog === undefined) {
this.dialog = new ConnectionStatusDialog({
title: 'Not connected',
content
});
}
return this.dialog;
}

protected handleOnline() {
const message = 'Successfully reconnected to the backend.';
this.logger.info(message);
if (this.dialog !== undefined) {
this.dialog.dispose();
this.dialog = undefined;
}
}

protected handleOffline() {
const message = 'The application connection to the backend is lost. Attempting to reconnect...';
if (this.dialog === undefined) {
this.logger.error(message);
this.getOrCreateDialog(message).open();
} else {
this.dialog.updateContent(message);
}
}

protected handleIdle() {
if (this.dialog !== undefined) {
const message = 'The application connection to the backend is lost.';
this.dialog.updateContent(message);
}
}

}

export class ConnectionStatusDialog extends AbstractDialog<void> {

public readonly value: void;
protected readonly contentText: Text;

constructor(dialogProps: DialogProps & { content: string }) {
super(dialogProps);
// Just to remove the X, so that the dialog cannot be closed by the user.
this.closeCrossNode.remove();
this.contentText = document.createTextNode(dialogProps.content);
this.contentNode.appendChild(this.contentText);
}

protected onAfterAttach() {
// NOOP.
// We need disable the key listener for escape and return so that the dialog cannot be closed by the user.
}

public updateContent(content: string) {
this.contentText.data = content;
this.update();
}

}
Loading

0 comments on commit 316241f

Please sign in to comment.