diff --git a/.gitignore b/.gitignore index c6befdab3f..64683de297 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ build dist node_modules +coverage npm-debug.log # Signing Identity @@ -19,4 +20,4 @@ cache settings.json # IDEs -.idea \ No newline at end of file +.idea diff --git a/.jshintrc b/.jshintrc index 9240a3ae9f..daeaf45072 100644 --- a/.jshintrc +++ b/.jshintrc @@ -27,5 +27,5 @@ "jest": true, "pit": true }, - "predef": [ "-Promise" ] + "predef": [ "Promise" ] } diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000000..dabf931807 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,13 @@ +language: node_js +node_js: + - "0.10" + +sudo: false + +cache: + directories: + - resources + - node_modules + +after_success: + - which ./node_modules/coveralls/bin/coveralls.js && cat coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5c885f559f..3fe22bdc17 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -53,6 +53,10 @@ We're thrilled to receive pull requests of any kind. Anything from bug fix, test That said, please let us know what you're planning to do! For large changes always create a proposal. Maintainers will love to give you advice on building it and it keeps the app's design coherent. +### Pull Request Requirements: +- Tests +- [Signed Off](https://github.com/docker/docker/blob/master/CONTRIBUTING.md#sign-your-work) + ## Code Guidelines ### Javascript diff --git a/README.md b/README.md index 834ac194ec..f363f71fe7 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ -[![CircleCI](https://img.shields.io/circleci/project/kitematic/kitematic.svg)](https://circleci.com/gh/kitematic/kitematic/tree/master) +[![Build Status](https://travis-ci.org/kitematic/kitematic.svg?branch=master)](https://travis-ci.org/kitematic/kitematic) +[![Coverage Status](https://coveralls.io/repos/kitematic/kitematic/badge.svg?branch=master)](https://coveralls.io/r/kitematic/kitematic?branch=master) [![bitHound Score](https://app.bithound.io/kitematic/kitematic/badges/score.svg)](http://app.bithound.io/kitematic/kitematic) [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/kitematic/kitematic?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) diff --git a/circle.yml b/circle.yml index 0ddf9ac841..9933147169 100644 --- a/circle.yml +++ b/circle.yml @@ -5,3 +5,6 @@ dependencies: cache_directories: - "resources" - "node_modules" +notify: + webhooks: + - url: https://coveralls.io/webhook diff --git a/images/button-restart.png b/images/button-restart.png index 69f030d3ec..0cf9ae43ce 100644 Binary files a/images/button-restart.png and b/images/button-restart.png differ diff --git a/images/button-restart@2x.png b/images/button-restart@2x.png index c710010d35..bb62729cb3 100644 Binary files a/images/button-restart@2x.png and b/images/button-restart@2x.png differ diff --git a/images/button-terminal.png b/images/button-terminal.png index e64a22aa5e..cfcedd974f 100644 Binary files a/images/button-terminal.png and b/images/button-terminal.png differ diff --git a/images/button-terminal@2x.png b/images/button-terminal@2x.png index be2c92fa24..658786d3f6 100644 Binary files a/images/button-terminal@2x.png and b/images/button-terminal@2x.png differ diff --git a/images/button-view.png b/images/button-view.png index 4d67f928f4..0eb0c2e5bd 100644 Binary files a/images/button-view.png and b/images/button-view.png differ diff --git a/images/button-view@2x.png b/images/button-view@2x.png index a445545558..9a9e206961 100644 Binary files a/images/button-view@2x.png and b/images/button-view@2x.png differ diff --git a/package.json b/package.json index e657c0d092..34f55d73ad 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "Kitematic", - "version": "0.5.11", + "version": "0.5.13", "author": "Kitematic", "description": "Simple Docker Container management for Mac OS X.", "homepage": "https://kitematic.com/", @@ -12,7 +12,7 @@ "bugs": "https://github.com/kitematic/kitematic/issues", "scripts": { "start": "gulp", - "test": "jest", + "test": "jest --coverage", "release": "gulp release", "release:beta": "gulp release --beta", "lint": "jsxhint src && jsxhint browser", @@ -27,25 +27,30 @@ "jest": { "scriptPreprocessor": "/util/preprocessor.js", "setupEnvScriptFile": "/util/testenv.js", + "collectCoverage": true, + "testDirectoryName": "src", + "testPathIgnorePatterns": [ + "/node_modules/", + "^((?!-test).)*$" + ], "unmockedModulePathPatterns": [ + "stream", "tty", "net", "crypto", - "stream", + "/node_modules/.*JSONStream", "/node_modules/object-assign", "/node_modules/underscore", - "/node_modules/react", - "/node_modules/bluebird", - "/node_modules/babel" + "/node_modules/bluebird" ] }, "docker-version": "1.5.0", "docker-machine-version": "0.1.0-kitematic-0.5.10", "atom-shell-version": "0.21.3", - "virtualbox-version": "4.3.24", - "virtualbox-filename": "VirtualBox-4.3.24.pkg", + "virtualbox-version": "4.3.26", + "virtualbox-filename": "VirtualBox-4.3.26.pkg", "virtualbox-filename-win": "VirtualBox-4.3.26.exe", - "virtualbox-checksum": "100eee21df3808fc1b1e461e86f10b6d2a748935c5f31bc665f4ff0777e44a0b", + "virtualbox-checksum": "668f61c95efe37f8fc65cafe95b866fba64e37f2492dfc1e2b44a7ac3dcafa3b", "virtualbox-checksum-win": "9cb265babf307d825f5178693af95ffca077f80ae22cf43868c3538c159123ff", "dependencies": { "ansi-to-html": "0.3.0", @@ -53,7 +58,8 @@ "async": "^0.9.0", "bluebird": "^2.9.12", "bugsnag-js": "^2.4.7", - "dockerode": "^2.0.7", + "coveralls": "^2.11.2", + "dockerode": "^2.1.1", "exec": "0.2.0", "fs-extra": "^0.17.0", "fs-promise": "^0.3.1", @@ -81,6 +87,7 @@ "gulp-cssmin": "^0.1.6", "gulp-download-atom-shell": "0.0.4", "gulp-if": "^1.2.5", + "gulp-insert": "^0.4.0", "gulp-less": "^3.0.1", "gulp-livereload": "^3.8.0", "gulp-plumber": "^0.6.6", @@ -89,7 +96,7 @@ "gulp-shell": "^0.3.0", "gulp-sourcemaps": "^1.5.0", "gulp-util": "^3.0.4", - "jest-cli": "^0.4.0", + "jest-cli": "kitematic/jest", "jsxhint": "^0.12.1", "react-tools": "^0.12.2", "run-sequence": "^1.0.2" diff --git a/src/ContainerHome.react.js b/src/ContainerHome.react.js index 0bb752b999..4cb9df636b 100644 --- a/src/ContainerHome.react.js +++ b/src/ContainerHome.react.js @@ -74,7 +74,9 @@ var ContainerHome = React.createClass({ if (this.props.error) { body = (
-

