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) { %> + + <% } %> + + + + +
+
Loading...
+
+
+
+
+
+
+
+
+
+ + + 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 = `
+
Loading...
+
+
+
+
+
+
+
+
+
`; + + 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') + ] +});