Skip to content

Commit

Permalink
UI for manage Theia development mode (#13)
Browse files Browse the repository at this point in the history
* UI for development mode

* Code improvements

Signed-off-by: Vitaliy Gulyy <vguliy@codenvy.com>
  • Loading branch information
Vitaliy Gulyy authored and benoitf committed Jun 20, 2018
1 parent d0f4dca commit e26a507
Show file tree
Hide file tree
Showing 6 changed files with 444 additions and 41 deletions.
90 changes: 90 additions & 0 deletions packages/plugin-ext/src/hosted/browser/hosted-plugin-informer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* Copyright (C) 2018 Red Hat, Inc. 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 { injectable, inject } from 'inversify';
import { StatusBar } from '@theia/core/lib/browser/status-bar/status-bar';
import { StatusBarAlignment, StatusBarEntry, FrontendApplicationContribution } from '@theia/core/lib/browser';
import { WorkspaceService } from '@theia/workspace/lib/browser';
import { HostedPluginServer } from '../../common/plugin-protocol';
import { ConnectionStatusService, ConnectionState } from '@theia/core/lib/browser/connection-status-service';
import URI from '@theia/core/lib/common/uri';
import { FileStat } from '@theia/filesystem/lib/common';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';

/**
* Informs the user whether Theia is running with hosted plugin.
* Adds 'Development Host' status bar element and appends the same prefix to window title.
*/
@injectable()
export class HostedPluginInformer implements FrontendApplicationContribution {

public static readonly DEVELOPMENT_HOST_TITLE = "Development Host";

public static readonly DEVELOPMENT_HOST = "development-host";

public static readonly DEVELOPMENT_HOST_OFFLINE = "development-host-offline";

private entry: StatusBarEntry;

@inject(StatusBar)
protected readonly statusBar: StatusBar;

@inject(WorkspaceService)
protected readonly workspaceService: WorkspaceService;

@inject(HostedPluginServer)
protected readonly hostedPluginServer: HostedPluginServer;

@inject(ConnectionStatusService)
protected readonly connectionStatusService: ConnectionStatusService;

@inject(FrontendApplicationStateService)
protected readonly frontendApplicationStateService: FrontendApplicationStateService;

public initialize(): void {
this.workspaceService.root.then(root => {
this.hostedPluginServer.getHostedPlugin().then(pluginMetadata => {
if (pluginMetadata) {
this.updateTitle(root);

this.entry = {
text: `$(cube) ${HostedPluginInformer.DEVELOPMENT_HOST_TITLE}`,
tooltip: `Hosted Plugin '${pluginMetadata.model.name}'`,
alignment: StatusBarAlignment.LEFT,
priority: 100
};

this.frontendApplicationStateService.reachedState('ready').then(() => {
this.updateStatusBarElement();
});

this.connectionStatusService.onStatusChange(() => this.updateStatusBarElement());
}
});
});
}

private updateStatusBarElement(): void {
if (this.connectionStatusService.currentState.state === ConnectionState.OFFLINE) {
this.entry.className = HostedPluginInformer.DEVELOPMENT_HOST_OFFLINE;
} else {
this.entry.className = HostedPluginInformer.DEVELOPMENT_HOST;
}

this.statusBar.setElement(HostedPluginInformer.DEVELOPMENT_HOST, this.entry);
}

private updateTitle(root: FileStat | undefined): void {
if (root) {
const uri = new URI(root.uri);
document.title = HostedPluginInformer.DEVELOPMENT_HOST_TITLE + " - " + uri.displayName;
} else {
document.title = HostedPluginInformer.DEVELOPMENT_HOST_TITLE;
}
}

}
241 changes: 241 additions & 0 deletions packages/plugin-ext/src/main/browser/hosted-plugin-controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
/*
* Copyright (C) 2018 Red Hat, Inc. 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 { injectable, inject } from 'inversify';
import { StatusBar } from '@theia/core/lib/browser/status-bar/status-bar';
import { StatusBarAlignment, StatusBarEntry, FrontendApplicationContribution } from '@theia/core/lib/browser';
import { HostedPluginServer } from '../../common/plugin-protocol';
import { HostedPluginManagerClient, HostedPluginState, HostedPluginCommands } from './plugin-manager-client';
import { CommandRegistry } from '@phosphor/commands';
import { Menu } from '@phosphor/widgets';
import { setTimeout } from 'timers';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
import { ConnectionStatusService, ConnectionState } from '@theia/core/lib/browser/connection-status-service';

/**
* Adds a status bar element displaying the state of secondary Theia instance with hosted plugin and
* allows controlling the instance by simple clicking on the status bar element.
*/
@injectable()
export class HostedPluginController implements FrontendApplicationContribution {

public static readonly HOSTED_PLUGIN = "hosted-plugin";
public static readonly HOSTED_PLUGIN_OFFLINE = "hosted-plugin-offline";
public static readonly HOSTED_PLUGIN_FAILED = "hosted-plugin-failed";

@inject(StatusBar)
protected readonly statusBar: StatusBar;

@inject(FrontendApplicationStateService)
protected readonly frontendApplicationStateService: FrontendApplicationStateService;

@inject(HostedPluginServer)
protected readonly hostedPluginServer: HostedPluginServer;

@inject(HostedPluginManagerClient)
protected readonly hostedPluginManagerClient: HostedPluginManagerClient;

@inject(ConnectionStatusService)
protected readonly connectionStatusService: ConnectionStatusService;

private pluginState: HostedPluginState = HostedPluginState.Stopped;

private entry: StatusBarEntry | undefined;

public initialize(): void {
this.hostedPluginServer.getHostedPlugin().then(pluginMetadata => {
if (!pluginMetadata) {
this.frontendApplicationStateService.reachedState('ready').then(() => {
this.hostedPluginManagerClient.onStateChanged(e => {
if (e === 'starting') {
this.onHostedPluginStarting();
} else if (e === 'running') {
this.onHostedPluginRunning();
} else if (e === 'stopped') {
this.onHostedPluginStopped();
} else if (e === 'failed') {
this.onHostedPluginFailed();
}
});

this.hostedPluginServer.isHostedTheiaRunning().then((running) => {
if (running) {
this.onHostedPluginRunning();
}
});
});

this.connectionStatusService.onStatusChange(() => this.onConnectionStatusChanged());
}
});
}

/**
* Display status bar element for stopped plugin.
*/
protected async onHostedPluginStopped(): Promise<void> {
this.pluginState = HostedPluginState.Stopped;

this.entry = {
text: `Hosted Plugin: Stopped $(angle-up)`,
alignment: StatusBarAlignment.LEFT,
priority: 100,
onclick: e => {
this.showMenu(e.clientX, e.clientY);
}
};

this.entry.className = HostedPluginController.HOSTED_PLUGIN;
await this.statusBar.setElement(HostedPluginController.HOSTED_PLUGIN, this.entry);
}

/**
* Display status bar element for starting plugin.
*/
protected async onHostedPluginStarting(): Promise<void> {
this.pluginState = HostedPluginState.Starting;

this.entry = {
text: `$(cog~spin) Hosted Plugin: Starting`,
alignment: StatusBarAlignment.LEFT,
priority: 100
};

this.entry.className = HostedPluginController.HOSTED_PLUGIN;
await this.statusBar.setElement(HostedPluginController.HOSTED_PLUGIN, this.entry);
}

/**
* Display status bar element for running plugin.
*/
protected async onHostedPluginRunning(): Promise<void> {
this.pluginState = HostedPluginState.Running;

this.entry = {
text: `$(cog~spin) Hosted Plugin: Running $(angle-up)`,
alignment: StatusBarAlignment.LEFT,
priority: 100,
onclick: e => {
this.showMenu(e.clientX, e.clientY);
}
};

this.entry.className = HostedPluginController.HOSTED_PLUGIN;
await this.statusBar.setElement(HostedPluginController.HOSTED_PLUGIN, this.entry);
}

/**
* Display status bar element for failed plugin.
*/
protected async onHostedPluginFailed(): Promise<void> {
this.pluginState = HostedPluginState.Failed;

this.entry = {
text: `Hosted Plugin: Stopped $(angle-up)`,
alignment: StatusBarAlignment.LEFT,
priority: 100,
onclick: e => {
this.showMenu(e.clientX, e.clientY);
}
};

this.entry.className = HostedPluginController.HOSTED_PLUGIN_FAILED;
await this.statusBar.setElement(HostedPluginController.HOSTED_PLUGIN, this.entry);
}

/**
* Updaing status bar element when changing connection status.
*/
private onConnectionStatusChanged(): void {
if (this.connectionStatusService.currentState.state === ConnectionState.OFFLINE) {
// Re-set the element only if it's visible on status bar
if (this.entry) {
const offlineElement = {
text: `Hosted Plugin: Stopped`,
alignment: StatusBarAlignment.LEFT,
priority: 100
};

this.entry.className = HostedPluginController.HOSTED_PLUGIN_OFFLINE;
this.statusBar.setElement(HostedPluginController.HOSTED_PLUGIN, offlineElement);
}
} else {
// ask state of hosted plugin when switching to Online
if (this.entry) {
this.hostedPluginServer.isHostedTheiaRunning().then((running) => {
if (running) {
this.onHostedPluginRunning();
} else {
this.onHostedPluginStopped();
}
});
}
}
}

/**
* Show menu containing actions to start/stop/restart hosted plugin.
*/
protected showMenu(x: number, y: number): void {
const commands = new CommandRegistry();
const menu = new Menu({
commands
});

if (this.pluginState === 'running') {
this.addCommandsForRunningPlugin(commands, menu);
} else if (this.pluginState === 'stopped' || this.pluginState === 'failed') {
this.addCommandsForStoppedPlugin(commands, menu);
}

menu.open(x, y);
}

/**
* Adds commands to the menu for running plugin.
*/
protected addCommandsForRunningPlugin(commands: CommandRegistry, menu: Menu): void {
commands.addCommand(HostedPluginCommands.STOP.id, {
label: 'Stop Instance',
icon: 'fa fa-stop',
execute: () => setTimeout(() => this.hostedPluginManagerClient.stop(), 100)
});

menu.addItem({
type: 'command',
command: HostedPluginCommands.STOP.id
});

commands.addCommand(HostedPluginCommands.RESTART.id, {
label: 'Restart Instance',
icon: 'fa fa-repeat',
execute: () => setTimeout(() => this.hostedPluginManagerClient.restart(), 100)
});

menu.addItem({
type: 'command',
command: HostedPluginCommands.RESTART.id
});
}

/**
* Adds command to the menu for stopped plugin.
*/
protected addCommandsForStoppedPlugin(commands: CommandRegistry, menu: Menu): void {
commands.addCommand(HostedPluginCommands.START.id, {
label: "Start Instance",
icon: 'fa fa-play',
execute: () => setTimeout(() => this.hostedPluginManagerClient.start(), 100)
});

menu.addItem({
type: 'command',
command: HostedPluginCommands.START.id
});
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
* 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 '../../../src/main/style/status-bar.css';

import { ContainerModule } from "inversify";
import { FrontendApplicationContribution, FrontendApplication, WidgetFactory, KeybindingContribution } from "@theia/core/lib/browser";
import { MaybePromise, CommandContribution, MenuContribution } from "@theia/core/lib/common";
Expand All @@ -19,6 +22,9 @@ import { ModalNotification } from './dialogs/modal-notification';
import { PluginWidget } from "./plugin-ext-widget";
import { PluginFrontendViewContribution } from "./plugin-frontend-view-contribution";

import { HostedPluginInformer } from "../../hosted/browser/hosted-plugin-informer";
import { HostedPluginController } from "./hosted-plugin-controller";

import '../../../src/main/browser/style/index.css';
import { PluginExtDeployCommandService } from "./plugin-ext-deploy-command";

Expand All @@ -30,6 +36,9 @@ export default new ContainerModule(bind => {
bind(HostedPluginWatcher).toSelf().inSingletonScope();
bind(HostedPluginManagerClient).toSelf().inSingletonScope();

bind(FrontendApplicationContribution).to(HostedPluginInformer).inSingletonScope();
bind(FrontendApplicationContribution).to(HostedPluginController).inSingletonScope();

bind(PluginApiFrontendContribution).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toDynamicValue(c => c.container.get(PluginApiFrontendContribution));
bind(CommandContribution).toDynamicValue(c => c.container.get(PluginApiFrontendContribution));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,16 @@ export class PluginApiFrontendContribution implements CommandContribution {
protected readonly pluginExtDeployCommandService: PluginExtDeployCommandService;

registerCommands(commands: CommandRegistry): void {
commands.registerCommand(HostedPluginCommands.RUN, {
commands.registerCommand(HostedPluginCommands.START, {
execute: () => this.hostedPluginManagerClient.start()
});
commands.registerCommand(HostedPluginCommands.TERMINATE, {
commands.registerCommand(HostedPluginCommands.STOP, {
execute: () => this.hostedPluginManagerClient.stop()
});
commands.registerCommand(HostedPluginCommands.RESTART, {
execute: () => this.hostedPluginManagerClient.restart()
});
commands.registerCommand(HostedPluginCommands.SELECT_PLUGIN_PATH, {
commands.registerCommand(HostedPluginCommands.SELECT_PATH, {
execute: () => this.hostedPluginManagerClient.selectPluginPath()
});

Expand Down
Loading

0 comments on commit e26a507

Please sign in to comment.