There was a problem connecting to the Docker Engine in the VirtualBox VM.
This could be caused because this Mac is currently connected to a VPN, blocking access to the VM. If the issue persists, please file a ticket on our GitHub repo.

+

An error occurred:

+

{this.props.error.statusCode} {this.props.error.reason} - {this.props.error.json}

+

If you feel that this error is invalid, please file a ticket on our GitHub repo.

); diff --git a/src/ContainerHomeFolders.react.js b/src/ContainerHomeFolders.react.js index e20b95e1fd..aa1218f4a6 100644 --- a/src/ContainerHomeFolders.react.js +++ b/src/ContainerHomeFolders.react.js @@ -2,20 +2,46 @@ var _ = require('underscore'); var React = require('react/addons'); var RetinaImage = require('react-retina-image'); var path = require('path'); -var exec = require('exec'); +var shell = require('shell'); +var util = require('./Util'); var metrics = require('./Metrics'); var Router = require('react-router'); -var util = require('./Util'); +var ContainerStore = require('./ContainerStore'); var ContainerHomeFolder = React.createClass({ mixins: [Router.State, Router.Navigation], - handleClickFolder: function (path) { + handleClickFolder: function (hostVolume, containerVolume) { metrics.track('Opened Volume Directory', { from: 'home' }); - util.openPathOrUrl(path, function (err) { - if (err) { throw err; } - }); + + if (hostVolume.indexOf(process.env.HOME) === -1) { + var volumes = _.clone(this.props.container.Volumes); + var newHostVolume = path.join(util.home(), 'Kitematic', this.props.container.Name, containerVolume); + volumes[containerVolume] = newHostVolume; + var binds = _.pairs(volumes).map(function (pair) { + if(util.isWindows()) { + var home = util.home(); + home = home.charAt(0).toLowerCase() + home.slice(1); + home = '/' + home.replace(':', '').replace(/\\/g, '/'); + var fullPath = path.join(home, 'Kitematic', pair[1], pair[0]); + fullPath = fullPath.replace(/\\/g, '/'); + return fullPath + ':' + pair[0]; + } + return pair[1] + ':' + pair[0]; + }); + ContainerStore.updateContainer(this.props.container.Name, { + Binds: binds + }, function (err) { + if (err) { + console.log(err); + return; + } + shell.showItemInFolder(newHostVolume); + }); + } else { + shell.showItemInFolder(hostVolume); + } }, handleClickChangeFolders: function () { metrics.track('Viewed Volume Settings', { @@ -24,24 +50,21 @@ var ContainerHomeFolder = React.createClass({ this.transitionTo('containerSettingsVolumes', {name: this.getParams().name}); }, render: function () { - var folders; - if (this.props.container) { - var self = this; - folders = _.map(self.props.container.Volumes, function (val, key) { - var firstFolder = key.split(path.sep)[1]; - if (!val || val.indexOf(process.env.HOME) === -1) { - return; - } else { - return ( -
- -
{firstFolder}
-
- ); - } - }); + if (!this.props.container) { + return false; } - if (this.props.container && this.props.container.Volumes && _.keys(this.props.container.Volumes).length > 0 && this.props.container.State.Running) { + + var folders = _.map(this.props.container.Volumes, (val, key) => { + var firstFolder = key.split(path.sep)[1]; + return ( +
+ +
{firstFolder}
+
+ ); + }); + + if (this.props.container.Volumes && _.keys(this.props.container.Volumes).length > 0 && this.props.container.State.Running) { return (

Edit Files

@@ -52,9 +75,7 @@ var ContainerHomeFolder = React.createClass({
); } else { - return ( -
- ); + return false; } } }); diff --git a/src/ContainerHomeLogs.react.js b/src/ContainerHomeLogs.react.js index a13f310c3c..d400a48d72 100644 --- a/src/ContainerHomeLogs.react.js +++ b/src/ContainerHomeLogs.react.js @@ -52,6 +52,9 @@ var ContainerHomeLogs = React.createClass({ var logs = this.state.logs.map(function (l, i) { return

; }); + if (logs.length === 0) { + logs = "No logs for this container."; + } return (

Logs

diff --git a/src/ContainerHomePreview.react.js b/src/ContainerHomePreview.react.js index 91995c82ef..880462b129 100644 --- a/src/ContainerHomePreview.react.js +++ b/src/ContainerHomePreview.react.js @@ -38,9 +38,6 @@ var ContainerHomePreview = React.createClass({ }); } }, - componentDidUpdate: function () { - this.reload(); - }, componentWillUnmount: function() { clearInterval(this.timer); }, diff --git a/src/ContainerList.react.js b/src/ContainerList.react.js index 1d6d25a034..d2954dc603 100644 --- a/src/ContainerList.react.js +++ b/src/ContainerList.react.js @@ -9,8 +9,14 @@ var ContainerList = React.createClass({ render: function () { var self = this; var containers = this.props.containers.map(function (container) { + var containerId = container.Id; + if (!containerId && container.State.Downloading) { + // Fall back to the container image name when there is no id. (when the + // image is downloading). + containerId = container.Image; + } return ( - + ); }); var newItem; diff --git a/src/ContainerLogs.react.js b/src/ContainerLogs.react.js index cdde86f4d2..e20910cc49 100644 --- a/src/ContainerLogs.react.js +++ b/src/ContainerLogs.react.js @@ -45,6 +45,9 @@ var ContainerLogs = React.createClass({ var logs = this.state.logs.map(function (l, i) { return

; }); + if (logs.length === 0) { + logs = "No logs for this container."; + } return (
{logs} diff --git a/src/ContainerSettingsVolumes.react.js b/src/ContainerSettingsVolumes.react.js index 86d2aec584..0b6d2ad614 100644 --- a/src/ContainerSettingsVolumes.react.js +++ b/src/ContainerSettingsVolumes.react.js @@ -2,7 +2,6 @@ var _ = require('underscore'); var React = require('react/addons'); var Router = require('react-router'); var remote = require('remote'); -var exec = require('exec'); var dialog = remote.require('dialog'); var metrics = require('./Metrics'); var ContainerStore = require('./ContainerStore'); @@ -32,6 +31,21 @@ var ContainerSettingsVolumes = React.createClass({ } }); }, + handleRemoveVolumeClick: function (dockerVol) { + metrics.track('Removed Volume Directory', { + from: 'settings' + }); + var volumes = _.clone(this.props.container.Volumes); + delete volumes[dockerVol]; + var binds = _.pairs(volumes).map(function (pair) { + return pair[1] + ':' + pair[0]; + }); + ContainerStore.updateContainer(this.props.container.Name, { + Binds: binds + }, function (err) { + if (err) { console.log(err); } + }); + }, handleOpenVolumeClick: function (path) { metrics.track('Opened Volume Directory', { from: 'settings' @@ -51,6 +65,7 @@ var ContainerSettingsVolumes = React.createClass({ No Folder Change + Remove ); } else { @@ -58,6 +73,7 @@ var ContainerSettingsVolumes = React.createClass({ {val.replace(process.env.HOME, '~')} Change + Remove ); } diff --git a/src/ContainerStore.js b/src/ContainerStore.js index 3ced6dbafb..109a022d19 100644 --- a/src/ContainerStore.js +++ b/src/ContainerStore.js @@ -1,14 +1,12 @@ var _ = require('underscore'); var EventEmitter = require('events').EventEmitter; var async = require('async'); -var path = require('path'); var assign = require('object-assign'); var docker = require('./Docker'); var metrics = require('./Metrics'); var registry = require('./Registry'); var logstore = require('./LogStore'); var bugsnag = require('bugsnag-js'); -var util = require('./Util'); var _placeholders = {}; var _containers = {}; @@ -58,6 +56,12 @@ var ContainerStore = assign(Object.create(EventEmitter.prototype), { var data = JSON.parse(str); console.log(data); + if (data.error) { + _error = data.error; + callback(data.error); + return; + } + if (data.status && (data.status === 'Pulling dependent layers' || data.status.indexOf('already being pulled by another client') !== -1)) { blockedCallback(); return; @@ -84,7 +88,8 @@ var ContainerStore = assign(Object.create(EventEmitter.prototype), { progressCallback(totalProgress); }); stream.on('end', function () { - callback(); + callback(_error); + _error = null; }); }); }); @@ -92,48 +97,22 @@ var ContainerStore = assign(Object.create(EventEmitter.prototype), { }, _startContainer: function (name, containerData, callback) { var self = this; - docker.client().getImage(containerData.Image).inspect(function (err, data) { + var binds = containerData.Binds || []; + var startopts = { + Binds: binds + }; + if (containerData.NetworkSettings && containerData.NetworkSettings.Ports) { + startopts.PortBindings = containerData.NetworkSettings.Ports; + } else{ + startopts.PublishAllPorts = true; + } + var container = docker.client().getContainer(name); + container.start(startopts, function (err) { if (err) { callback(err); return; } - var binds = containerData.Binds || []; - if (data.Config.Volumes) { - _.each(data.Config.Volumes, function (value, key) { - var existingBind = _.find(binds, b => { - return b.indexOf(':' + key) !== -1; - }); - if (!existingBind) { - var home = util.home(); - - if(util.isWindows()) { - home = home.charAt(0).toLowerCase() + home.slice(1); - home = "/" + home.replace(':', '').replace(/\\/g, '/'); - var fullPath = path.join(home, 'Kitematic', name, key); - fullPath = fullPath.replace(/\\/g, '/'); - binds.push(fullPath + ':' + key); - } else { - binds.push(path.join(home, 'Kitematic', name, key) + ':' + key); - } - } - }); - } - var startopts = { - Binds: binds - }; - if (containerData.NetworkSettings && containerData.NetworkSettings.Ports) { - startopts.PortBindings = containerData.NetworkSettings.Ports; - } else{ - startopts.PublishAllPorts = true; - } - var container = docker.client().getContainer(name); - container.start(startopts, function (err) { - if (err) { - callback(err); - return; - } - self.fetchContainer(name, callback); - }); + self.fetchContainer(name, callback); }); }, _createContainer: function (name, containerData, callback) { @@ -147,6 +126,9 @@ var ContainerStore = assign(Object.create(EventEmitter.prototype), { if (containerData.Config && containerData.Config.Image) { containerData.Image = containerData.Config.Image; } + if (!containerData.Env && containerData.Config && containerData.Config.Env) { + containerData.Env = containerData.Config.Env; + } existing.kill(function () { existing.remove(function () { docker.client().createContainer(containerData, function (err) { @@ -154,11 +136,7 @@ var ContainerStore = assign(Object.create(EventEmitter.prototype), { callback(err, null); return; } - if (containerData.State && !containerData.State.Running) { - self.fetchContainer(containerData.name, callback); - } else { - self._startContainer(name, containerData, callback); - } + self._startContainer(name, containerData, callback); }); }); }); diff --git a/src/ImageCard.react.js b/src/ImageCard.react.js index b132aec25f..659d1407e6 100644 --- a/src/ImageCard.react.js +++ b/src/ImageCard.react.js @@ -5,6 +5,7 @@ var ContainerStore = require('./ContainerStore'); var metrics = require('./Metrics'); var OverlayTrigger = require('react-bootstrap').OverlayTrigger; var Tooltip = require('react-bootstrap').Tooltip; +var util = require('./Util'); var ImageCard = React.createClass({ getInitialState: function () { @@ -30,16 +31,33 @@ var ImageCard = React.createClass({ handleTagOverlayClick: function (name) { var $tagOverlay = $(this.getDOMNode()).find('.tag-overlay'); $tagOverlay.fadeIn(300); - $.get('https://registry.hub.docker.com/v1/repositories/' + name + '/tags', function (result) { + $.get('https://registry.hub.docker.com/v1/repositories/' + name + '/tags', result => { this.setState({ tags: result }); - }.bind(this)); + }); }, handleCloseTagOverlay: function () { var $tagOverlay = $(this.getDOMNode()).find('.tag-overlay'); $tagOverlay.fadeOut(300); }, + handleRepoClick: function () { + var $repoUri = 'https://registry.hub.docker.com/'; + if (this.props.image.is_official) { + $repoUri = $repoUri + "_/"; + } else { + $repoUri = $repoUri + "u/"; + } + util.exec(['open', $repoUri + this.props.image.name]); + }, + componentDidMount: function() { + $.get('https://registry.hub.docker.com/v1/repositories/' + this.props.image.name + '/tags', result => { + this.setState({ + tags: result, + chosenTag: result[0].name + }); + }); + }, render: function () { var self = this; var name; @@ -57,8 +75,8 @@ var ImageCard = React.createClass({ name = (
{namespace}
- {this.props.image.name}}> - {repo} + View on DockerHub}> + {repo}
); @@ -66,8 +84,8 @@ var ImageCard = React.createClass({ name = (
{namespace}
- {this.props.image.name}}> - {repo} + View on DockerHub}> + {repo}
); diff --git a/__tests__/SetupStore-test.js b/src/SetupStore-test.js similarity index 93% rename from __tests__/SetupStore-test.js rename to src/SetupStore-test.js index b26471e2c8..b8ddc79892 100644 --- a/__tests__/SetupStore-test.js +++ b/src/SetupStore-test.js @@ -1,10 +1,9 @@ -jest.dontMock('../src/SetupStore'); -var setupStore = require('../src/SetupStore'); -var virtualBox = require('../src/VirtualBox'); -var util = require('../src/Util'); -var machine = require('../src/DockerMachine'); -var setupUtil = require('../src/SetupUtil'); -var Promise = require('bluebird'); +jest.dontMock('./SetupStore'); +var setupStore = require('./SetupStore'); +var virtualBox = require('./VirtualBox'); +var util = require('./Util'); +var machine = require('./DockerMachine'); +var setupUtil = require('./SetupUtil'); describe('SetupStore', function () { describe('download step', function () { diff --git a/src/SetupStore.js b/src/SetupStore.js index 87acb0189a..f1c93cdf30 100644 --- a/src/SetupStore.js +++ b/src/SetupStore.js @@ -180,7 +180,7 @@ var SetupStore = assign(Object.create(EventEmitter.prototype), { var required = {}; var vboxfile = path.join(util.supportDir(), setupUtil.virtualBoxFileName()); var vboxNeedsInstall = !virtualBox.installed(); - + required.download = vboxNeedsInstall && (!fs.existsSync(vboxfile) || setupUtil.checksum(vboxfile) !== setupUtil.virtualBoxChecksum()); required.install = vboxNeedsInstall || setupUtil.needsBinaryFix(); required.init = required.install || !(yield machine.exists()) || (yield machine.state()) !== 'Running' || !isoversion || setupUtil.compareVersions(isoversion, packagejson['docker-version']) < 0; @@ -270,6 +270,8 @@ var SetupStore = assign(Object.create(EventEmitter.prototype), { step: _currentStep, message: err.message }); + console.log(err); + console.log(err.stack); bugsnag.notify('SetupError', err.message, { error: err, step: _currentStep diff --git a/src/SetupUtil.js b/src/SetupUtil.js index 9dfed3c7dc..bd3cf5a9f8 100644 --- a/src/SetupUtil.js +++ b/src/SetupUtil.js @@ -33,9 +33,9 @@ var SetupUtil = { return; } - yield fs.chown(util.binsPath(), process.getuid(), '80'); - yield fs.chown(util.dockerBinPath(), process.getuid(), '80'); - yield fs.chown(util.dockerMachineBinPath(), process.getuid(), '80'); + yield fs.chown(util.binsPath(), process.getuid(), 80); + yield fs.chown(util.dockerBinPath(), process.getuid(), 80); + yield fs.chown(util.dockerMachineBinPath(), process.getuid(), 80); return Promise.resolve(); }), installVirtualBoxCmd: Promise.coroutine(function* () { diff --git a/__tests__/Virtualbox-test.js b/src/Virtualbox-test.js similarity index 76% rename from __tests__/Virtualbox-test.js rename to src/Virtualbox-test.js index 1a8efa7aab..567210a08a 100644 --- a/__tests__/Virtualbox-test.js +++ b/src/Virtualbox-test.js @@ -1,7 +1,6 @@ -jest.dontMock('../src/VirtualBox'); -var virtualBox = require('../src/VirtualBox'); -var util = require('../src/Util'); -var Promise = require('bluebird'); +jest.dontMock('./VirtualBox'); +var virtualBox = require('./VirtualBox'); +var util = require('./Util'); describe('VirtualBox', function () { it('returns the right command', function () { diff --git a/styles/left-panel.less b/styles/left-panel.less index ecba94e97c..2aa98270fc 100644 --- a/styles/left-panel.less +++ b/styles/left-panel.less @@ -201,7 +201,7 @@ left: -20px; .at2x('runningwave.png', 20px, 20px); -webkit-animation-name: translatewave; - -webkit-animation-duration: 8.0s; + -webkit-animation-duration: 7.0s; -webkit-animation-iteration-count: infinite; -webkit-animation-timing-function: linear; } diff --git a/styles/new-container.less b/styles/new-container.less index 0a201ae8f3..a00f9c3eb3 100644 --- a/styles/new-container.less +++ b/styles/new-container.less @@ -186,6 +186,7 @@ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; + text-decoration: underline; } } .description { diff --git a/styles/right-panel.less b/styles/right-panel.less index 2d5af45524..384b3e8934 100644 --- a/styles/right-panel.less +++ b/styles/right-panel.less @@ -25,7 +25,6 @@ .action { display: inline-block; position: relative; - top: 10px; &.disabled { opacity: 0.3; } @@ -36,11 +35,11 @@ .btn-label { position: absolute; color: @brand-action; - font-size: 10px; + font-size: 9px; width: 200px; - top: 30px; + top: 45px; &.view { - left: 6px; + left: 7px; //left: 0px; } &.restart {