(function() { 'use strict'; var app = angular.module('DockerPlay', ['ngMaterial', 'ngFileUpload']); // Automatically redirects user to a new session when bypassing captcha. // Controller keeps code/logic separate from the HTML app.controller("BypassController", ['$scope', '$log', '$http', '$location', '$timeout', function($scope, $log, $http, $location, $timeout) { setTimeout(function() { document.getElementById("welcomeFormBypass").submit(); }, 500); }]); function SessionBuilderModalController($mdDialog, $scope) { $scope.createBuilderTerminal(); $scope.closeSessionBuilder = function() { $mdDialog.cancel(); } } app.controller('PlayController', ['$scope', '$log', '$http', '$location', '$timeout', '$mdDialog', '$window', 'TerminalService', 'KeyboardShortcutService', 'InstanceService', 'SessionService', 'Upload', function($scope, $log, $http, $location, $timeout, $mdDialog, $window, TerminalService, KeyboardShortcutService, InstanceService, SessionService, Upload) { $scope.sessionId = SessionService.getCurrentSessionId(); $scope.instances = []; $scope.idx = {}; $scope.idxByHostname = {}; $scope.selectedInstance = null; $scope.isAlive = true; $scope.ttl = '--:--:--'; $scope.connected = false; $scope.type = {windows: false}; $scope.isInstanceBeingCreated = false; $scope.newInstanceBtnText = '+ Add new instance'; $scope.deleteInstanceBtnText = 'Delete'; $scope.isInstanceBeingDeleted = false; $scope.uploadProgress = 0; $scope.uploadFiles = function (files, invalidFiles) { let total = files.length; let uploadFile = function() { let file = files.shift(); if (!file){ $scope.uploadMessage = ""; $scope.uploadProgress = 0; return } $scope.uploadMessage = "Uploading file(s) " + (total - files.length) + "/"+ total + " : " + file.name; let upload = Upload.upload({url: '/sessions/' + $scope.sessionId + '/instances/' + $scope.selectedInstance.name + '/uploads', data: {file: file}, method: 'POST'}) .then(function(){}, function(){}, function(evt) { $scope.uploadProgress = parseInt(100.0 * evt.loaded / evt.total); }); // process next file upload.finally(uploadFile); } uploadFile(); } var selectedKeyboardShortcuts = KeyboardShortcutService.getCurrentShortcuts(); $scope.resizeHandler = null; angular.element($window).bind('resize', function() { if ($scope.selectedInstance) { if (!$scope.resizeHandler) { $scope.resizeHandler = setTimeout(function() { $scope.resizeHandler = null $scope.resize($scope.selectedInstance.term.proposeGeometry()); }, 1000); } } }); $scope.$on("settings:shortcutsSelected", function(e, preset) { selectedKeyboardShortcuts = preset; }); $scope.showAlert = function(title, content, parent, cb) { $mdDialog.show( $mdDialog.alert() .parent(angular.element(document.querySelector(parent || '#popupContainer'))) .clickOutsideToClose(true) .title(title) .textContent(content) .ok('Got it!') ).finally(function() { if (cb) { cb(); } }); } $scope.resize = function(geometry) { $scope.socket.emit('instance viewport resize', geometry.cols, geometry.rows); } KeyboardShortcutService.setResizeFunc($scope.resize); $scope.closeSession = function() { // Remove alert before closing browser tab window.onbeforeunload = null; $scope.socket.emit('session close'); } $scope.upsertInstance = function(info) { var i = info; if (!$scope.idx[i.name]) { $scope.instances.push(i); i.buffer = ''; $scope.idx[i.name] = i; $scope.idxByHostname[i.hostname] = i; } else { $scope.idx[i.name].ip = i.ip; $scope.idx[i.name].hostname = i.hostname; $scope.idx[i.name].proxy_host = i.proxy_host; } return $scope.idx[i.name]; } $scope.newInstance = function() { updateNewInstanceBtnState(true); var instanceType = $scope.type.windows ? 'windows': 'linux'; $http({ method: 'POST', url: '/sessions/' + $scope.sessionId + '/instances', data : { ImageName : InstanceService.getDesiredImage(), type: instanceType } }).then(function(response) { $scope.upsertInstance(response.data); }, function(response) { if (response.status == 409) { $scope.showAlert('Max instances reached', 'Maximum number of instances reached') } else if (response.status == 503 && response.data.error == 'out_of_capacity') { $scope.showAlert('Out Of Capacity', 'We are really sorry. But we are currently out of capacity and cannot create new instances. Please try again later.') } }).finally(function() { updateNewInstanceBtnState(false); }); } $scope.setSessionState = function(state) { $scope.ready = state; if (!state) { $mdDialog.show({ onComplete: function(){SessionBuilderModalController($mdDialog, $scope)}, contentElement: '#builderDialog', parent: angular.element(document.body), clickOutsideToClose: false, scope: $scope, preserveScope: true }); } } $scope.loadPlaygroundConf = function() { $http({ method: 'GET', url: '/my/playground', }).then(function(response) { $scope.playground = response.data; }); } $scope.getSession = function(sessionId) { $http({ method: 'GET', url: '/sessions/' + $scope.sessionId, }).then(function(response) { $scope.setSessionState(response.data.ready); if (response.data.created_at) { $scope.expiresAt = moment(response.data.expires_at); setInterval(function() { $scope.ttl = moment.utc($scope.expiresAt.diff(moment())).format('HH:mm:ss'); $scope.$apply(); }, 1000); } var i = response.data; for (var k in i.instances) { var instance = i.instances[k]; $scope.instances.push(instance); $scope.idx[instance.name] = instance; $scope.idxByHostname[instance.hostname] = instance; } var base = ''; if (window.location.protocol == 'http:') { base = 'ws://'; } else { base = 'wss://'; } base += window.location.host; if (window.location.port) { base += ':' + window.location.port; } var socket = new ReconnectingWebSocket(base + '/sessions/' + sessionId + '/ws/', null, {reconnectInterval: 1000}); socket.listeners = {}; socket.on = function(name, cb) { if (!socket.listeners[name]) { socket.listeners[name] = []; } socket.listeners[name].push(cb); } socket.emit = function() { var name = arguments[0] var args = []; for (var i = 1; i < arguments.length; i++) { args.push(arguments[i]); } socket.send(JSON.stringify({name: name, args: args})); } socket.addEventListener('open', function (event) { $scope.connected = true; for (var i in $scope.instances) { var instance = $scope.instances[i]; if (instance.term) { instance.term.setOption('disableStdin', false); } } }); socket.addEventListener('close', function (event) { $scope.connected = false; for (var i in $scope.instances) { var instance = $scope.instances[i]; if (instance.term) { instance.term.setOption('disableStdin', true); } } }); socket.addEventListener('message', function (event) { var m = JSON.parse(event.data); var ls = socket.listeners[m.name]; if (ls) { for (var i=0; i 0) { // if no instance has been passed, select the first. $scope.showInstance($scope.instances[0]); } }, function(response) { if (response.status == 404) { document.write('session not found'); return } }); } $scope.getProxyUrl = function(instance, port) { var url = 'http://' + instance.proxy_host + '-' + port + '.direct.' + window.location.host; return url; } $scope.showInstance = function(instance) { $scope.selectedInstance = instance; $location.hash(instance.name); if (!instance.term) { $timeout(function() { createTerminal(instance); TerminalService.setFontSize(TerminalService.getFontSize()); instance.term.focus(); $timeout(function() { }, 0, false); }, 0, false); return } } $scope.removeInstance = function(name) { if ($scope.idx[name]) { var handler = $scope.idx[name].terminalBufferInterval; clearInterval(handler); } if ($scope.idx[name]) { delete $scope.idx[name]; $scope.instances = $scope.instances.filter(function(i) { return i.name != name; }); if ($scope.instances.length) { $scope.showInstance($scope.instances[0]); } } } $scope.deleteInstance = function(instance) { updateDeleteInstanceBtnState(true); $http({ method: 'DELETE', url: '/sessions/' + $scope.sessionId + '/instances/' + instance.name, }).then(function(response) { $scope.removeInstance(instance.name); }, function(response) { console.log('error', response); }).finally(function() { updateDeleteInstanceBtnState(false); }); } $scope.loadPlaygroundConf(); $scope.getSession($scope.sessionId); $scope.createBuilderTerminal = function() { var builderTerminalContainer = document.getElementById('builder-terminal'); let term = new Terminal({ cursorBlink: false }); term.open(builderTerminalContainer); $scope.builderTerminal = term; } function createTerminal(instance, cb) { if (instance.term) { return instance.term; } var terminalContainer = document.getElementById('terminal-' + instance.name); var term = new Terminal({ cursorBlink: false }); term.attachCustomKeydownHandler(function(e) { // Ctrl + Alt + C if (e.ctrlKey && e.altKey && (e.keyCode == 67)) { document.execCommand('copy'); return false; } }); term.attachCustomKeydownHandler(function(e) { if (selectedKeyboardShortcuts == null) return; var presets = selectedKeyboardShortcuts.presets .filter(function(preset) { return preset.keyCode == e.keyCode }) .filter(function(preset) { return (preset.metaKey == undefined && !e.metaKey) || preset.metaKey == e.metaKey }) .filter(function(preset) { return (preset.ctrlKey == undefined && !e.ctrlKey) || preset.ctrlKey == e.ctrlKey }) .filter(function(preset) { return (preset.altKey == undefined && !e.altKey) || preset.altKey == e.altKey }) .forEach(function(preset) { preset.action({ terminal : term })}); }); term.open(terminalContainer); // Set geometry during the next tick, to avoid race conditions. /* setTimeout(function() { $scope.resize(term.proposeGeometry()); }, 4); */ instance.terminalBuffer = ''; instance.terminalBufferInterval = setInterval(function() { if (instance.terminalBuffer.length > 0) { $scope.socket.emit('instance terminal in', instance.name, instance.terminalBuffer); instance.terminalBuffer = ''; } }, 70); term.on('data', function(d) { instance.terminalBuffer += d; }); instance.term = term; if (cb) { cb(); } } function updateNewInstanceBtnState(isInstanceBeingCreated) { if (isInstanceBeingCreated === true) { $scope.newInstanceBtnText = '+ Creating...'; $scope.isInstanceBeingCreated = true; } else { $scope.newInstanceBtnText = '+ Add new instance'; $scope.isInstanceBeingCreated = false; } } function updateDeleteInstanceBtnState(isInstanceBeingDeleted) { if (isInstanceBeingDeleted === true) { $scope.deleteInstanceBtnText = 'Deleting...'; $scope.isInstanceBeingDeleted = true; } else { $scope.deleteInstanceBtnText = 'Delete'; $scope.isInstanceBeingDeleted = false; } } }]) .config(['$mdIconProvider', '$locationProvider', '$mdThemingProvider', function($mdIconProvider, $locationProvider, $mdThemingProvider) { $locationProvider.html5Mode({enabled: true, requireBase: false}); $mdIconProvider.defaultIconSet('../assets/social-icons.svg', 24); $mdThemingProvider.theme('kube') .primaryPalette('grey') .accentPalette('grey'); }]) .component('settingsIcon', { template : "settings", controller : function($mdDialog) { var $ctrl = this; $ctrl.onClick = function() { $mdDialog.show({ controller : function() {}, template : "", parent: angular.element(document.body), clickOutsideToClose : true }) } } }) .component('templatesIcon', { template : "build", controller : function($mdDialog) { var $ctrl = this; $ctrl.onClick = function() { $mdDialog.show({ controller : function() {}, template : "", parent: angular.element(document.body), clickOutsideToClose : true }) } } }) .component("templatesDialog", { templateUrl : "templates-modal.html", controller : function($mdDialog, $scope, SessionService) { var $ctrl = this; $scope.building = false; $scope.templates = SessionService.getAvailableTemplates(); $ctrl.close = function() { $mdDialog.cancel(); } $ctrl.setupSession = function(setup) { $scope.building = true; SessionService.setup(setup, function(err) { $scope.building = false; if (err) { $scope.errorMessage = err; return; } $ctrl.close(); }); } } }) .component("settingsDialog", { templateUrl : "settings-modal.html", controller : function($mdDialog, KeyboardShortcutService, $rootScope, InstanceService, TerminalService) { var $ctrl = this; $ctrl.$onInit = function() { $ctrl.keyboardShortcutPresets = KeyboardShortcutService.getAvailablePresets(); $ctrl.selectedShortcutPreset = KeyboardShortcutService.getCurrentShortcuts(); $ctrl.instanceImages = InstanceService.getAvailableImages(); $ctrl.selectedInstanceImage = InstanceService.getDesiredImage(); $ctrl.terminalFontSizes = TerminalService.getFontSizes(); }; $ctrl.currentShortcutConfig = function(value) { if (value !== undefined) { value = JSON.parse(value); KeyboardShortcutService.setCurrentShortcuts(value); $ctrl.selectedShortcutPreset = angular.copy(KeyboardShortcutService.getCurrentShortcuts()); $rootScope.$broadcast('settings:shortcutsSelected', $ctrl.selectedShortcutPreset); } return JSON.stringify(KeyboardShortcutService.getCurrentShortcuts()); }; $ctrl.currentDesiredInstanceImage = function(value) { if (value !== undefined) { InstanceService.setDesiredImage(value); } return InstanceService.getDesiredImage(value); }; $ctrl.currentTerminalFontSize = function(value) { if (value !== undefined) { // set font size TerminalService.setFontSize(value); return; } return TerminalService.getFontSize(); } $ctrl.close = function() { $mdDialog.cancel(); } } }) .service("SessionService", function($http) { var templates = [ { title: '3 Managers and 2 Workers', icon: '/assets/swarm.png', setup: { instances: [ {hostname: 'manager1', is_swarm_manager: true}, {hostname: 'manager2', is_swarm_manager: true}, {hostname: 'manager3', is_swarm_manager: true}, {hostname: 'worker1', is_swarm_worker: true}, {hostname: 'worker2', is_swarm_worker: true} ] } }, { title: '5 Managers and no workers', icon: '/assets/swarm.png', setup: { instances: [ {hostname: 'manager1', is_swarm_manager: true}, {hostname: 'manager2', is_swarm_manager: true}, {hostname: 'manager3', is_swarm_manager: true}, {hostname: 'manager4', is_swarm_manager: true}, {hostname: 'manager5', is_swarm_manager: true} ] } } ]; return { getAvailableTemplates: getAvailableTemplates, getCurrentSessionId: getCurrentSessionId, setup: setup, }; function getCurrentSessionId() { return window.location.pathname.replace('/p/', ''); } function getAvailableTemplates() { return templates; } function setup(plan, cb) { return $http .post("/sessions/" + getCurrentSessionId() + "/setup", plan) .then(function(response) { if (cb) cb(); }, function(response) { if (cb) cb(response.data); }); } }) .service("InstanceService", function($http) { var instanceImages = []; _prepopulateAvailableImages(); return { getAvailableImages : getAvailableImages, setDesiredImage : setDesiredImage, getDesiredImage : getDesiredImage, }; function getAvailableImages() { return instanceImages; } function getDesiredImage() { var image = localStorage.getItem("settings.desiredImage"); if (image == null) return instanceImages[0]; return image; } function setDesiredImage(image) { if (image === null) localStorage.removeItem("settings.desiredImage"); else localStorage.setItem("settings.desiredImage", image); } function _prepopulateAvailableImages() { return $http .get("/instances/images") .then(function(response) { instanceImages = response.data; }); } }) .run(function(InstanceService) { /* forcing pre-populating for now */ }) .service("KeyboardShortcutService", ['TerminalService', function(TerminalService) { var resizeFunc; return { getAvailablePresets : getAvailablePresets, getCurrentShortcuts : getCurrentShortcuts, setCurrentShortcuts : setCurrentShortcuts, setResizeFunc : setResizeFunc }; function setResizeFunc(f) { resizeFunc = f; } function getAvailablePresets() { return [ { name : "None", presets : [ { description : "Toggle terminal fullscreen", command : "Alt+enter", altKey : true, keyCode : 13, action : function(context) { TerminalService.toggleFullscreen(context.terminal, resizeFunc); }} ] }, { name : "Mac OSX", presets : [ { description : "Clear terminal", command : "Cmd+K", metaKey : true, keyCode : 75, action : function(context) { context.terminal.clear(); }}, { description : "Toggle terminal fullscreen", command : "Alt+enter", altKey : true, keyCode : 13, action : function(context) { TerminalService.toggleFullscreen(context.terminal, resizeFunc); }} ] } ] } function getCurrentShortcuts() { var shortcuts = localStorage.getItem("shortcut-preset-name"); if (shortcuts == null) { shortcuts = getDefaultShortcutPrefixName(); if (shortcuts == null) return null; } var preset = getAvailablePresets() .filter(function(preset) { return preset.name == shortcuts; }); if (preset.length == 0) console.error("Unable to find preset with name '" + shortcuts + "'"); return preset[0]; return (shortcuts == null) ? null : JSON.parse(shortcuts); } function setCurrentShortcuts(config) { localStorage.setItem("shortcut-preset-name", config.name); } function getDefaultShortcutPrefixName() { if (window.navigator.platform.toUpperCase().indexOf('MAC') >= 0) return "Mac OSX"; return "None"; } }]) .service('TerminalService', ['$window', function($window) { var fullscreen; var fontSize = getFontSize(); return { getFontSizes : getFontSizes, setFontSize : setFontSize, getFontSize : getFontSize, increaseFontSize : increaseFontSize, decreaseFontSize : decreaseFontSize, toggleFullscreen : toggleFullscreen }; function getFontSizes() { var terminalFontSizes = []; for (var i=3; i<40; i++) { terminalFontSizes.push(i+'px'); } return terminalFontSizes; }; function getFontSize() { if (!fontSize) { return $('.terminal').css('font-size'); } return fontSize; } function setFontSize(value) { fontSize = value; var size = parseInt(value); $('.terminal').css('font-size', value).css('line-height', (size + 2)+'px'); //.css('line-height', value).css('height', value); angular.element($window).trigger('resize'); } function increaseFontSize() { var sizes = getFontSizes(); var size = getFontSize(); var i = sizes.indexOf(size); if (i == -1) { return; } if (i+1 > sizes.length) { return; } setFontSize(sizes[i+1]); } function decreaseFontSize() { var sizes = getFontSizes(); var size = getFontSize(); var i = sizes.indexOf(size); if (i == -1) { return; } if (i-1 < 0) { return; } setFontSize(sizes[i-1]); } function toggleFullscreen(terminal, resize) { if(fullscreen) { terminal.toggleFullscreen(); resize(fullscreen); fullscreen = null; } else { fullscreen = terminal.proposeGeometry(); terminal.toggleFullscreen(); angular.element($window).trigger('resize'); } } }]); })();