From 0357ce7b0efc3eac3aec519bc636975b09e25ce7 Mon Sep 17 00:00:00 2001 From: Jeff Morgan Date: Fri, 28 Nov 2014 17:56:38 -0500 Subject: [PATCH] Fixing outstanding installer & port mapping bugs --- index.js | 5 +- meteor/client/lib/apputil.js | 201 ++++++++---------- meteor/client/lib/boot2docker.js | 49 ++++- meteor/client/lib/docker.js | 30 ++- meteor/client/lib/form-schemas.js | 4 +- meteor/client/lib/imageutil.js | 150 +++++++------ meteor/client/lib/router.js | 2 +- meteor/client/lib/setup.js | 35 +-- meteor/client/lib/virtualbox.js | 43 +++- meteor/client/main.js | 13 +- .../client/stylesheets/dashboard.import.less | 46 ++-- .../dashboard/apps/dashboard-apps-ports.html | 24 +-- .../apps/dashboard-apps-settings.html | 10 + .../dashboard/apps/dashboard-apps-settings.js | 2 +- .../dashboard/apps/dashboard-single-app.html | 48 +++-- .../dashboard/apps/dashboard-single-app.js | 42 ++-- .../dashboard/components/modal-create-app.js | 1 - .../components/modal-create-image.js | 2 +- .../layouts/dashboard-apps-layout.js | 3 +- meteor/collections/apps.js | 23 +- package.json | 5 +- script/dist.sh | 32 ++- script/setup.sh | 37 ++-- 23 files changed, 467 insertions(+), 340 deletions(-) diff --git a/index.js b/index.js index 66f0e4c857..b50cc94093 100644 --- a/index.js +++ b/index.js @@ -124,7 +124,10 @@ app.on('ready', function() { width: 800, height: 578, resizable: false, - frame: false + frame: false, + 'web-preferences': { + 'web-security': false + } }; mainWindow = new BrowserWindow(windowOptions); mainWindow.hide(); diff --git a/meteor/client/lib/apputil.js b/meteor/client/lib/apputil.js index db389f0c43..9639c3f196 100644 --- a/meteor/client/lib/apputil.js +++ b/meteor/client/lib/apputil.js @@ -1,6 +1,7 @@ var exec = require('exec'); var path = require('path'); var fs = require('fs'); +var async = require('async'); var Convert = require('ansi-to-html'); var convert = new Convert(); @@ -8,9 +9,10 @@ AppUtil = {}; AppUtil.run = function (app, callback) { var image = Images.findOne({_id: app.imageId}); - // Delete old container if one already exists + Apps.update(app._id, {$set: { + status: 'STARTING' + }}); Docker.removeContainer(app.name, function (err) { - if (err) { callback(err); } Docker.runContainer(app, image, function (err, container) { if (err) { callback(err); } Docker.getContainerData(container.id, function (err, data) { @@ -26,22 +28,6 @@ AppUtil.run = function (app, callback) { }); }; -AppUtil.restartHelper = function (app) { - if (app.docker && app.docker.Id) { - var container = Docker.client().getContainer(app.docker.Id); - container.restart(function (err) { - if (err) { console.error(err); } - Docker.getContainerData(app.docker.Id, function (err, data) { - if (err) { console.error(err); } - Apps.update(app._id, {$set: { - status: 'READY', - docker: data - }}); - }); - }); - } -}; - AppUtil.start = function (appId) { var app = Apps.findOne(appId); if (app && app.docker) { @@ -78,16 +64,6 @@ AppUtil.stop = function (appId) { } }; -AppUtil.restart = function (appId) { - var app = Apps.findOne(appId); - if (app && app.docker) { - Apps.update(app._id, {$set: { - status: 'STARTING' - }}); - AppUtil.restartHelper(app); - } -}; - AppUtil.remove = function (appId) { var app = Apps.findOne(appId); Apps.remove({_id: appId}); @@ -153,94 +129,103 @@ AppUtil.recover = function () { }); }; -AppUtil.sync = function () { +AppUtil.sync = function (callback) { Docker.listContainers(function (err, containers) { if (err) { - console.error(err); - } else { - var apps = Apps.find({}).fetch(); - _.each(apps, function (app) { - var app = Apps.findOne(app._id); - if (app && app.docker && app.docker.Id) { - var duplicateApps = Apps.find({'docker.Id': app.docker.Id, _id: {$ne: app._id}}).fetch(); - _.each(duplicateApps, function (duplicateApp) { - Apps.remove(duplicateApp._id); - }); - Docker.getContainerData(app.docker.Id, function (err, data) { - var status = 'STARTING'; - if (data && data.State && data.State.Running) { - status = 'READY'; - } else if (data && data.State && !data.State.Running) { - status = 'ERROR'; - } - Apps.update(app._id, { - $set: { - docker: data, - status: status - } - }) + callback(err); + return; + } + + var apps = Apps.find({}).fetch(); + var dockerIds = _.map(apps, function (app) { + if (app.docker && app.docker.Id) { + return app.docker.Id; + } + }); + var containerIds = _.map(containers, function (container) { + return container.Id; + }); + var diffApps = _.difference(dockerIds, containerIds); + _.each(diffApps, function (appContainerId) { + var app = Apps.findOne({'docker.Id': appContainerId}); + if (app && app.status !== 'STARTING') { + AppUtil.remove(app._id); + } + }); + var diffContainers = _.reject(containers, function (container) { + return _.contains(dockerIds, container.Id); + }); + _.each(diffContainers, function (container) { + var appName = container.Name.substring(1); + var startingApp = _.find(apps, function (app) { + return app.status === 'STARTING' && app.name === appName; + }); + if (!startingApp && appName !== 'kite-dns') { + var appPath = path.join(Util.KITE_PATH, appName); + if (!fs.existsSync(appPath)) { + console.log('Created Kite ' + appName + ' directory.'); + fs.mkdirSync(appPath, function (err) { + if (err) { throw err; } }); } - }); - var dockerIds = _.map(apps, function (app) { - if (app.docker && app.docker.Id) { - return app.docker.Id; - } - }); - var containerIds = _.map(containers, function (container) { - return container.Id; - }); - var diffApps = _.difference(dockerIds, containerIds); - _.each(diffApps, function (appContainerId) { - var app = Apps.findOne({'docker.Id': appContainerId}); - if (app && app.status !== 'STARTING') { - AppUtil.remove(app._id); - } - }); - var diffContainers = _.reject(containers, function (container) { - return _.contains(dockerIds, container.Id); - }); - _.each(diffContainers, function (container) { - var appName = container.Name.substring(1); - var startingApp = _.find(apps, function (app) { - return app.status === 'STARTING' && app.name === appName; + var envVars = container.Config.Env; + var config = {}; + _.each(envVars, function (envVar) { + var eqPos = envVar.indexOf('='); + var envKey = envVar.substring(0, eqPos); + var envVal = envVar.substring(eqPos + 1); + config[envKey] = envVal; }); - if (!startingApp && appName !== 'kite-dns') { - var appPath = path.join(Util.KITE_PATH, appName); - if (!fs.existsSync(appPath)) { - console.log('Created Kite ' + appName + ' directory.'); - fs.mkdirSync(appPath, function (err) { - if (err) { throw err; } - }); - } - var envVars = container.Config.Env; - var config = {}; - _.each(envVars, function (envVar) { - var eqPos = envVar.indexOf('='); - var envKey = envVar.substring(0, eqPos); - var envVal = envVar.substring(eqPos + 1); - config[envKey] = envVal; - }); + var status = 'STARTING'; + if (container.State.Running) { + status = 'READY'; + } else { + status = 'ERROR'; + } + var appObj = { + name: appName, + docker: container, + status: status, + config: config, + path: appPath, + logs: [], + createdAt: new Date(), + }; + if (container.HostConfig.Binds && container.HostConfig.Binds.length) { + appObj.volumesEnabled = true; + } else { + appObj.volumesEnabled = false; + } + console.log(appObj); + Apps.insert(appObj); + } + }); + + async.each(apps, function (app, callback) { + if (app && app.docker && app.docker.Id) { + var duplicateApps = Apps.find({'docker.Id': app.docker.Id, _id: {$ne: app._id}}).fetch(); + _.each(duplicateApps, function (duplicateApp) { + Apps.remove(duplicateApp._id); + }); + Docker.getContainerData(app.docker.Id, function (err, data) { + if (err) {callback(err); return;} var status = 'STARTING'; - if (container.State.Running) { + if (data && data.State && data.State.Running) { status = 'READY'; - } else { + } else if (data && data.State && !data.State.Running) { status = 'ERROR'; } - var appObj = { - name: appName, - docker: container, - status: status, - config: config, - path: appPath, - logs: [], - createdAt: new Date(), - volumesEnabled: true - }; - console.log(appObj); - Apps.insert(appObj); - } - }); - } + Apps.update(app._id, { + $set: { + docker: data, + status: status + } + }); + callback(); + }); + } + }, function (err) { + callback(err); + }); }); }; diff --git a/meteor/client/lib/boot2docker.js b/meteor/client/lib/boot2docker.js index 40d0f92ade..df2f39849b 100644 --- a/meteor/client/lib/boot2docker.js +++ b/meteor/client/lib/boot2docker.js @@ -2,11 +2,12 @@ var exec = require('exec'); var path = require('path'); var fs = require('fs'); var path = require('path'); +var async = require('async'); +var pkginfo = require('pkginfo')(module); + Boot2Docker = {}; - -Boot2Docker.REQUIRED_IP = '192.168.60.103'; -Boot2Docker.VERSION = '1.3.1'; +Boot2Docker.VERSION = module.exports['boot2docker-version']; Boot2Docker.command = function () { return path.join(Util.getBinDir(), 'boot2docker-' + Boot2Docker.VERSION); @@ -252,7 +253,43 @@ Boot2Docker.vmUpToDate = function (callback) { if (err) { callback(err); return; } - var index = data.indexOf('Boot2Docker-v' + Boot2Docker.VERSION); - callback(null, index !== -1); + var match = data.match(/Boot2Docker-v(\d+\.\d+\.\d+)/); + if (!match) { + callback('Could not parse boot2docker iso version'); + return; + } + callback (null, Util.compareVersions(match[1], Boot2Docker.VERSION) < 0); }); -} \ No newline at end of file +}; + +Boot2Docker.status = function (callback) { + this.exec('status', function (stderr, stdout, code) { + if (code) {callback(stderr); return;} + callback(null, stdout.trim()); + }); +}; + +Boot2Docker.portAvailable = function (port, protocol, callback) { + this.exec('ssh netstat -lntu | grep LISTEN | grep ' + protocol + ' | grep -c ":::' + port + '\\s"', function (stdout, stderr, code) { + if (stderr.trim() === '0') { + callback(true); + } else { + callback(false); + } + }); +}; + +Boot2Docker.waitWhileStatus = function (status, callback) { + var current = status; + async.whilst(function () { + return current === status; + }, function (innerCallback) { + Boot2Docker.status(function (err, vmStatus) { + if (err) {innerCallback(err); return;} + current = vmStatus.trim(); + innerCallback(); + }); + }, function (err) { + callback(err); + }); +}; \ No newline at end of file diff --git a/meteor/client/lib/docker.js b/meteor/client/lib/docker.js index 00128f473f..67f916ff9d 100644 --- a/meteor/client/lib/docker.js +++ b/meteor/client/lib/docker.js @@ -95,13 +95,23 @@ Docker.runContainer = function (app, image, callback) { var builtStr = key + '=' + app.config[key]; envParam.push(builtStr); }); - Docker.client().createContainer({ + + var containerOpts = { Image: image.docker.Id, Tty: false, Env: envParam, Hostname: app.name, name: app.name - }, function (err, container) { + }; + + + if (app.docker && app.docker.NetworkSettings.Ports) { + containerOpts.ExposedPorts = app.docker.NetworkSettings.Ports; + } + + console.log(containerOpts); + + Docker.client().createContainer(containerOpts, function (err, container) { if (err) { callback(err, null); return; } console.log('Created container: ' + container.id); // Bind volumes @@ -111,11 +121,19 @@ Docker.runContainer = function (app, image, callback) { binds.push([Util.getHomePath(), 'Kitematic', app.name, vol.Path].join('/') + ':' + vol.Path); }); } - // Start the container - container.start({ - PublishAllPorts: true, + + var startOpts = { Binds: binds - }, function (err) { + }; + + if (app.docker && app.docker.NetworkSettings.Ports) { + startOpts.PortBindings = app.docker.NetworkSettings.Ports; + } else { + startOpts.PublishAllPorts = true; + } + + console.log(startOpts); + container.start(startOpts, function (err) { if (err) { callback(err, null); return; } console.log('Started container: ' + container.id); callback(null, container); diff --git a/meteor/client/lib/form-schemas.js b/meteor/client/lib/form-schemas.js index ea165b9b54..f96c5f1904 100644 --- a/meteor/client/lib/form-schemas.js +++ b/meteor/client/lib/form-schemas.js @@ -2,11 +2,11 @@ FormSchema = { formCreateApp: { name: { - label: 'app name', + label: 'container name', required: true, transforms: ['clean', 'slugify'], messages: { - 'uniqueAppName': "This app name is already being used." + 'uniqueAppName': "This container name is already being used." }, rules: { uniqueAppName: true diff --git a/meteor/client/lib/imageutil.js b/meteor/client/lib/imageutil.js index 83618ab27f..edcf3713d6 100644 --- a/meteor/client/lib/imageutil.js +++ b/meteor/client/lib/imageutil.js @@ -3,6 +3,7 @@ var convert = new Convert(); var exec = require('exec'); var path = require('path'); var fs = require('fs'); +var async = require('async'); ImageUtil = {}; @@ -239,79 +240,88 @@ ImageUtil.remove = function (imageId) { } }; -ImageUtil.sync = function () { +ImageUtil.sync = function (callback) { Docker.listImages(function (err, dockerImages) { if (err) { console.error(err); - } else { - var images = Images.find({}).fetch(); - _.each(images, function (image) { - var image = Images.findOne(image._id); - if (image && image.docker && image.docker.Id) { - var duplicateImages = Images.find({'docker.Id': image.docker.Id, _id: {$ne: image._id}}).fetch(); - _.each(duplicateImages, function (duplicateImage) { - Images.remove(duplicateImage._id); - }); - var imageData = _.find(dockerImages, function (dockerImage) { - return dockerImage.Id === image.docker.Id; - }); - if (imageData && imageData.RepoTags) { - Images.update(image._id, { - $set: { - tags: imageData.RepoTags - } - }); - } - Docker.getImageData(image.docker.Id, function (err, data) { - Images.update(image._id, { - $set: { - docker: data - } - }) - }); - } - }); - var dockerIds = _.map(images, function (image) { - if (image.docker && image.docker.Id) { - return image.docker.Id; - } - }); - var imageIds = _.map(dockerImages, function (image) { - return image.Id; - }); - var diffImages = _.difference(dockerIds, imageIds); - _.each(diffImages, function (imageId) { - var image = Images.findOne({'docker.Id': imageId}); - if (image && image.status !== 'BUILDING') { - ImageUtil.remove(image._id); - } - }); - var diffDockerImages = _.reject(dockerImages, function (image) { - return _.contains(dockerIds, image.Id); - }); - _.each(diffDockerImages, function (image) { - var repoTag = _.first(image.RepoTags); - var repoTagTokens = repoTag.split(':'); - var name = repoTagTokens[0]; - var version = repoTagTokens[1]; - var buildingImage = _.find(images, function (image) { - return image.status === 'BUILDING' && image.meta.name === name && image.meta.version === version; - }); - if (!buildingImage && name !== '' && version !== '' && name !== 'kite-dns') { - var imageObj = { - status: 'READY', - docker: image, - buildLogs: [], - createdAt: new Date(), - tags: image.RepoTags, - meta: { - name: name, - version: version - } - }; - Images.insert(imageObj); - } - }); + return; } + var images = Images.find({}).fetch(); + + // Delete missing GUI images + var kitematicIds = _.map(images, function (image) { + if (image.docker && image.docker.Id) { + return image.docker.Id; + } + }); + var daemonIds = _.map(daemonIds, function (image) { + return image.Id; + }); + var diffImages = _.difference(kitematicIds, daemonIds); + _.each(diffImages, function (imageId) { + var image = Images.findOne({'docker.Id': imageId}); + if (image && image.status !== 'BUILDING') { + Images.remove(image._id); + } + }); + + // Add missing Daemon images + var diffDockerImages = _.reject(dockerImages, function (image) { + return _.contains(kitematicIds, image.Id); + }); + _.each(diffDockerImages, function (image) { + var repoTag = _.first(image.RepoTags); + var repoTagTokens = repoTag.split(':'); + var name = repoTagTokens[0]; + var version = repoTagTokens[1]; + var buildingImage = _.find(images, function (image) { + return image.status === 'BUILDING' && image.meta.name === name && image.meta.version === version; + }); + if (!buildingImage && name !== '' && version !== '' && name !== 'kite-dns') { + var imageObj = { + status: 'READY', + docker: image, + buildLogs: [], + createdAt: new Date(), + tags: image.RepoTags, + meta: { + name: name, + version: version + } + }; + Images.insert(imageObj); + } + }); + + async.each(images, function (image, callback) { + var image = Images.findOne(image._id); + if (image && image.docker && image.docker.Id) { + var duplicateImages = Images.find({'docker.Id': image.docker.Id, _id: {$ne: image._id}}).fetch(); + _.each(duplicateImages, function (duplicateImage) { + Images.remove(duplicateImage._id); + }); + var imageData = _.find(dockerImages, function (dockerImage) { + return dockerImage.Id === image.docker.Id; + }); + if (imageData && imageData.RepoTags) { + Images.update(image._id, { + $set: { + tags: imageData.RepoTags + } + }); + } + Docker.getImageData(image.docker.Id, function (err, data) { + if (err) {callback(err); return;} + Images.update(image._id, { + $set: { + docker: data + } + }); + callback(); + }); + } + }, function (err) { + callback(err); + }); }); }; diff --git a/meteor/client/lib/router.js b/meteor/client/lib/router.js index 33db518057..7c1ebb9063 100755 --- a/meteor/client/lib/router.js +++ b/meteor/client/lib/router.js @@ -52,7 +52,7 @@ Router.map(function () { } Session.set('onIntro', false); startUpdatingBoot2DockerUtilization(); - // startSyncingAppState(); + startSyncingAppState(); Router.go('dashboard_apps'); } }); diff --git a/meteor/client/lib/setup.js b/meteor/client/lib/setup.js index 0b688b34c2..bda23abf52 100644 --- a/meteor/client/lib/setup.js +++ b/meteor/client/lib/setup.js @@ -67,6 +67,17 @@ Setup.steps = [ }, message: 'Downloading VirtualBox...' }, + { + run: function (callback) { + VirtualBox.shutdownVM('kitematic-vm', function (err, removed) { + if (err) { + console.log(err); + } + callback(); + }); + }, + message: 'Cleaning up existing Docker VM...' + }, // Initialize Boot2Docker if necessary. { @@ -74,10 +85,6 @@ Setup.steps = [ Boot2Docker.exists(function (err, exists) { if (err) { callback(err); return; } if (!exists) { - var vmFilesPath = path.join(Util.getHomePath(), 'VirtualBox\ VMs', 'boot2docker-vm'); - if (fs.existsSync(vmFilesPath)) { - Util.deleteFolder(vmFilesPath); - } Boot2Docker.init(function (err) { callback(err); }); @@ -105,15 +112,17 @@ Setup.steps = [ }, { run: function (callback) { - Boot2Docker.state(function (err, state) { - if (err) {callback(err); return;} - if (state !== 'running') { - Boot2Docker.start(function (err) { - callback(err); - }); - } else { - callback(); - } + Boot2Docker.waitWhileStatus('saving', function (err) { + Boot2Docker.status(function (err, status) { + if (err) {callback(err); return;} + if (status !== 'running') { + Boot2Docker.start(function (err) { + callback(err); + }); + } else { + callback(); + } + }); }); }, message: 'Starting the Docker VM...' diff --git a/meteor/client/lib/virtualbox.js b/meteor/client/lib/virtualbox.js index 6f0e537dd8..39099cb87e 100644 --- a/meteor/client/lib/virtualbox.js +++ b/meteor/client/lib/virtualbox.js @@ -1,6 +1,7 @@ var fs = require('fs'); var exec = require('exec'); var path = require('path'); +var async = require('async'); VirtualBox = {}; @@ -51,7 +52,7 @@ VirtualBox.version = function (callback) { }); }; -VirtualBox.shutDownRunningVMs = function (callback) { +VirtualBox.saveRunningVMs = function (callback) { if (!this.installed()) { callback('VirtualBox not installed.'); return; @@ -66,7 +67,7 @@ VirtualBox.shutDownRunningVMs = function (callback) { }; VirtualBox.killAllProcesses = function (callback) { - this.shutDownRunningVMs(function (err) { + this.saveRunningVMs(function (err) { if (err) {callback(err); return;} exec('pkill VirtualBox', function (stderr, stdout, code) { if (code) {callback(stderr); return;} @@ -77,3 +78,41 @@ VirtualBox.killAllProcesses = function (callback) { }); }); }; + +VirtualBox.vmState = function (name, callback) { + VirtualBox.exec('showvminfo ' + name + ' --machinereadable', function (stderr, stdout, code) { + if (code) { callback(stderr); return; } + var match = stdout.match(/VMState="(\w+)"/); + if (!match) { + callback('Could not parse VMState'); + return; + } + callback(null, match[1]); + }); +}; + +VirtualBox.shutdownVM = function (name, callback) { + VirtualBox.vmState(name, function (err, state) { + // No VM found + if (err) { callback(null, false); return; } + VirtualBox.exec('controlvm ' + name + ' acpipowerbutton', function (stderr, stdout, code) { + if (code) { callback(stderr, false); return; } + var state = null; + + async.until(function () { + return state === 'poweroff'; + }, function (callback) { + VirtualBox.vmState(name, function (err, newState) { + if (err) { callback(err); return; } + state = newState; + setTimeout(callback, 250); + }); + }, function (err) { + VirtualBox.exec('unregistervm ' + name + ' --delete', function (stderr, stdout, code) { + if (code) { callback(err); return; } + callback(); + }); + }); + }); + }); +}; \ No newline at end of file diff --git a/meteor/client/main.js b/meteor/client/main.js index a35249fa19..8b50603a96 100755 --- a/meteor/client/main.js +++ b/meteor/client/main.js @@ -100,10 +100,11 @@ startUpdatingBoot2DockerUtilization = function () { }; startSyncingAppState = function () { - ImageUtil.sync(); - AppUtil.sync(); - Meteor.setTimeout(function () { - ImageUtil.sync(); - AppUtil.sync(); - }, 5000); + ImageUtil.sync(function (err) { + if (err) {console.log(err);} + AppUtil.sync(function (err) { + if (err) {console.log(err);} + Meteor.setTimeout(startSyncingAppState, 5000); + }); + }); }; \ No newline at end of file diff --git a/meteor/client/stylesheets/dashboard.import.less b/meteor/client/stylesheets/dashboard.import.less index 2bbb30aeea..204da71ad2 100755 --- a/meteor/client/stylesheets/dashboard.import.less +++ b/meteor/client/stylesheets/dashboard.import.less @@ -30,8 +30,11 @@ } } .ports { + position: relative; + top: -2px; .dropdown-menu { min-width: 241px; + padding: 10px 15px 3px; } .btn-group { top: -2px; @@ -62,7 +65,6 @@ &.open + .tooltip { display: none !important; } - position: relative; } } &:hover { @@ -281,30 +283,38 @@ } } - .env-var-pair { - .make-row(); - margin-bottom: 0.2em; - font-size: 12px; - .env-var-key { - .make-xs-column(5); - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; + .form-env-vars { + label { + font-size: 13px; } - .env-var-value { - .make-xs-column(5); - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; + input { + font-size: 13px; + padding: 5px 9px; } - .options { - .make-xs-column(2); + .env-var-pair { + .make-row(); + margin-bottom: 0.2em; + font-size: 13px; + .env-var-key { + .make-xs-column(5); + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } + .env-var-value { + .make-xs-column(5); + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } + .options { + .make-xs-column(2); + } } } .app-ports { cursor: default; - padding: 10px 15px 3px; min-width: 240px; li { padding-bottom: 7px; diff --git a/meteor/client/views/dashboard/apps/dashboard-apps-ports.html b/meteor/client/views/dashboard/apps/dashboard-apps-ports.html index d5d37a9400..7797c621b9 100644 --- a/meteor/client/views/dashboard/apps/dashboard-apps-ports.html +++ b/meteor/client/views/dashboard/apps/dashboard-apps-ports.html @@ -1,23 +1,13 @@ \ No newline at end of file diff --git a/meteor/client/views/dashboard/apps/dashboard-apps-settings.html b/meteor/client/views/dashboard/apps/dashboard-apps-settings.html index c4fb38f652..ba35617731 100755 --- a/meteor/client/views/dashboard/apps/dashboard-apps-settings.html +++ b/meteor/client/views/dashboard/apps/dashboard-apps-settings.html @@ -1,4 +1,14 @@