diff --git a/.gitignore b/.gitignore
index 889ca6a50f..ad1452dc8f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,6 +17,8 @@ maven-eclipse.xml
*.iws
.idea/
+.vscode/
+
# Compiled source #
###################
!*.com/
diff --git a/assembly/assembly-main/pom.xml b/assembly/assembly-main/pom.xml
index d5a1697a1c..8a707a4ec0 100644
--- a/assembly/assembly-main/pom.xml
+++ b/assembly/assembly-main/pom.xml
@@ -27,6 +27,11 @@
assembly-ide-war
war
+
+ org.eclipse.che
+ assembly-workspace-loader-war
+ war
+
org.eclipse.che
assembly-wsagent-server
diff --git a/assembly/assembly-main/src/assembly/assembly.xml b/assembly/assembly-main/src/assembly/assembly.xml
index 83f7672d5a..909d2db227 100644
--- a/assembly/assembly-main/src/assembly/assembly.xml
+++ b/assembly/assembly-main/src/assembly/assembly.xml
@@ -55,6 +55,15 @@
org.eclipse.che.dashboard:che-dashboard-war
+
+ false
+ false
+ tomcat/webapps
+ workspace-loader.war
+
+ org.eclipse.che:assembly-workspace-loader-war
+
+
false
false
diff --git a/assembly/assembly-workspace-loader-war/pom.xml b/assembly/assembly-workspace-loader-war/pom.xml
new file mode 100644
index 0000000000..647f2ac08d
--- /dev/null
+++ b/assembly/assembly-workspace-loader-war/pom.xml
@@ -0,0 +1,72 @@
+
+
+
+ 4.0.0
+
+ che-assembly-parent
+ org.eclipse.che
+ 6.2.0-SNAPSHOT
+
+ assembly-workspace-loader-war
+ war
+ Che Workspace Loader :: War Packaging
+ Packages Che Workspace Loader application as a Java web app
+ 2018
+
+
+ javax.servlet
+ javax.servlet-api
+ provided
+
+
+ org.eclipse.che.workspace.loader
+ che-workspace-loader
+ zip
+ runtime
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-war-plugin
+
+
+
+ org.eclipse.che.workspace.loader
+ che-workspace-loader
+ zip
+
+
+ /webapp/
+
+
+
+ org.apache.maven.plugins
+ maven-dependency-plugin
+
+
+ analyze
+
+
+
+ org.eclipse.che.workspace.loader:che-workspace-loader
+
+
+
+
+
+
+
+
diff --git a/assembly/assembly-workspace-loader-war/src/main/java/org/eclipse/che/WSLoaderController.java b/assembly/assembly-workspace-loader-war/src/main/java/org/eclipse/che/WSLoaderController.java
new file mode 100644
index 0000000000..ad99ccc801
--- /dev/null
+++ b/assembly/assembly-workspace-loader-war/src/main/java/org/eclipse/che/WSLoaderController.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2018-2018 Red Hat, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Red Hat, Inc. - initial API and implementation
+ */
+package org.eclipse.che;
+
+import java.io.IOException;
+import javax.servlet.RequestDispatcher;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Defines a controller that is serving the index.html page of a workspace loader.
+ *
+ * @author Florent Benoit
+ */
+public class WSLoaderController extends HttpServlet {
+
+ /** Use the default dispatcher to serve the resource. */
+ @Override
+ protected void doGet(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+
+ RequestDispatcher dispatcher = request.getRequestDispatcher("/loader/index.html");
+ dispatcher.forward(request, response);
+ }
+}
diff --git a/assembly/assembly-workspace-loader-war/src/main/webapp/WEB-INF/web.xml b/assembly/assembly-workspace-loader-war/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 0000000000..ad42c6bd56
--- /dev/null
+++ b/assembly/assembly-workspace-loader-war/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+ WSLoader
+ org.eclipse.che.WSLoaderController
+
+
+
+ WSLoader
+ /*
+
+
+
+ default
+ /loader/*
+
+
+
diff --git a/assembly/pom.xml b/assembly/pom.xml
index 34cf54527e..58c3e8ade5 100644
--- a/assembly/pom.xml
+++ b/assembly/pom.xml
@@ -29,6 +29,7 @@
assembly-wsagent-server
assembly-ide-war
assembly-wsmaster-war
+ assembly-workspace-loader-war
assembly-main
diff --git a/dashboard/gulp/proxy.js b/dashboard/gulp/proxy.js
index 4d83001332..b4619d46b8 100644
--- a/dashboard/gulp/proxy.js
+++ b/dashboard/gulp/proxy.js
@@ -22,7 +22,7 @@ var serverOptions = {
var options = minimist(process.argv.slice(2), serverOptions);
-var patterns = ['/api', '/ext', '/ws', '/datasource', '/java-ca', '/im', '/che', '/admin'];
+var patterns = ['/api', '/ext', '/ws', '/datasource', '/java-ca', '/im', '/che', '/admin', '/workspace-loader'];
var proxies = [];
@@ -36,6 +36,8 @@ patterns.forEach(function(pattern) {
proxyOptions.route = '/admin';
} else if (pattern === '/ext') {
proxyOptions.route = '/ext';
+ } else if (pattern === '/workspace-loader') {
+ proxyOptions.route = '/workspace-loader';
} else {
proxyOptions.route = '/api';
}
diff --git a/dashboard/src/app/ide/ide.service.ts b/dashboard/src/app/ide/ide.service.ts
index aaa85326d7..63090b06a2 100644
--- a/dashboard/src/app/ide/ide.service.ts
+++ b/dashboard/src/app/ide/ide.service.ts
@@ -141,11 +141,11 @@ class IdeSvc {
let inDevMode = this.userDashboardConfig.developmentMode;
let randVal = Math.floor((Math.random() * 1000000) + 1);
let appendUrl = '?uid=' + randVal;
-
let workspace = this.cheWorkspace.getWorkspaceById(workspaceId);
this.openedWorkspace = workspace;
- let ideUrlLink = workspace.links.ide;
+ let workspaceLoaderUrl = this.cheWorkspace.getWorkspaceLoaderUrl(workspace.namespace, workspace.config.name);
+ let ideUrlLink = workspaceLoaderUrl || workspace.links.ide;
if (this.ideAction != null) {
appendUrl = appendUrl + '&action=' + this.ideAction;
diff --git a/dashboard/src/components/api/che-organizations.factory.ts b/dashboard/src/components/api/che-organizations.factory.ts
index 3d46b13556..2d786983db 100644
--- a/dashboard/src/components/api/che-organizations.factory.ts
+++ b/dashboard/src/components/api/che-organizations.factory.ts
@@ -514,7 +514,7 @@ export class CheOrganization implements che.api.ICheOrganization {
return this.cheResourcesDistribution.fetchAvailableOrganizationResources(organization.id).then(() => {
let resource = this.cheResourcesDistribution.getOrganizationAvailableResourceByType(organization.id, this.resourceLimits.RAM);
if (resource.amount === -1) {
- return 'RAM is not limited'
+ return 'RAM is not limited';
}
return resource ? 'Available RAM: ' + (resource.amount / 1024) + 'GB' : null;
diff --git a/dashboard/src/components/api/test/che-http-backend.ts b/dashboard/src/components/api/test/che-http-backend.ts
index 572fc7a1b3..68926198d7 100644
--- a/dashboard/src/components/api/test/che-http-backend.ts
+++ b/dashboard/src/components/api/test/che-http-backend.ts
@@ -93,6 +93,7 @@ export class CheHttpBackend {
this.$httpBackend.when('GET', '/api/').respond(200, {rootResources: []});
this.$httpBackend.when('GET', '/api/keycloak/settings').respond(404);
+ this.$httpBackend.when('GET', '/workspace-loader/').respond(404);
// add the remote call
let workspaceReturn = [];
diff --git a/dashboard/src/components/api/workspace/che-workspace.factory.ts b/dashboard/src/components/api/workspace/che-workspace.factory.ts
index ebd165de2d..8724eb07e4 100644
--- a/dashboard/src/components/api/workspace/che-workspace.factory.ts
+++ b/dashboard/src/components/api/workspace/che-workspace.factory.ts
@@ -79,6 +79,7 @@ export class CheWorkspace {
private statusDefers: Object;
private workspaceSettings: any;
private jsonRpcApiLocation: string;
+ private workspaceLoaderUrl: string;
/**
* Map with instance of Observable by workspaceId.
*/
@@ -164,6 +165,8 @@ export class CheWorkspace {
cheBranding.unregisterCallback(CONTEXT_FETCHER_ID);
};
cheBranding.registerCallback(CONTEXT_FETCHER_ID, callback.bind(this));
+
+ this.checkWorkspaceLoader(userDashboardConfig.developmentMode, proxySettings);
}
/**
@@ -648,6 +651,10 @@ export class CheWorkspace {
return '/ide/' + namespace + '/' + workspaceName;
}
+ getWorkspaceLoaderUrl(namespace: string, workspaceName: string): string {
+ return this.workspaceLoaderUrl ? this.workspaceLoaderUrl + namespace + '/' + workspaceName : null;
+ }
+
/**
* Creates deferred object which will be resolved
* when workspace change it's status to given
@@ -797,4 +804,17 @@ export class CheWorkspace {
}
return wsUrl;
}
+
+ private checkWorkspaceLoader(devmode: boolean, proxySettings: string): void {
+ let url = '/workspace-loader/';
+
+ let promise = this.$http.get(url);
+ promise.then((response: {data: any}) => {
+ this.workspaceLoaderUrl = devmode ? proxySettings + url : url;
+ }, (error: any) => {
+ if (error.status !== 304) {
+ this.workspaceLoaderUrl = null;
+ }
+ });
+ }
}
diff --git a/ide/che-ide-gwt-app/src/main/resources/org/eclipse/che/ide/public/IDE.html b/ide/che-ide-gwt-app/src/main/resources/org/eclipse/che/ide/public/IDE.html
index 7a09f0237e..da07f27d38 100644
--- a/ide/che-ide-gwt-app/src/main/resources/org/eclipse/che/ide/public/IDE.html
+++ b/ide/che-ide-gwt-app/src/main/resources/org/eclipse/che/ide/public/IDE.html
@@ -104,6 +104,7 @@
script.onerror = script.onabort = function() {
console.error("Cannot load " + script.src);
+ Loader.startLoading();
};
document.head.appendChild(script);
@@ -135,10 +136,14 @@
* Show loader and load compilation-mapping.txt file to determine which IDE JavaScript file will be loaded
*/
this.startLoading = function() {
- setTimeout(function() {
+ setTimeout(() => {
document.getElementById("ide-loader").style.opacity = 1;
}, 1);
+ setTimeout(() => {
+ window.parent.postMessage("show-ide", "*");
+ }, 250);
+
var msg = "Cannot load compilation mappings";
try {
@@ -362,9 +367,8 @@
};
};
- setTimeout(function() {
+ setTimeout(() => {
Loader.loadKeycloakSettings();
- window.parent.postMessage("show-ide", "*");
}, 1);
diff --git a/pom.xml b/pom.xml
index ca2f61b6b0..d473705e66 100644
--- a/pom.xml
+++ b/pom.xml
@@ -36,6 +36,7 @@
ide/che-ide-full
ide/che-ide-gwt-app
dashboard
+ workspace-loader
assembly
selenium
@@ -89,6 +90,12 @@
assembly-main
${che.version}
+
+ org.eclipse.che
+ assembly-workspace-loader-war
+ ${che.version}
+ war
+
org.eclipse.che
assembly-wsagent-server
@@ -1876,6 +1883,12 @@
che-selenium-test
${che.version}
+
+ org.eclipse.che.workspace.loader
+ che-workspace-loader
+ ${che.version}
+ zip
+
org.eclipse.che.core
che-core-commons-test
diff --git a/workspace-loader/.dockerignore b/workspace-loader/.dockerignore
new file mode 100644
index 0000000000..b72069b30e
--- /dev/null
+++ b/workspace-loader/.dockerignore
@@ -0,0 +1,5 @@
+Dockerfile
+node_modules
+target
+dist
+package-lock.json
diff --git a/workspace-loader/.gitignore b/workspace-loader/.gitignore
new file mode 100644
index 0000000000..320c107b3e
--- /dev/null
+++ b/workspace-loader/.gitignore
@@ -0,0 +1,3 @@
+node_modules/
+dist/
+package-lock.json
diff --git a/workspace-loader/Dockerfile b/workspace-loader/Dockerfile
new file mode 100644
index 0000000000..38b3a78b1d
--- /dev/null
+++ b/workspace-loader/Dockerfile
@@ -0,0 +1,24 @@
+# Copyright (c) 2018-2018 Red Hat, Inc
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Eclipse Public License v1.0
+# which accompanies this distribution, and is available at
+# http://www.eclipse.org/legal/epl-v10.html
+
+# This is a Dockerfile allowing to build workspace loader by using a docker container.
+# Build step: $ docker build -t eclipse-che-workspace-loader .
+# It builds an archive file that can be used by doing later
+# $ docker run --rm eclipse-che-workspace-loader | tar -C target/ -zxf -
+FROM node:6.11.2
+
+COPY package.json /workspace-loader/
+RUN cd /workspace-loader && npm install
+
+COPY . /workspace-loader/
+
+RUN cd /workspace-loader && \
+ npm run build && \
+ npm run test && \
+ cd target && \
+ tar zcf /tmp/workspace-loader.tar.gz dist
+
+CMD zcat /tmp/workspace-loader.tar.gz
diff --git a/workspace-loader/assembly.xml b/workspace-loader/assembly.xml
new file mode 100644
index 0000000000..1625098dd6
--- /dev/null
+++ b/workspace-loader/assembly.xml
@@ -0,0 +1,24 @@
+
+
+ workspace-loader-zip
+
+ zip
+
+ false
+
+
+ ${project.basedir}/target/dist
+ /loader
+
+
+
diff --git a/workspace-loader/package.json b/workspace-loader/package.json
new file mode 100644
index 0000000000..808fa0d750
--- /dev/null
+++ b/workspace-loader/package.json
@@ -0,0 +1,44 @@
+{
+ "name": "che-workspace-loader",
+ "version": "1.0.0",
+ "description": "Eclipse Che Workspace Loader App",
+ "main": "index.js",
+ "scripts": {
+ "test": "jest",
+ "build": "webpack --config webpack.prod.js",
+ "start": "webpack-dev-server --open --config webpack.dev.js"
+ },
+ "author": "",
+ "license": "EPL-1.0",
+ "devDependencies": {
+ "@types/jest": "^22.1.3",
+ "clean-webpack-plugin": "^0.1.18",
+ "css-loader": "^0.28.9",
+ "extract-text-webpack-plugin": "^3.0.2",
+ "html-webpack-plugin": "^2.30.1",
+ "jest": "^22.4.2",
+ "style-loader": "^0.20.1",
+ "ts-loader": "^3.4.0",
+ "typescript": "^2.7.1",
+ "uglifyjs-webpack-plugin": "^1.1.8",
+ "webpack": "^3.10.0",
+ "webpack-dev-server": "^2.11.1",
+ "webpack-merge": "^4.1.1"
+ },
+ "dependencies": {},
+ "jest": {
+ "moduleFileExtensions": [
+ "ts",
+ "js"
+ ],
+ "transform": {
+ "^.+\\.(ts)$": "/test/preprocessor.js"
+ },
+ "moduleNameMapper": {
+ "\\.(css|less)$": "/test/mock.js"
+ },
+ "testMatch": [
+ "**/test/*.(ts)"
+ ]
+ }
+}
diff --git a/workspace-loader/pom.xml b/workspace-loader/pom.xml
new file mode 100644
index 0000000000..c757342792
--- /dev/null
+++ b/workspace-loader/pom.xml
@@ -0,0 +1,177 @@
+
+
+
+ 4.0.0
+
+ che-parent
+ org.eclipse.che
+ 6.2.0-SNAPSHOT
+ ../pom.xml
+
+ org.eclipse.che.workspace.loader
+ che-workspace-loader
+ 6.2.0-SNAPSHOT
+ pom
+ Che Workspace Loader :: Web App
+ 2018
+
+ workspace-loader
+
+
+ org.apache.maven.plugins
+ maven-assembly-plugin
+
+
+ make-assembly
+ package
+
+ single
+
+
+
+
+ posix
+ false
+ false
+
+ ${project.basedir}/assembly.xml
+
+ workspace-loader
+
+
+
+ maven-clean-plugin
+
+
+
+ ${basedir}/node_modules
+ false
+
+
+ ${basedir}/dist
+ false
+
+
+ ${basedir}
+
+ **/package-lock.json
+
+ false
+
+
+
+
+
+
+
+
+
+ docker
+
+ true
+
+
+
+
+ maven-antrun-plugin
+
+
+ build-image
+ generate-sources
+
+ run
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ unpack-docker-build
+ generate-sources
+
+ run
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ native
+
+
+
+ maven-antrun-plugin
+
+
+ build-workspace-loader
+ compile
+
+ run
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ test-workspace-loader
+ test
+
+ run
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/workspace-loader/src/custom.d.ts b/workspace-loader/src/custom.d.ts
new file mode 100644
index 0000000000..024950cf34
--- /dev/null
+++ b/workspace-loader/src/custom.d.ts
@@ -0,0 +1,126 @@
+/*
+ * Copyright (c) 2018-2018 Red Hat, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Red Hat, Inc. - initial API and implementation
+ */
+declare module 'che' {
+ export = che;
+}
+
+declare function require(string: string): any;
+
+declare namespace che {
+ export interface IWorkspace {
+ id?: string;
+ projects?: any;
+ links?: {
+ ide?: string
+ [rel: string]: string;
+ };
+ temporary?: boolean;
+ status?: string;
+ namespace?: string;
+ attributes?: IWorkspaceAttributes;
+ config: IWorkspaceConfig;
+ runtime?: IWorkspaceRuntime;
+ isLocked?: boolean;
+ usedResources?: string;
+ }
+
+ export interface IWorkspaceConfig {
+ name?: string;
+ defaultEnv?: string;
+ environments: {
+ [envName: string]: IWorkspaceEnvironment
+ };
+ projects?: Array;
+ commands?: Array;
+ }
+
+ export interface IWorkspaceEnvironment {
+ machines: {
+ [machineName: string]: IEnvironmentMachine
+ };
+ recipe: IRecipe;
+ }
+
+ export interface IRecipe {
+ id?: string;
+ content?: string;
+ location?: string;
+ contentType?: string;
+ type: string;
+ }
+
+ export interface IEnvironmentMachine {
+ installers?: string[];
+ attributes?: {
+ memoryLimitBytes?: string | number;
+ [attrName: string]: string | number;
+ };
+ servers?: {
+ [serverRef: string]: IEnvironmentMachineServer
+ };
+ volumes?: {
+ [volumeRef: string]: IEnvironmentMachineVolume
+ };
+ env?: { [envName: string]: string };
+ }
+
+ export interface IEnvironmentMachineServer {
+ port: string | number;
+ protocol: string;
+ path?: string;
+ properties?: any;
+ attributes?: {
+ [attrName: string]: string | number;
+ };
+ }
+
+ export interface IEnvironmentMachineVolume {
+ path: string;
+ }
+
+ export interface IWorkspaceAttributes {
+ created: number;
+ updated?: number;
+ stackId?: string;
+ errorMessage?: string;
+ [propName: string]: string | number;
+ }
+
+ export interface IWorkspaceRuntime {
+ activeEnv: string;
+ links: any[];
+ machines: {
+ [machineName: string]: IWorkspaceRuntimeMachine
+ };
+ owner: string;
+ warnings: IWorkspaceWarning[];
+ }
+
+ export interface IWorkspaceWarning {
+ code?: number;
+ message: string;
+ }
+
+ export interface IWorkspaceRuntimeMachine {
+ attributes: { [propName: string]: string };
+ servers: { [serverName: string]: IWorkspaceRuntimeMachineServer };
+ }
+
+ export interface IWorkspaceRuntimeMachineServer {
+ status: string;
+ port: string;
+ url: string;
+ ref: string;
+ protocol: string;
+ path: string;
+ attributes: { [propName: string]: string };
+ }
+}
diff --git a/workspace-loader/src/index.html b/workspace-loader/src/index.html
new file mode 100644
index 0000000000..2091fb14ad
--- /dev/null
+++ b/workspace-loader/src/index.html
@@ -0,0 +1,38 @@
+
+
+
+
+
+ <%= htmlWebpackPlugin.options.title %>
+ <% if(htmlWebpackPlugin.options.cssName) { %>
+
+ <% } %>
+
+
+
+
+
+
+
+
+
diff --git a/workspace-loader/src/index.ts b/workspace-loader/src/index.ts
new file mode 100644
index 0000000000..6434cc0478
--- /dev/null
+++ b/workspace-loader/src/index.ts
@@ -0,0 +1,247 @@
+/*
+ * Copyright (c) 2018-2018 Red Hat, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Red Hat, Inc. - initial API and implementation
+ */
+'use strict';
+
+require('./style.css');
+
+import { WebsocketClient } from './json-rpc/websocket-client';
+import { CheJsonRpcMasterApi } from './json-rpc/che-json-rpc-master-api';
+import { Loader } from './loader/loader'
+
+const WEBSOCKET_CONTEXT = '/api/websocket';
+
+export class WorkspaceLoader {
+
+ loader: Loader;
+ workspace: che.IWorkspace;
+ startAfterStopping = false;
+
+ constructor(loader: Loader) {
+ this.loader = loader;
+
+ /** Ask dashboard to show the IDE. */
+ window.parent.postMessage("show-ide", "*");
+ }
+
+ /**
+ * Loads the workspace.
+ */
+ load(): Promise {
+ let workspaceKey = this.getWorkspaceKey();
+
+ if (!workspaceKey || workspaceKey === "") {
+ console.error("Workspace is not defined");
+ return;
+ }
+
+ return this.getWorkspace(workspaceKey)
+ .then((workspace) => {
+ this.workspace = workspace;
+
+ if (this.hasPreconfiguredIDE()) {
+ return this.handleWorkspace();
+ } else {
+ return new Promise((resolve) => {
+ this.openURL(workspace.links.ide + this.getQueryString());
+ resolve();
+ });
+ }
+ })
+ .catch(err => {
+ console.error(err);
+ });
+ }
+
+ /**
+ * Determines whether the workspace has preconfigured IDE.
+ */
+ hasPreconfiguredIDE() : boolean {
+ if (this.workspace.config.defaultEnv && this.workspace.config.environments) {
+ let defaultEnvironment = this.workspace.config.defaultEnv;
+ let environment = this.workspace.config.environments[defaultEnvironment];
+
+ let machines = environment.machines;
+ for (let machineName in machines) {
+ let servers = machines[machineName].servers;
+ for (let serverName in servers) {
+ let attributes = servers[serverName].attributes;
+ if (attributes['type'] === 'ide') {
+ return true;
+ }
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns workspace key from current address or empty string when it is undefined.
+ */
+ getWorkspaceKey(): string {
+ let result: string = window.location.pathname.substr(1);
+ return result.substr(result.indexOf('/') + 1, result.length);
+ }
+
+ /**
+ * Returns base websocket URL.
+ */
+ websocketBaseURL(): string {
+ let wsProtocol = 'http:' === document.location.protocol ? 'ws' : 'wss';
+ return wsProtocol + '://' + document.location.host;
+ }
+
+ /**
+ * Returns query string.
+ */
+ getQueryString(): string {
+ return location.search;
+ }
+
+ /**
+ * Get workspace by ID.
+ *
+ * @param workspaceId workspace id
+ */
+ getWorkspace(workspaceId: string): Promise {
+ return new Promise((resolve, reject) => {
+ let request = new XMLHttpRequest();
+ request.open("GET", '/api/workspace/' + workspaceId);
+ request.send();
+ request.onreadystatechange = function () {
+ if (this.readyState !== 4) { return; }
+ if (this.status !== 200) {
+ reject(this.status ? this.statusText : "Unknown error");
+ return;
+ }
+ resolve(JSON.parse(this.responseText));
+ };
+ });
+ }
+
+ /**
+ * Start current workspace.
+ */
+ startWorkspace(): Promise {
+ return new Promise((resolve, reject) => {
+ let request = new XMLHttpRequest();
+ request.open("POST", `/api/workspace/${this.workspace.id}/runtime`);
+ request.send();
+ request.onreadystatechange = function () {
+ if (this.readyState !== 4) { return; }
+ if (this.status !== 200) {
+ reject(this.status ? this.statusText : "Unknown error");
+ return;
+ }
+ resolve(JSON.parse(this.responseText));
+ };
+ });
+ }
+
+ /**
+ * Handles workspace status.
+ */
+ handleWorkspace(): Promise {
+ if (this.workspace.status === 'RUNNING') {
+ this.openIDE();
+ return;
+ }
+
+ return this.subscribeWorkspaceEvents().then(() => {
+ if (this.workspace.status === 'STOPPED') {
+ this.startWorkspace();
+ } else if (this.workspace.status === 'STOPPING') {
+ this.startAfterStopping = true;
+ }
+ });
+ }
+
+ /**
+ * Shows environment outputs.
+ *
+ * @param message output message
+ */
+ onEnvironmentOutput(message) : void {
+ this.loader.log(message);
+ }
+
+ /**
+ * Handles changing of workspace status.
+ *
+ * @param status workspace status
+ */
+ onWorkspaceStatusChanged(status) : void {
+ if (status === 'RUNNING') {
+ this.openIDE();
+ } else if (status === 'STOPPED' && this.startAfterStopping) {
+ this.startWorkspace();
+ }
+ }
+
+ /**
+ * Subscribes to the workspace events.
+ */
+ subscribeWorkspaceEvents() : Promise {
+ let master = new CheJsonRpcMasterApi(new WebsocketClient());
+ return new Promise((resolve) => {
+ master.connect(this.websocketBaseURL() + WEBSOCKET_CONTEXT).then(() => {
+ master.subscribeEnvironmentOutput(this.workspace.id,
+ (message: any) => this.onEnvironmentOutput(message.text));
+
+ master.subscribeWorkspaceStatus(this.workspace.id,
+ (message: any) => this.onWorkspaceStatusChanged(message.status));
+
+ resolve();
+ });
+ });
+ }
+
+ /**
+ * Opens IDE for the workspace.
+ */
+ openIDE() : void {
+ this.getWorkspace(this.workspace.id).then((workspace) => {
+ let machines = workspace.runtime.machines;
+ for (let machineName in machines) {
+ let servers = machines[machineName].servers;
+ for (let serverId in servers) {
+ let attributes = servers[serverId].attributes;
+ if (attributes['type'] === 'ide') {
+ this.openURL(servers[serverId].url);
+ return;
+ }
+ }
+ }
+
+ this.openURL(workspace.links.ide);
+ });
+ }
+
+ /**
+ * Schedule opening URL.
+ * Scheduling prevents appearing an error net::ERR_CONNECTION_REFUSED instead opening the URL.
+ *
+ * @param url url to be opened
+ */
+ openURL(url) : void {
+ // Preconfigured IDE may use dedicated port. In this case Chrome browser fails
+ // with error net::ERR_CONNECTION_REFUSED. Timer helps to open the URL without errors.
+ setTimeout(() => {
+ window.location.href = url;
+ }, 1000);
+ }
+
+};
+
+/** Initialize */
+if (document.getElementById('workspace-console')) {
+ new WorkspaceLoader(new Loader()).load();
+}
diff --git a/workspace-loader/src/json-rpc/che-json-rpc-api-service.ts b/workspace-loader/src/json-rpc/che-json-rpc-api-service.ts
new file mode 100644
index 0000000000..98372c89ee
--- /dev/null
+++ b/workspace-loader/src/json-rpc/che-json-rpc-api-service.ts
@@ -0,0 +1,86 @@
+/*
+ * Copyright (c) 2018-2018 Red Hat, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Red Hat, Inc. - initial API and implementation
+ */
+'use strict';
+import {ICommunicationClient, JsonRpcClient} from './json-rpc-client';
+
+export class IChannel {
+ subscription: string;
+ unsubscription: string;
+ notification: string;
+}
+
+/**
+ * Class for basic CHE API communication methods.
+ *
+ * @author Ann Shumilova
+ */
+export class CheJsonRpcApiClient {
+ /**
+ * Client that implements JSON RPC protocol.
+ */
+ private jsonRpcClient: JsonRpcClient;
+ /**
+ * Communication client (can be http, websocket).
+ */
+ private client: ICommunicationClient;
+
+ constructor (client: ICommunicationClient) {
+ this.client = client;
+ this.jsonRpcClient = new JsonRpcClient(client);
+ }
+
+ /**
+ * Subscribe on the events from service.
+ *
+ * @param event event's name to subscribe
+ * @param notification notification name to handle
+ * @param handler event's handler
+ * @param params params (optional)
+ */
+ subscribe(event: string, notification: string, handler: Function, params?: any): void {
+ this.jsonRpcClient.addNotificationHandler(notification, handler);
+ this.jsonRpcClient.notify(event, params);
+ }
+
+ /**
+ * Unsubscribe concrete handler from events from service.
+ *
+ * @param event event's name to unsubscribe
+ * @param notification notification name binded to the event
+ * @param handler handler to be removed
+ * @param params params (optional)
+ */
+ unsubscribe(event: string, notification: string, handler: Function, params?: any): void {
+ this.jsonRpcClient.removeNotificationHandler(notification, handler);
+ this.jsonRpcClient.notify(event, params);
+ }
+
+ /**
+ * Connects to the pointed entrypoint
+ *
+ * @param entrypoint entrypoint to connect to
+ * @returns {Promise} promise
+ */
+ connect(entrypoint: string): Promise {
+ return this.client.connect(entrypoint);
+ }
+
+ /**
+ * Makes request.
+ *
+ * @param method
+ * @param params
+ * @returns {ng.IPromise}
+ */
+ request(method: string, params?: any): Promise {
+ return this.jsonRpcClient.request(method, params);
+ }
+}
diff --git a/workspace-loader/src/json-rpc/che-json-rpc-master-api.ts b/workspace-loader/src/json-rpc/che-json-rpc-master-api.ts
new file mode 100644
index 0000000000..9374e61efc
--- /dev/null
+++ b/workspace-loader/src/json-rpc/che-json-rpc-master-api.ts
@@ -0,0 +1,181 @@
+/*
+ * Copyright (c) 2018-2018 Red Hat, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Red Hat, Inc. - initial API and implementation
+ */
+'use strict';
+import {CheJsonRpcApiClient} from './che-json-rpc-api-service';
+import {ICommunicationClient} from './json-rpc-client';
+
+enum MasterChannels {
+ ENVIRONMENT_OUTPUT = 'machine/log',
+ ENVIRONMENT_STATUS = 'machine/statusChanged',
+ WS_AGENT_OUTPUT = 'installer/log',
+ WORKSPACE_STATUS = 'workspace/statusChanged'
+}
+const SUBSCRIBE: string = 'subscribe';
+const UNSUBSCRIBE: string = 'unsubscribe';
+
+/**
+ * Client API for workspace master interactions.
+ *
+ * @author Ann Shumilova
+ */
+export class CheJsonRpcMasterApi {
+ private cheJsonRpcApi: CheJsonRpcApiClient;
+ private clientId: string;
+
+ constructor (client: ICommunicationClient) {
+ this.cheJsonRpcApi = new CheJsonRpcApiClient(client);
+ }
+
+ /**
+ * Opens connection to pointed entrypoint.
+ *
+ * @param entrypoint
+ * @returns {IPromise>}
+ */
+ connect(entrypoint: string): Promise {
+ return this.cheJsonRpcApi.connect(entrypoint).then(() => {
+ return this.fetchClientId();
+ });
+ }
+
+ /**
+ * Subscribes the environment output.
+ *
+ * @param workspaceId workspace's id
+ * @param machineName machine's name
+ * @param callback callback to process event
+ */
+ subscribeEnvironmentOutput(workspaceId: string, callback: Function): void {
+ this.subscribe(MasterChannels.ENVIRONMENT_OUTPUT, workspaceId, callback);
+ }
+
+ /**
+ * Un-subscribes the pointed callback from the environment output.
+ *
+ * @param workspaceId workspace's id
+ * @param machineName machine's name
+ * @param callback callback to process event
+ */
+ unSubscribeEnvironmentOutput(workspaceId: string, callback: Function): void {
+ this.unsubscribe(MasterChannels.ENVIRONMENT_OUTPUT, workspaceId, callback);
+ }
+
+ /**
+ * Subscribes the environment status changed.
+ *
+ * @param workspaceId workspace's id
+ * @param callback callback to process event
+ */
+ subscribeEnvironmentStatus(workspaceId: string, callback: Function): void {
+ this.subscribe(MasterChannels.ENVIRONMENT_STATUS, workspaceId, callback);
+ }
+
+ /**
+ * Un-subscribes the pointed callback from environment status changed.
+ *
+ * @param workspaceId workspace's id
+ * @param callback callback to process event
+ */
+ unSubscribeEnvironmentStatus(workspaceId: string, callback: Function): void {
+ this.unsubscribe(MasterChannels.ENVIRONMENT_STATUS, workspaceId, callback);
+ }
+
+ /**
+ * Subscribes on workspace agent output.
+ *
+ * @param workspaceId workspace's id
+ * @param callback callback to process event
+ */
+ subscribeWsAgentOutput(workspaceId: string, callback: Function): void {
+ this.subscribe(MasterChannels.WS_AGENT_OUTPUT, workspaceId, callback);
+ }
+
+ /**
+ * Un-subscribes from workspace agent output.
+ *
+ * @param workspaceId workspace's id
+ * @param callback callback to process event
+ */
+ unSubscribeWsAgentOutput(workspaceId: string, callback: Function): void {
+ this.unsubscribe(MasterChannels.WS_AGENT_OUTPUT, workspaceId, callback);
+ }
+
+ /**
+ * Subscribes to workspace's status.
+ *
+ * @param workspaceId workspace's id
+ * @param callback callback to process event
+ */
+ subscribeWorkspaceStatus(workspaceId: string, callback: Function): void {
+ let statusHandler = (message: any) => {
+ if (workspaceId === message.workspaceId) {
+ callback(message);
+ }
+ };
+ this.subscribe(MasterChannels.WORKSPACE_STATUS, workspaceId, statusHandler);
+ }
+
+ /**
+ * Un-subscribes pointed callback from workspace's status.
+ *
+ * @param workspaceId
+ * @param callback
+ */
+ unSubscribeWorkspaceStatus(workspaceId: string, callback: Function): void {
+ this.unsubscribe(MasterChannels.WORKSPACE_STATUS, workspaceId, callback);
+ }
+
+ /**
+ * Fetch client's id and strores it.
+ *
+ * @returns {IPromise}
+ */
+ fetchClientId(): Promise {
+ return this.cheJsonRpcApi.request('websocketIdService/getId').then((data: any) => {
+ this.clientId = data[0];
+ });
+ }
+
+ /**
+ * Returns client's id.
+ *
+ * @returns {string} clinet connection identifier
+ */
+ getClientId(): string {
+ return this.clientId;
+ }
+
+ /**
+ * Performs subscribe to the pointed channel for pointed workspace's ID and callback.
+ *
+ * @param channel channel to un-subscribe
+ * @param workspaceId workspace's id
+ * @param callback callback
+ */
+ private subscribe(channel: MasterChannels, workspaceId: string, callback: Function): void {
+ let method: string = channel.toString();
+ let params = {method: method, scope: {workspaceId: workspaceId}};
+ this.cheJsonRpcApi.subscribe(SUBSCRIBE, method, callback, params);
+ }
+
+ /**
+ * Performs un-subscribe of the pointed channel by pointed workspace's ID and callback.
+ *
+ * @param channel channel to un-subscribe
+ * @param workspaceId workspace's id
+ * @param callback callback
+ */
+ private unsubscribe(channel: MasterChannels, workspaceId: string, callback: Function): void {
+ let method: string = channel.toString();
+ let params = {method: method, scope: {workspaceId: workspaceId}};
+ this.cheJsonRpcApi.unsubscribe(UNSUBSCRIBE, method, callback, params);
+ }
+}
diff --git a/workspace-loader/src/json-rpc/json-rpc-client.ts b/workspace-loader/src/json-rpc/json-rpc-client.ts
new file mode 100644
index 0000000000..8635c3271d
--- /dev/null
+++ b/workspace-loader/src/json-rpc/json-rpc-client.ts
@@ -0,0 +1,198 @@
+/*
+ * Copyright (c) 2018-2018 Red Hat, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Red Hat, Inc. - initial API and implementation
+ */
+'use strict';
+import { IDeffered, Deffered } from './util';
+const JSON_RPC_VERSION: string = '2.0';
+
+/**
+ * Interface for communication between two entrypoints.
+ * The implementation can be through websocket or http protocol.
+ */
+export interface ICommunicationClient {
+ /**
+ * Process responses.
+ */
+ onResponse: Function;
+ /**
+ * Performs connections.
+ *
+ * @param entrypoint
+ */
+ connect(entrypoint: string): Promise;
+ /**
+ * Close the connection.
+ */
+ disconnect(): void;
+ /**
+ * Send pointed data.
+ *
+ * @param data data to be sent
+ */
+ send(data: any): void;
+}
+
+interface IRequest {
+ jsonrpc: string;
+ id: string;
+ method: string;
+ params: any;
+}
+
+interface INotification {
+ jsonrpc: string;
+ method: string;
+ params: any;
+}
+
+/**
+ * This client is handling the JSON RPC requests, responses and notifications.
+ *
+ * @author Ann Shumilova
+ */
+export class JsonRpcClient {
+ /**
+ * Client for performing communications.
+ */
+ private client: ICommunicationClient;
+ /**
+ * The list of the pending requests by request id.
+ */
+ private pendingRequests: Map>;
+ /**
+ * The list of notification handlers by method name.
+ */
+ private notificationHandlers: Map>;
+ private counter: number = 100;
+
+ constructor(client: ICommunicationClient) {
+ this.client = client;
+ this.pendingRequests = new Map>();
+ this.notificationHandlers = new Map>();
+
+ this.client.onResponse = (message: any): void => {
+ this.processResponse(message);
+ };
+ }
+
+ /**
+ * Performs JSON RPC request.
+ *
+ * @param method method's name
+ * @param params params
+ * @returns {IPromise}
+ */
+ request(method: string, params?: any): Promise {
+ let deferred = new Deffered();
+ let id: string = (this.counter++).toString();
+ this.pendingRequests.set(id, deferred);
+
+ let request: IRequest = {
+ jsonrpc: JSON_RPC_VERSION,
+ id: id,
+ method: method,
+ params: params
+ };
+
+ this.client.send(request);
+ return deferred.promise;
+ }
+
+ /**
+ * Sends JSON RPC notification.
+ *
+ * @param method method's name
+ * @param params params (optional)
+ */
+ notify(method: string, params?: any): void {
+ let request: INotification = {
+ jsonrpc: JSON_RPC_VERSION,
+ method: method,
+ params: params
+ };
+
+ this.client.send(request);
+ }
+
+ /**
+ * Adds notification handler.
+ *
+ * @param method method's name
+ * @param handler handler to process notification
+ */
+ public addNotificationHandler(method: string, handler: Function): void {
+ let handlers = this.notificationHandlers.get(method);
+
+ if (handlers) {
+ handlers.push(handler);
+ } else {
+ handlers = [handler];
+ this.notificationHandlers.set(method, handlers);
+ }
+ }
+
+ /**
+ * Removes notification handler.
+ *
+ * @param method method's name
+ * @param handler handler
+ */
+ public removeNotificationHandler(method: string, handler: Function): void {
+ let handlers = this.notificationHandlers.get(method);
+
+ if (handlers) {
+ handlers.splice(handlers.indexOf(handler), 1);
+ }
+ }
+
+ /**
+ * Processes response - detects whether it is JSON RPC response or notification.
+ *
+ * @param message
+ */
+ private processResponse(message: any): void {
+ if (message.id && this.pendingRequests.has(message.id)) {
+ this.processResponseMessage(message);
+ } else {
+ this.processNotification(message);
+ }
+ }
+
+ /**
+ * Processes JSON RPC notification.
+ *
+ * @param message message
+ */
+ private processNotification(message: any): void {
+ let method = message.method;
+ let handlers = this.notificationHandlers.get(method);
+ if (handlers && handlers.length > 0) {
+ handlers.forEach((handler: Function) => {
+ handler(message.params);
+ });
+ }
+ }
+
+ /**
+ * Process JSON RPC response.
+ *
+ * @param message
+ */
+ private processResponseMessage(message: any): void {
+ let promise = this.pendingRequests.get(message.id);
+ if (message.result) {
+ promise.resolve(message.result);
+ return;
+ }
+ if (message.error) {
+ promise.reject(message.error);
+ }
+ }
+}
diff --git a/workspace-loader/src/json-rpc/util.ts b/workspace-loader/src/json-rpc/util.ts
new file mode 100644
index 0000000000..70bba9788e
--- /dev/null
+++ b/workspace-loader/src/json-rpc/util.ts
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2018-2018 Red Hat, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Red Hat, Inc. - initial API and implementation
+ */
+export interface IDeffered {
+ resolve(value?: T): void;
+ reject(reason?: any): void;
+ promise: Promise;
+}
+
+export class Deffered implements IDeffered {
+
+ promise: Promise;
+ private resolveF;
+ private rejectF;
+ constructor() {
+ this.promise = new Promise((resolve, reject) => {
+ this.resolve = resolve;
+ this.reject = reject;
+ });
+ }
+ resolve(value?: T): void {
+ this.resolveF(value);
+ }
+ reject(reason?: any): void {
+ this.rejectF(reason);
+ }
+}
diff --git a/workspace-loader/src/json-rpc/websocket-client.ts b/workspace-loader/src/json-rpc/websocket-client.ts
new file mode 100644
index 0000000000..7581f159f9
--- /dev/null
+++ b/workspace-loader/src/json-rpc/websocket-client.ts
@@ -0,0 +1,67 @@
+/*
+ * Copyright (c) 2018-2018 Red Hat, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Red Hat, Inc. - initial API and implementation
+ */
+'use strict';
+import { ICommunicationClient } from './json-rpc-client';
+
+/**
+ * The implementation for JSON RPC protocol communication through websocket.
+ *
+ * @author Ann Shumilova
+ */
+export class WebsocketClient implements ICommunicationClient {
+ onResponse: Function;
+ private websocketStream: WebSocket;
+
+ constructor() {
+
+ }
+
+ /**
+ * Performs connection to the pointed entrypoint.
+ *
+ * @param entrypoint the entrypoint to connect to
+ */
+ connect(entrypoint: string): Promise {
+ return new Promise((resolve, reject) => {
+ this.websocketStream = new WebSocket(entrypoint);
+ this.websocketStream.addEventListener("open", () => {
+ resolve();
+ });
+
+ this.websocketStream.addEventListener("error", () => {
+ reject();
+ });
+ this.websocketStream.addEventListener("message", (message) => {
+ let data = JSON.parse(message.data);
+ this.onResponse(data);
+ });
+ });
+
+ }
+
+ /**
+ * Performs closing the connection.
+ */
+ disconnect(): void {
+ if (this.websocketStream) {
+ this.websocketStream.close();
+ }
+ }
+
+ /**
+ * Sends pointed data.
+ *
+ * @param data to be sent
+ */
+ send(data: any): void {
+ this.websocketStream.send(JSON.stringify(data));
+ }
+}
diff --git a/workspace-loader/src/loader/loader.ts b/workspace-loader/src/loader/loader.ts
new file mode 100644
index 0000000000..8f40c82da5
--- /dev/null
+++ b/workspace-loader/src/loader/loader.ts
@@ -0,0 +1,53 @@
+/*
+ * Copyright (c) 2018-2018 Red Hat, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Red Hat, Inc. - initial API and implementation
+ */
+'use strict';
+
+export class Loader {
+
+ /**
+ * Initializes the Loader.
+ */
+ constructor() {
+ /** Show the loader */
+ setTimeout(() => {
+ document.getElementById('workspace-loader').style.display = "block";
+ setTimeout(() => {
+ document.getElementById('workspace-loader').style.opacity = "1";
+ }, 1);
+ }, 1);
+
+ /** Add click handler to maximize output */
+ document.getElementById('workspace-console').onclick = () => this.onclick();
+ }
+
+ /**
+ * Adds a message to output console.
+ *
+ * @param message message to log
+ */
+ log(message: string): void {
+ let element = document.createElement("pre");
+ element.innerHTML = message;
+ document.getElementById("workspace-console-container").appendChild(element);
+ element.scrollIntoView();
+ }
+
+ onclick(): void {
+ if (document.getElementById('workspace-loader').hasAttribute("max")) {
+ document.getElementById('workspace-loader').removeAttribute("max");
+ document.getElementById('workspace-console').removeAttribute("max");
+ } else {
+ document.getElementById('workspace-loader').setAttribute("max", "");
+ document.getElementById('workspace-console').setAttribute("max", "");
+ }
+ }
+
+}
diff --git a/workspace-loader/src/style.css b/workspace-loader/src/style.css
new file mode 100644
index 0000000000..81f994013e
--- /dev/null
+++ b/workspace-loader/src/style.css
@@ -0,0 +1,195 @@
+/**
+ * Copyright (c) 2018-2018 Red Hat, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Red Hat, Inc. - initial API and implementation
+ */
+
+#workspace-loader {
+ position: fixed;
+ width: 300px;
+ height: 45px;
+ left: 50%;
+ top: 30%;
+ margin-left: -150px;
+ opacity: 0;
+ display: none;
+ transition: all 0.2s ease-in-out;
+}
+
+#workspace-loader-label {
+ position: absolute;
+ left: 0px;
+ top: 0px;
+ width: 300px;
+ height: 30px;
+ font-family: sans-serif;
+ font-size: 14px;
+ line-height: 30px;
+ text-align: center;
+ color: #a0a9b7;
+}
+
+#workspace-loader-progress {
+ position: absolute;
+ width: 300px;
+ height: 15px;
+ left: 0px;
+ bottom: 0px;
+ background-color: #202325;
+ border: 1px solid #456594;
+ box-sizing: border-box;
+}
+
+#workspace-loader-progress>div {
+ position: absolute;
+ left: 1px;
+ right: 1px;
+ top: 1px;
+ bottom: 1px;
+ overflow: hidden;
+}
+
+#workspace-loader-progress-bar {
+ box-sizing: border-box;
+ height: 100%;
+ width: 0%;
+ background-color: #498fe1;
+ transition: all 0.2s linear;
+ animation-name: dancing;
+ animation-duration: 3s;
+ animation-delay: 1s;
+ animation-timing-function: linear;
+ animation-iteration-count: infinite;
+}
+
+#workspace-console {
+ position: fixed;
+ left: 30px;
+ right: 30px;
+ bottom: 25px;
+ height: 30%;
+ background-color: transparent;
+ overflow: auto;
+ color: #e6e6e6;
+ left: 2px;
+ right: 2px;
+ bottom: 2px;
+ cursor: pointer;
+ transition: all 0.2s ease-in-out;
+}
+
+#workspace-console>div {
+ position: relative;
+ width: 100%;
+}
+
+#workspace-console pre {
+ font-family: "Droid Sans Mono", monospace;
+ font-size: 9pt;
+ line-height: 13px;
+ padding: 0;
+ margin: 0;
+ white-space: pre-wrap;
+ word-wrap: break-word;
+ cursor: text;
+ cursor: pointer;
+}
+
+#workspace-loader[max] {
+ top: 5%;
+}
+
+#workspace-console[max] {
+ height: calc(100% - 110px);
+}
+
+@-webkit-keyframes dancing {
+ 0% {
+ width: 0%;
+ margin-left: 0%;
+ }
+ 30% {
+ width: 30%;
+ margin-left: 0%;
+ }
+ 70% {
+ width: 30%;
+ margin-left: 70%;
+ }
+ 100% {
+ width: 0%;
+ margin-left: 100%;
+ }
+}
+
+@keyframes dancing {
+ 0% {
+ width: 0%;
+ margin-left: 0%;
+ }
+ 6% {
+ width: 30%;
+ margin-left: 0%;
+ }
+ 14% {
+ width: 30%;
+ margin-left: 70%;
+ }
+ 20% {
+ width: 0%;
+ margin-left: 100%;
+ }
+ 100% {
+ width: 0%;
+ margin-left: 100%;
+ }
+}
+
+/********************************************************************************************
+ *
+ * Styled scroll bars
+ *
+ ********************************************************************************************/
+
+::-webkit-scrollbar {
+ width: 7px;
+ height: 7px;
+}
+
+::-webkit-scrollbar-button {
+ width: 0px;
+ height: 0px;
+ display: none;
+}
+
+::-webkit-scrollbar-corner {
+ background-color: transparent;
+ display: none;
+}
+
+::-webkit-scrollbar-track, ::-webkit-scrollbar-track:hover, ::-webkit-scrollbar-track:active {
+ background-color: transparent;
+ -webkit-box-shadow: none;
+ box-shadow: none;
+ border: none;
+}
+
+::-webkit-scrollbar-thumb {
+ background-clip: padding-box;
+ background-color: rgba(215, 215, 215, 0.10);
+ ;
+ border: 2px solid transparent;
+ border-radius: 4px;
+ -webkit-box-shadow: inset 0 0 2px rgba(235, 235, 235, 0.3);
+ box-shadow: inset 0 0 2px rgba(235, 235, 235, 0.3);
+ min-height: 32px;
+}
+
+::-webkit-scrollbar-thumb:hover {
+ background-color: rgba(215, 215, 215, 0.3);
+}
diff --git a/workspace-loader/test/mock.js b/workspace-loader/test/mock.js
new file mode 100644
index 0000000000..3d983335ad
--- /dev/null
+++ b/workspace-loader/test/mock.js
@@ -0,0 +1,13 @@
+/*
+ * Copyright (c) 2018-2018 Red Hat, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Red Hat, Inc. - initial API and implementation
+ */
+'use strict';
+
+module.exports = {};
diff --git a/workspace-loader/test/preprocessor.js b/workspace-loader/test/preprocessor.js
new file mode 100644
index 0000000000..a222b31e11
--- /dev/null
+++ b/workspace-loader/test/preprocessor.js
@@ -0,0 +1,18 @@
+/*
+ * Copyright (c) 2018-2018 Red Hat, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Red Hat, Inc. - initial API and implementation
+ */
+
+const compiler = require('typescript');
+
+module.exports = {
+ process(source, path) {
+ return compiler.transpile(source, {}, path, []);
+ }
+};
diff --git a/workspace-loader/test/test.spec.ts b/workspace-loader/test/test.spec.ts
new file mode 100644
index 0000000000..fbfa3b50e0
--- /dev/null
+++ b/workspace-loader/test/test.spec.ts
@@ -0,0 +1,336 @@
+/*
+ * Copyright (c) 2018-2018 Red Hat, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Red Hat, Inc. - initial API and implementation
+ */
+
+ ///
+
+'use strict';
+
+import {WorkspaceLoader} from '../src/index';
+import { Loader } from '../src/loader/loader';
+
+describe('Workspace Loader', () => {
+
+ let fakeWorkspaceConfig: che.IWorkspace;
+
+ beforeEach(function() {
+ document.body.innerHTML = `
+ `;
+
+ fakeWorkspaceConfig = {
+ status: 'STOPPED',
+ links: {
+ ide: "test url"
+ },
+ config: {
+ defaultEnv: "default",
+ "environments": {
+ "default": {
+ machines: {
+ machine: {
+ servers: {
+ server1: {
+ attributes: {
+ type: "ide"
+ },
+ port: 0,
+ protocol: ""
+ }
+ }
+ },
+ },
+ recipe: {
+ type: ""
+ }
+ }
+ }
+ }
+ } as che.IWorkspace;
+ });
+
+ it('must have "workspace-loader" in DOM', () => {
+ const loader = document.getElementById('workspace-loader');
+ expect(loader).toBeTruthy();
+ });
+
+ it('test when workspace key is not specified', () => {
+ let loader = new Loader();
+ let workspaceLoader = new WorkspaceLoader(loader);
+
+ spyOn(workspaceLoader, 'getWorkspaceKey');
+ spyOn(workspaceLoader, 'getWorkspace');
+
+ workspaceLoader.load();
+
+ expect(workspaceLoader.getWorkspaceKey).toHaveBeenCalled();
+ expect(workspaceLoader.getWorkspace).not.toHaveBeenCalled();
+ });
+
+ it('test getWorkspace with test value', () => {
+ let loader = new Loader();
+ let workspaceLoader = new WorkspaceLoader(loader);
+
+ spyOn(workspaceLoader, 'getWorkspaceKey').and.returnValue("foo/bar");
+
+ spyOn(workspaceLoader, 'getWorkspace').and.callFake(() => {
+ return new Promise((resolve) => {
+ resolve(fakeWorkspaceConfig);
+ });
+ });
+
+ workspaceLoader.load();
+
+ expect(workspaceLoader.getWorkspaceKey).toHaveBeenCalled();
+ expect(workspaceLoader.getWorkspace).toHaveBeenCalledWith("foo/bar");
+ });
+
+ describe('must open IDE directly when workspace does not have IDE server', () => {
+ let workspaceLoader;
+
+ beforeEach((done) => {
+ let loader = new Loader();
+ workspaceLoader = new WorkspaceLoader(loader);
+
+ spyOn(workspaceLoader, 'getWorkspaceKey').and.returnValue("foo/bar");
+ spyOn(workspaceLoader, 'getQueryString').and.returnValue("");
+
+ spyOn(workspaceLoader, 'getWorkspace').and.callFake(() => {
+ return new Promise((resolve) => {
+ fakeWorkspaceConfig.config.environments["default"].machines = {};
+ resolve(fakeWorkspaceConfig);
+ });
+ });
+
+ spyOn(workspaceLoader, "handleWorkspace");
+
+ spyOn(workspaceLoader, "openURL").and.callFake(() => {
+ done();
+ });
+
+ workspaceLoader.load();
+ });
+
+ it('basic workspace function must be called', () => {
+ expect(workspaceLoader.getWorkspaceKey).toHaveBeenCalled();
+ expect(workspaceLoader.getWorkspace).toHaveBeenCalledWith("foo/bar");
+ });
+
+ it('handleWorkspace must not be called', () => {
+ expect(workspaceLoader.handleWorkspace).not.toHaveBeenCalled();
+ });
+
+ it('must open IDE with `test url`', () => {
+ expect(workspaceLoader.openURL).toHaveBeenCalledWith("test url");
+ });
+ });
+
+ describe('must open default IDE with query parameters when workspace does not have IDE server', () => {
+ let workspaceLoader;
+
+ beforeEach((done) => {
+ let loader = new Loader();
+ workspaceLoader = new WorkspaceLoader(loader);
+
+ spyOn(workspaceLoader, 'getWorkspaceKey').and.returnValue("foo/bar");
+ spyOn(workspaceLoader, 'getQueryString').and.returnValue("?param=value");
+
+ spyOn(workspaceLoader, 'getWorkspace').and.callFake(() => {
+ return new Promise((resolve) => {
+ fakeWorkspaceConfig.config.environments["default"].machines = {};
+ resolve(fakeWorkspaceConfig);
+ });
+ });
+
+ spyOn(workspaceLoader, "handleWorkspace");
+
+ spyOn(workspaceLoader, "openURL").and.callFake(() => {
+ done();
+ });
+
+ workspaceLoader.load();
+ });
+
+ it('must open IDE with `test url` and query param `param=value`', () => {
+ expect(workspaceLoader.openURL).toHaveBeenCalledWith("test url?param=value");
+ });
+ });
+
+ describe('must handle workspace when it has IDE server', () => {
+ let workspaceLoader;
+
+ beforeEach((done) => {
+ let loader = new Loader();
+ workspaceLoader = new WorkspaceLoader(loader);
+
+ spyOn(workspaceLoader, 'getWorkspaceKey').and.returnValue("foo/bar");
+
+ spyOn(workspaceLoader, 'getWorkspace').and.callFake(() => {
+ return new Promise((resolve) => {
+ resolve(fakeWorkspaceConfig);
+ });
+ });
+
+ spyOn(workspaceLoader, "handleWorkspace").and.callFake(() => {
+ done();
+ });
+
+ workspaceLoader.load();
+ });
+
+ it('basic workspace function must be called', () => {
+ expect(workspaceLoader.getWorkspaceKey).toHaveBeenCalled();
+ expect(workspaceLoader.getWorkspace).toHaveBeenCalledWith("foo/bar");
+ });
+
+ it('must be called', () => {
+ expect(workspaceLoader.handleWorkspace).toHaveBeenCalled();
+ });
+ });
+
+ describe('must open IDE for RUNNING workspace', () => {
+ let workspaceLoader;
+
+ beforeEach((done) => {
+ let loader = new Loader();
+ workspaceLoader = new WorkspaceLoader(loader);
+
+ spyOn(workspaceLoader, 'getWorkspaceKey').and.returnValue("foo/bar");
+
+ spyOn(workspaceLoader, 'getWorkspace').and.callFake(() => {
+ return new Promise((resolve) => {
+ fakeWorkspaceConfig.status = 'RUNNING';
+ resolve(fakeWorkspaceConfig);
+ });
+ });
+
+ spyOn(workspaceLoader, "subscribeWorkspaceEvents");
+
+ spyOn(workspaceLoader, "openIDE").and.callFake(() => {
+ done();
+ });
+
+ workspaceLoader.load();
+ });
+
+ it('must not subscribe to events', () => {
+ expect(workspaceLoader.subscribeWorkspaceEvents).not.toHaveBeenCalled();
+ });
+
+ it('must open IDE immediately', () => {
+ expect(workspaceLoader.openIDE).toHaveBeenCalled();
+ });
+ });
+
+ describe('> must start STOPPED workspace', () => {
+ let workspaceLoader;
+
+ beforeEach((done) => {
+ let loader = new Loader();
+ workspaceLoader = new WorkspaceLoader(loader);
+
+ spyOn(workspaceLoader, 'getWorkspaceKey').and.returnValue("foo/bar");
+
+ spyOn(workspaceLoader, 'getWorkspace').and.callFake(() => {
+ return new Promise((resolve) => {
+ fakeWorkspaceConfig.status = 'STOPPED';
+ resolve(fakeWorkspaceConfig);
+ });
+ });
+
+ spyOn(workspaceLoader, "subscribeWorkspaceEvents").and.callFake(() => {
+ return new Promise((resolve) => {
+ resolve();
+ });
+ });
+
+ spyOn(workspaceLoader, "startWorkspace").and.callFake(() => {
+ done();
+ });
+
+ spyOn(workspaceLoader, "openIDE");
+
+ workspaceLoader.load();
+ });
+
+ it('openIDE must not be called if status is STOPPED', () => {
+ expect(workspaceLoader.openIDE).not.toHaveBeenCalled();
+ });
+
+ it('must subscribe to events', () => {
+ expect(workspaceLoader.subscribeWorkspaceEvents).toHaveBeenCalled();
+ });
+
+ it('must start the workspace', () => {
+ expect(workspaceLoader.startWorkspace).toHaveBeenCalled();
+ });
+
+ it('openIDE must be called when workspace become RUNNING', () => {
+ workspaceLoader.onWorkspaceStatusChanged("RUNNING");
+ expect(workspaceLoader.openIDE).toHaveBeenCalled();
+ });
+ });
+
+ describe('must restart STOPPING workspace', () => {
+ let workspaceLoader;
+
+ beforeEach((done) => {
+ let loader = new Loader();
+ workspaceLoader = new WorkspaceLoader(loader);
+
+ spyOn(workspaceLoader, 'getWorkspaceKey').and.returnValue("foo/bar");
+
+ spyOn(workspaceLoader, 'getWorkspace').and.callFake(() => {
+ return new Promise((resolve) => {
+ fakeWorkspaceConfig.status = 'STOPPING';
+ resolve(fakeWorkspaceConfig);
+ });
+ });
+
+ spyOn(workspaceLoader, "subscribeWorkspaceEvents").and.callFake(() => {
+ return new Promise((resolve) => {
+ resolve();
+ });
+ });
+
+ spyOn(workspaceLoader, "startWorkspace");
+ spyOn(workspaceLoader, "openIDE");
+
+ workspaceLoader.load().then(() => {
+ done();
+ });
+ });
+
+ it('must start the workspace after stopping', () => {
+ expect(workspaceLoader.startAfterStopping).toEqual(true);
+ });
+
+
+ it('must start workspace when workspace status become STOPPED', () => {
+ workspaceLoader.onWorkspaceStatusChanged("STOPPED");
+ expect(workspaceLoader.startWorkspace).toHaveBeenCalled();
+ expect(workspaceLoader.openIDE).not.toHaveBeenCalled();
+ });
+
+ it('must open IDE when workspace become RUNNING', () => {
+ workspaceLoader.onWorkspaceStatusChanged("RUNNING");
+ expect(workspaceLoader.openIDE).toHaveBeenCalled();
+ });
+ });
+
+});
diff --git a/workspace-loader/tsconfig.json b/workspace-loader/tsconfig.json
new file mode 100644
index 0000000000..6456e18db2
--- /dev/null
+++ b/workspace-loader/tsconfig.json
@@ -0,0 +1,14 @@
+{
+ "include": [
+ "src"
+ ],
+ "compilerOptions": {
+ "target": "es5",
+ "module": "commonjs",
+ "lib": ["dom","es6"],
+ "sourceMap": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+ }
+}
diff --git a/workspace-loader/tslint.json b/workspace-loader/tslint.json
new file mode 100644
index 0000000000..2b8cd8612c
--- /dev/null
+++ b/workspace-loader/tslint.json
@@ -0,0 +1,67 @@
+{
+ "defaultSeverity": "error",
+ "rules": {
+ "file-header": [
+ true,
+ "[\n\r]+ \\* Copyright \\(c\\) \\d{4}(-\\d{4})? .*[\n\r]+"
+ ],
+ "class-name": true,
+ "comment-format": [
+ true,
+ "check-space"
+ ],
+ "curly": true,
+ "eofline": true,
+ "forin": true,
+ "indent": [
+ true,
+ "spaces"
+ ],
+ "max-line-length": [
+ true,
+ 180
+ ],
+ "no-consecutive-blank-lines": true,
+ "no-trailing-whitespace": true,
+ "no-var-keyword": true,
+ "one-line": [
+ true,
+ "check-open-brace",
+ "check-catch",
+ "check-else",
+ "check-whitespace"
+ ],
+ "radix": true,
+ "semicolon": [
+ true,
+ "always",
+ "ignore-interfaces"
+ ],
+ "trailing-comma": [
+ false
+ ],
+ "triple-equals": [
+ true,
+ "allow-null-check"
+ ],
+ "typedef-whitespace": [
+ true,
+ {
+ "call-signature": "nospace",
+ "index-signature": "nospace",
+ "parameter": "nospace",
+ "property-declaration": "nospace",
+ "variable-declaration": "nospace"
+ }
+ ],
+ "variable-name": false,
+ "whitespace": [
+ true,
+ "check-branch",
+ "check-decl",
+ "check-operator",
+ "check-separator",
+ "check-type"
+ ]
+ }
+}
diff --git a/workspace-loader/webpack.common.js b/workspace-loader/webpack.common.js
new file mode 100644
index 0000000000..106b5e3642
--- /dev/null
+++ b/workspace-loader/webpack.common.js
@@ -0,0 +1,33 @@
+/*******************************************************************************
+ * Copyright (c) 2018-2018 Red Hat, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Red Hat, Inc. - initial API and implementation
+ *******************************************************************************/
+
+const path = require('path');
+const CleanWebpackPlugin = require('clean-webpack-plugin');
+
+module.exports = {
+ entry: './src/index.ts',
+ module: {
+ rules: [
+ {
+ test: /\.ts$/,
+ use: 'ts-loader',
+ exclude: /node_modules/
+ },
+ ]
+ },
+ resolve: {
+ extensions: ['.ts', '.js']
+ },
+ output: {
+ filename: 'bundle.js',
+ path: path.resolve(__dirname, 'target/dist')
+ }
+};
diff --git a/workspace-loader/webpack.dev.js b/workspace-loader/webpack.dev.js
new file mode 100644
index 0000000000..132b962e64
--- /dev/null
+++ b/workspace-loader/webpack.dev.js
@@ -0,0 +1,51 @@
+/*******************************************************************************
+ * Copyright (c) 2018-2018 Red Hat, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Red Hat, Inc. - initial API and implementation
+ *******************************************************************************/
+
+const merge = require('webpack-merge');
+const common = require('./webpack.common.js');
+const HtmlWebpackPlugin = require('html-webpack-plugin');
+
+module.exports = merge(common, {
+ devtool: 'inline-source-map',
+ module:{
+ rules:[
+ {
+ test: /\.css$/,
+ use: [{
+ loader: "style-loader" // creates style nodes from JS strings
+ }, {
+ loader: "css-loader" // translates CSS into CommonJS
+ }]
+ }
+ ]
+ },
+ devServer: {
+ contentBase: './dist',
+ port: 3000,
+ index: 'index.html',
+ historyApiFallback: true,
+ proxy: {
+ '/api/websocket': {
+ target: 'http://localhost:8080',
+ ws: true,
+ },
+ '/api/workspace': "http://localhost:8080",
+ }
+ },
+ plugins:[
+ new HtmlWebpackPlugin({
+ inject: false,
+ title: "Che Workspace Loader",
+ template:"src/index.html",
+ urlPrefix:"/"
+ })
+ ]
+});
diff --git a/workspace-loader/webpack.prod.js b/workspace-loader/webpack.prod.js
new file mode 100644
index 0000000000..0e0f6649c7
--- /dev/null
+++ b/workspace-loader/webpack.prod.js
@@ -0,0 +1,44 @@
+/*******************************************************************************
+ * Copyright (c) 2018-2018 Red Hat, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Red Hat, Inc. - initial API and implementation
+ *******************************************************************************/
+
+const merge = require('webpack-merge');
+const common = require('./webpack.common.js');
+const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
+const HtmlWebpackPlugin = require('html-webpack-plugin');
+const ExtractTextPlugin = require('extract-text-webpack-plugin');
+
+module.exports = merge(common, {
+ devtool: 'source-map',
+ module: {
+ rules: [
+ {
+ test: /\.css$/,
+ use: ExtractTextPlugin.extract({
+ fallback: "style-loader",
+ use: "css-loader"
+ })
+ }
+ ],
+ },
+ plugins: [
+ new UglifyJSPlugin({
+ sourceMap: true
+ }),
+ new HtmlWebpackPlugin({
+ inject: false,
+ template: 'src/index.html',
+ title: 'Che Workspace Loader',
+ urlPrefix: '/workspace-loader/loader/',
+ cssName: 'style.css'
+ }),
+ new ExtractTextPlugin('style.css')
+ ]
+});