diff --git a/.gitignore b/.gitignore index 3cd3b8edff..d35cfedc57 100644 --- a/.gitignore +++ b/.gitignore @@ -5,16 +5,10 @@ node_modules npm-debug.log # Signing Identity -script/identity +identity # Resources -resources/virtualbox-*.pkg resources/boot2docker* -resources/mongod -resources/MONGOD_LICENSE.txt -resources/node -resources/NODE_LICENSE.txt -resources/settings.json # Cache cache diff --git a/README.md b/README.md index e9c13b3236..ae738c4c2b 100755 --- a/README.md +++ b/README.md @@ -15,15 +15,15 @@ Kitematic's documentation and other information can be found at [http://kitemati ### Development - `sudo npm install -g less` -- `./script/npm install` +- `npm install` To run the app in development: -- `./script/gulp` +- `npm start` ### Building the Mac OS X Package -- `./script/release` +- `npm run release` ## Uninstalling diff --git a/app/ContainerDetails.react.js b/app/ContainerDetails.react.js index 73e995b24a..a126de1f9c 100644 --- a/app/ContainerDetails.react.js +++ b/app/ContainerDetails.react.js @@ -1,8 +1,7 @@ var _ = require('underscore'); -var React = require('react'); +var $ = require('jquery'); +var React = require('react/addons'); var Router = require('react-router'); -var Convert = require('ansi-to-html'); -var convert = new Convert(); var ContainerStore = require('./ContainerStore'); var docker = require('./docker'); var exec = require('exec'); @@ -17,85 +16,70 @@ var RouteHandler = Router.RouteHandler; var ContainerDetails = React.createClass({ mixins: [Router.State], + _oldHeight: 0, + PAGE_LOGS: 'logs', + PAGE_SETTINGS: 'settings', getInitialState: function () { return { - logs: [] + logs: [], + page: this.PAGE_LOGS }; }, componentWillReceiveProps: function () { - this.update(); this.setState({ - logs: [] + page: this.PAGE_LOGS }); - var self = this; - var logs = []; - var index = 0; - docker.client().getContainer(this.getParams().name).logs({ - follow: false, - stdout: true, - stderr: true, - timestamps: true - }, function (err, stream) { - stream.setEncoding('utf8'); - stream.on('data', function (buf) { - // Every other message is a header - if (index % 2 === 1) { - var time = buf.substr(0,buf.indexOf(' ')); - var msg = buf.substr(buf.indexOf(' ')+1); - logs.push(convert.toHtml(self._escapeHTML(msg))); - } - index += 1; - }); - stream.on('end', function (buf) { - self.setState({logs: logs}); - docker.client().getContainer(self.getParams().name).logs({ - follow: true, - stdout: true, - stderr: true, - timestamps: true, - tail: 0 - }, function (err, stream) { - stream.setEncoding('utf8'); - stream.on('data', function (buf) { - // Every other message is a header - if (index % 2 === 1) { - var time = buf.substr(0,buf.indexOf(' ')); - var msg = buf.substr(buf.indexOf(' ')+1); - logs.push(convert.toHtml(self._escapeHTML(msg))); - self.setState({logs: logs}); - } - index += 1; - }); - }); - }); - }); - }, - componentWillMount: function () { - this.update(); + ContainerStore.fetchLogs(this.getParams().name, function () { + this.updateLogs(); + }.bind(this)); }, componentDidMount: function () { - ContainerStore.addChangeListener(ContainerStore.CONTAINERS, this.update); - ContainerStore.addChangeListener(ContainerStore.PROGRESS, this.update); + ContainerStore.on(ContainerStore.SERVER_PROGRESS_EVENT, this.updateProgress); + ContainerStore.on(ContainerStore.SERVER_LOGS_EVENT, this.updateLogs); }, componentWillUnmount: function () { - ContainerStore.removeChangeListener(ContainerStore.CONTAINERS, this.update); - ContainerStore.removeChangeListener(ContainerStore.PROGRESS, this.update); + ContainerStore.removeListener(ContainerStore.SERVER_PROGRESS_EVENT, this.updateProgress); + ContainerStore.removeListener(ContainerStore.SERVER_LOGS_EVENT, this.updateLogs); }, - update: function () { - var name = this.getParams().name; + componentDidUpdate: function () { + var parent = $('.details-logs'); + if (!parent.length) { + return; + } + if (parent.scrollTop() >= this._oldHeight) { + parent.stop(); + parent.scrollTop(parent[0].scrollHeight - parent.height()); + } + this._oldHeight = parent[0].scrollHeight - parent.height(); + }, + updateLogs: function (name) { + if (name && name !== this.getParams().name) { + return; + } this.setState({ - container: ContainerStore.container(name), - progress: ContainerStore.progress(name) + logs: ContainerStore.logs(this.getParams().name) }); }, - _escapeHTML: function (html) { - var text = document.createTextNode(html); - var div = document.createElement('div'); - div.appendChild(text); - return div.innerHTML; + updateProgress: function (name) { + console.log('progress', name, ContainerStore.progress(name)); + if (name === this.getParams().name) { + this.setState({ + progress: ContainerStore.progress(name) + }); + } + }, + showLogs: function () { + this.setState({ + page: this.PAGE_LOGS + }); + }, + showSettings: function () { + this.setState({ + page: this.PAGE_SETTINGS + }); }, handleClick: function (name) { - var container = this.state.container; + var container = this.props.container; boot2docker.ip(function (err, ip) { var ports = _.map(container.NetworkSettings.Ports, function (value, key) { var portProtocolPair = key.split('/'); @@ -113,7 +97,6 @@ var ContainerDetails = React.createClass({ } return res; }); - console.log(ports); exec(['open', ports[0].url], function (err) { if (err) { throw err; } }); @@ -130,28 +113,19 @@ var ContainerDetails = React.createClass({ return

; }); - if (!this.state.container) { + if (!this.props.container) { return false; } var state; - if (this.state.container.State.Running) { + if (this.props.container.State.Running) { state =

running

; - } else if (this.state.container.State.Restarting) { + } else if (this.props.container.State.Restarting) { state =

restarting

; - } else if (this.state.container.State.Paused) { + } else if (this.props.container.State.Paused) { state =

paused

; - } - - var progress; - if (this.state.progress > 0 && this.state.progress != 1) { - progress = ( -
- -
- ); - } else { - progress =
; + } else if (this.props.container.State.Downloading) { + state =

downloading

; } var button; @@ -161,42 +135,75 @@ var ContainerDetails = React.createClass({ button = View; } - var name = this.state.container.Name.replace('/', ''); - var image = this.state.container.Config.Image; + var body; + if (this.props.container.State.Downloading) { + body = ( +
+ +
+ ); + } else { + if (this.state.page === this.PAGE_LOGS) { + body = ( +
+
+ {logs} +
+
+ ); + } else { + body = ( +
+
+
+
+ ); + } + } + + var textButtonClasses = React.addons.classSet({ + 'btn': true, + 'btn-action': true, + 'only-icon': true, + 'active': this.state.page === this.PAGE_LOGS + }); + + var gearButtonClass = React.addons.classSet({ + 'btn': true, + 'btn-action': true, + 'only-icon': true, + 'active': this.state.page === this.PAGE_SETTINGS + }); + + var name = this.props.container.Name; + var image = this.props.container.Config.Image; return (
-

{name}

{state}

Image

{image}

-
-
-
- View - +
+

{name}

{state}

Image

{image}

-
- Volume -
-
- Restart -
-
- Terminal -
-
-
-
- - -
-
- {progress} -
-

Container Logs

-
- {logs} +
+
+ View +
+ + + +
+ + +
+ {body}
); } diff --git a/app/ContainerList.react.js b/app/ContainerList.react.js index 81ade25892..0d00fa0e40 100644 --- a/app/ContainerList.react.js +++ b/app/ContainerList.react.js @@ -7,40 +7,16 @@ var Modal = require('react-bootstrap/Modal'); var RetinaImage = require('react-retina-image'); var ModalTrigger = require('react-bootstrap/ModalTrigger'); var ContainerModal = require('./ContainerModal.react'); -var ContainerStore = require('./ContainerStore'); var Header = require('./Header.react'); var docker = require('./docker'); -var Link = Router.Link; -var RouteHandler = Router.RouteHandler; -var Navigation= Router.Navigation; - var ContainerList = React.createClass({ - getInitialState: function () { - return { - containers: [] - }; - }, - componentDidMount: function () { - this.updateContainers(); - ContainerStore.addChangeListener(ContainerStore.CONTAINERS, this.updateContainers); - }, componentWillMount: function () { this._start = Date.now(); }, - componentWillUnmount: function () { - ContainerStore.removeChangeListener(ContainerStore.CONTAINERS, this.updateContainers); - }, - updateContainers: function () { - // Sort by name - var containers = _.values(ContainerStore.containers()).sort(function (a, b) { - return a.Name.localeCompare(b.Name); - }); - this.setState({containers: containers}); - }, render: function () { var self = this; - var containers = this.state.containers.map(function (container) { + var containers = this.props.containers.map(function (container) { var downloadingImage = null, downloading = false; var env = container.Config.Env; if (env.length) { @@ -76,22 +52,20 @@ var ContainerList = React.createClass({ state =
; } - var name = container.Name.replace('/', ''); - return ( - +
  • {state}
    - {name} + {container.Name}
    {imageName}
  • - +
    ); }); return ( diff --git a/app/ContainerModal.react.js b/app/ContainerModal.react.js index c8778663d0..8e8c348132 100644 --- a/app/ContainerModal.react.js +++ b/app/ContainerModal.react.js @@ -1,36 +1,51 @@ var async = require('async'); var $ = require('jquery'); -var React = require('react'); -var Router = require('react-router'); +var React = require('react/addons'); var Modal = require('react-bootstrap/Modal'); var RetinaImage = require('react-retina-image'); var ContainerStore = require('./ContainerStore'); var OverlayTrigger = require('react-bootstrap/OverlayTrigger'); var Popover = require('react-bootstrap/Popover'); -var Navigation = Router.Navigation; - var ContainerModal = React.createClass({ - mixins: [Navigation], _searchRequest: null, getInitialState: function () { return { query: '', - results: [], - recommended: ContainerStore.recommended() + results: ContainerStore.recommended(), + loading: false, }; }, componentDidMount: function () { this.refs.searchInput.getDOMNode().focus(); + ContainerStore.on(ContainerStore.SERVER_RECOMMENDED_EVENT, this.update); + }, + update: function () { + if (!this.state.query.length) { + this.setState({ + results: ContainerStore.recommended() + }); + } }, search: function (query) { + if (this._searchRequest) { + this._searchRequest.abort(); + this._searchRequest = null; + } + + this.setState({ + loading: true + }); + var self = this; this._searchRequest = $.get('https://registry.hub.docker.com/v1/search?q=' + query, function (result) { - self._searchRequest.abort(); + self.setState({ + query: query, + loading: false + }); self._searchRequest = null; if (self.isMounted()) { self.setState(result); - console.log(result); } }); }, @@ -41,85 +56,106 @@ var ContainerModal = React.createClass({ return; } - if (this._searchRequest) { - console.log('Cancel'); - this._searchRequest.abort(); - this._searchRequest = null; - } clearTimeout(this.timeout); - var self = this; - this.timeout = setTimeout(function () { - self.search(query); - }, 250); + if (!query.length) { + this.setState({ + query: query, + results: ContainerStore.recommended() + }); + } else { + var self = this; + this.timeout = setTimeout(function () { + self.search(query); + }, 200); + } }, handleClick: function (event) { var name = event.target.getAttribute('name'); var self = this; ContainerStore.create(name, 'latest', function (err, containerName) { - // this.transitionTo('containers', {container: containerName}); self.props.onRequestHide(); - }.bind(this)); + }); }, render: function () { var self = this; + var data = this.state.results.slice(0, 7); - var data; - if (this.state.query) { - data = this.state.results.splice(0, 7); - } else { - data = this.state.recommended; - } - var results = data.map(function (r) { - var name; - if (r.is_official) { - name = {r.name}; - } else { - name = {r.name}; - } - return ( -
  • -
    -
    - {name} + var results; + if (data.length) { + var items = data.map(function (r) { + var name; + if (r.is_official) { + name = {r.name}; + } else { + name = {r.name}; + } + return ( +
  • +
    +
    + {name} +
    +
    +
    +
    {r.star_count}
    +
    -
    -
    -
    {r.star_count}
    +
    +
    + Create + +
    -
    -
    -
    - Create - -
    -
    -
  • + + ); + }); + + results = ( +
    +
      + {items} +
    +
    ); - }); - - var title; - if (this.state.query) { - title =

    Results

    ; } else { - title =

    Recommended

    ; + results = ( +
    +

    + No Results +

    +
    + ); } + var title = this.state.query ? 'Results' : 'Recommended'; + var loadingClasses = React.addons.classSet({ + hidden: !this.state.loading, + loading: true + }); + var magnifierClasses = React.addons.classSet({ + hidden: this.state.loading, + icon: true, + 'icon-magnifier': true, + 'search-icon': true + }); + return (
    - -
    +
    + +
    + +
    An image is a template which a container can be created from.}> What's an image?
    - {title} -
      - {results} -
    +
    {title}
    + {results}
    ); diff --git a/app/Header.react.js b/app/Header.react.js index b69eb6f18c..e12d1199f4 100644 --- a/app/Header.react.js +++ b/app/Header.react.js @@ -2,6 +2,11 @@ var React = require('react/addons'); var remote = require('remote'); var Header = React.createClass({ + getInitialState: function () { + return { + fullscreen: false + }; + }, componentDidMount: function () { document.addEventListener('keyup', this.handleDocumentKeyUp, false); }, @@ -22,14 +27,16 @@ var Header = React.createClass({ }, handleFullscreen: function () { remote.getCurrentWindow().setFullScreen(!remote.getCurrentWindow().isFullScreen()); - this.forceUpdate(); + this.setState({ + fullscreen: remote.getCurrentWindow().isFullScreen() + }); }, handleFullscreenHover: function () { this.update(); }, render: function () { var buttons; - if (remote.getCurrentWindow().isFullScreen()) { + if (this.state.fullscreen) { return (
    diff --git a/app/NoContainers.react.js b/app/NoContainers.react.js new file mode 100644 index 0000000000..4d15223be6 --- /dev/null +++ b/app/NoContainers.react.js @@ -0,0 +1,14 @@ +var React = require('react/addons'); +var RetinaImage = require('react-retina-image'); + +var NoContainers = React.createClass({ + render: function () { + return ( +
    +

    No Containers

    +
    + ); + } +}); + +module.exports = NoContainers; diff --git a/app/Setup.react.js b/app/Setup.react.js index f04f58a969..9f8661eb9e 100644 --- a/app/Setup.react.js +++ b/app/Setup.react.js @@ -1,4 +1,4 @@ -var React = require('react'); +var React = require('react/addons'); var Router = require('react-router'); var Radial = require('./Radial.react.js'); var async = require('async'); @@ -134,6 +134,12 @@ var setupSteps = [ var Setup = React.createClass({ mixins: [ Router.Navigation ], + getInitialState: function () { + return { + message: '', + progress: 0 + }; + }, render: function () { var radial; if (this.state.progress) { diff --git a/app/images/downloading-arrow-white.png b/app/images/downloading-arrow-white.png new file mode 100644 index 0000000000..a986393f10 Binary files /dev/null and b/app/images/downloading-arrow-white.png differ diff --git a/app/images/downloading-arrow-white@2x.png b/app/images/downloading-arrow-white@2x.png new file mode 100644 index 0000000000..a2de93b9ec Binary files /dev/null and b/app/images/downloading-arrow-white@2x.png differ diff --git a/app/images/downloading-arrow.png b/app/images/downloading-arrow.png index dfca72b6ae..41002b231a 100644 Binary files a/app/images/downloading-arrow.png and b/app/images/downloading-arrow.png differ diff --git a/app/images/downloading-arrow@2x.png b/app/images/downloading-arrow@2x.png index 7f589b2596..d354d6eb4f 100644 Binary files a/app/images/downloading-arrow@2x.png and b/app/images/downloading-arrow@2x.png differ diff --git a/app/images/downloading-white.png b/app/images/downloading-white.png new file mode 100644 index 0000000000..3344a6dbd5 Binary files /dev/null and b/app/images/downloading-white.png differ diff --git a/app/images/downloading-white@2x.png b/app/images/downloading-white@2x.png new file mode 100644 index 0000000000..21a062a211 Binary files /dev/null and b/app/images/downloading-white@2x.png differ diff --git a/app/images/downloading.png b/app/images/downloading.png index 2dd4acfa27..fb9e63be82 100644 Binary files a/app/images/downloading.png and b/app/images/downloading.png differ diff --git a/app/images/downloading@2x.png b/app/images/downloading@2x.png index b3d8b3a16d..d974261408 100644 Binary files a/app/images/downloading@2x.png and b/app/images/downloading@2x.png differ diff --git a/app/images/loading.png b/app/images/loading.png index 6d8e975938..6217c07530 100644 Binary files a/app/images/loading.png and b/app/images/loading.png differ diff --git a/app/images/loading@2x.png b/app/images/loading@2x.png index 87a14647c4..102eae7805 100644 Binary files a/app/images/loading@2x.png and b/app/images/loading@2x.png differ diff --git a/app/images/roundedcontainer.png b/app/images/roundedcontainer.png new file mode 100644 index 0000000000..897e09e792 Binary files /dev/null and b/app/images/roundedcontainer.png differ diff --git a/app/images/roundedcontainer@2x.png b/app/images/roundedcontainer@2x.png new file mode 100644 index 0000000000..a220e3d093 Binary files /dev/null and b/app/images/roundedcontainer@2x.png differ diff --git a/app/images/stopped-white.png b/app/images/stopped-white.png new file mode 100644 index 0000000000..ec950d5c24 Binary files /dev/null and b/app/images/stopped-white.png differ diff --git a/app/images/stopped-white@2x.png b/app/images/stopped-white@2x.png new file mode 100644 index 0000000000..64110715fe Binary files /dev/null and b/app/images/stopped-white@2x.png differ diff --git a/app/main.js b/app/main.js index aa765b88f4..e508dc3261 100644 --- a/app/main.js +++ b/app/main.js @@ -1,15 +1,16 @@ +var module = require('module'); +require.main.paths.splice(0, 0, process.env.NODE_PATH); + +var Bugsnag = require('bugsnag-js'); var React = require('react'); var Router = require('react-router'); var RetinaImage = require('react-retina-image'); -var Raven = require('raven'); var async = require('async'); -var docker = require('./docker.js'); -var boot2docker = require('./boot2docker.js'); -var Setup = require('./Setup.react'); -var Containers = require('./Containers.react'); -var ContainerDetails = require('./ContainerDetails.react'); +var docker = require('./docker'); +var router = require('./router'); +var boot2docker = require('./boot2docker'); var ContainerStore = require('./ContainerStore'); -var Radial = require('./Radial.react'); +var app = require('remote').require('app'); var Route = Router.Route; var NotFoundRoute = Router.NotFoundRoute; @@ -17,53 +18,23 @@ var DefaultRoute = Router.DefaultRoute; var Link = Router.Link; var RouteHandler = Router.RouteHandler; -var App = React.createClass({ - render: function () { - return ( - - ); - } -}); +Bugsnag.apiKey = 'fc51aab02ce9dd1bb6ebc9fe2f4d43d7'; +Bugsnag.autoNotify = true; +Bugsnag.releaseStage = process.env.NODE_ENV === 'development' ? 'development' : 'production'; +Bugsnag.notifyReleaseStages = []; +Bugsnag.appVersion = app.getVersion(); -var NoContainers = React.createClass({ - render: function () { - return ( -
    - No Containers -
    - ); - } -}); - -var routes = ( - - - - - - - - - - -); - -boot2docker.ip(function (err, ip) { - if (window.location.hash !== '#/') { +if (window.location.hash === '#/') { + router.run(function (Handler) { + React.render(, document.body); + }); +} else { + boot2docker.ip(function (err, ip) { docker.setHost(ip); ContainerStore.init(function () { - Router.run(routes, function (Handler) { + router.run(function (Handler) { React.render(, document.body); }); }); - } else { - Router.run(routes, function (Handler) { - React.render(, document.body); - }); - } -}); - -if (process.env.NODE_ENV !== 'development') { - Raven.config('https://0a5f032d745d4acaae94ce46f762c586@app.getsentry.com/35057', { - }).install(); + }); } diff --git a/app/router.js b/app/router.js new file mode 100644 index 0000000000..d419f62176 --- /dev/null +++ b/app/router.js @@ -0,0 +1,8 @@ +var Router = require('react-router'); +var routes = require('./routes'); + +var router = Router.create({ + routes: routes +}); + +module.exports = router; diff --git a/app/routes.js b/app/routes.js new file mode 100644 index 0000000000..88dcf91bcf --- /dev/null +++ b/app/routes.js @@ -0,0 +1,33 @@ +var React = require('react/addons'); +var Setup = require('./Setup.react'); +var Containers = require('./Containers.react'); +var ContainerDetails = require('./ContainerDetails.react'); +var NoContainers = require('./NoContainers.react'); +var Router = require('react-router'); + +var Route = Router.Route; +var DefaultRoute = Router.DefaultRoute; +var RouteHandler = Router.RouteHandler; + +var App = React.createClass({ + render: function () { + return ( + + ); + } +}); + +var routes = ( + + + + + + + + + + +); + +module.exports = routes; diff --git a/app/styles/containers.less b/app/styles/containers.less index 94155a28c3..37b7000149 100644 --- a/app/styles/containers.less +++ b/app/styles/containers.less @@ -22,7 +22,7 @@ display: flex; border-bottom: 1px solid transparent; transition: border-bottom 0.25s; - padding: 0px 28px 0px 10px; + padding: 0px 10px 0px 10px; &.sep { border-bottom: 1px solid #eee; @@ -32,20 +32,26 @@ h4 { align-self: flex-start; padding: 0 24px; - margin: 10px 0 0; + margin: 14px 0 0; display: inline-block; + font-size: 14px; position: relative; } .create { flex: 1 auto; text-align: right; - - .wrapper { - text-align: center; - display: inline-block; + .btn { + margin-top: 4px; + padding: 4px 7px; + font-size: 16px; + position: relative; + .icon { + position: relative; + top: 3px; + left: 1px; + } } - } } @@ -64,6 +70,7 @@ margin: 0; min-width: 240px; padding: 0; + margin-top: 4px; display: flex; flex-direction: column; @@ -72,9 +79,12 @@ color: inherit; flex-shrink: 0; cursor: default; - margin: 0px 5px 0px 20px; + margin: 0px 3px 0px 8px; + outline: none; + padding: 4px 5px; &.active { +<<<<<<< HEAD background: @brand-primary; li { .name { @@ -111,6 +121,38 @@ } .state-stopped { .at2x('still-white.png', 20px, 20px); +======= + li { + border-bottom: none; + border-radius: 40px; + background: @brand-primary; + .name { + color: white; + } + .image { + color: white; + opacity: 0.9; + } + + .state-running { + .at2x('running-white.png', 20px, 20px); + + .runningwave { + .at2x('runningwave-white.png', 20px, 20px); + } + } + .state-stopped { + .at2x('stopped-white.png', 20px, 20px); + } + + .state-downloading { + .at2x('downloading-white.png', 20px, 20px); + + .downloading-arrow { + .at2x('downloading-arrow-white.png', 20px, 20px); + } + } +>>>>>>> master } } @@ -225,6 +267,22 @@ } } + .no-containers { + flex: 1 auto; + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + position: relative; + + h3 { + position: relative; + top: -44px; + font-size: 18px; + color: #C7D7D7; + } + } + .details { margin: 0; padding: 0; @@ -235,64 +293,72 @@ display: flex; flex-direction: column; - .details-actions { - flex: 0 auto; - display: flex; - flex-direction: row; - padding: 20px 40px 10px 40px; - position: relative; - border-bottom: 1px solid transparent; - transition: border-bottom 0.25s; - .action { - margin-right: 24px; - } - } - - .details-tabs { - .tabs { - z-index: 0; - float: right; - margin-right: 40px; - margin-top: -42px; - } - } - .details-header { flex: 0 auto; display: flex; - flex-direction: row; + flex-direction: column; padding: 4px 40px 10px 40px; position: relative; - a { - position: absolute; - right: 30px; - top: -4px; - } - h1 { - margin: 0; - font-size: 20px; - margin: 0; - color: @gray-darkest; - } - h2 { - &.status { - margin: 8px 0px 0px 16px; - text-transform: uppercase; - font-weight: bold; - font-size: 10px; - &.running { - color: @brand-positive; + border-bottom: 1px solid #eee; + + .details-header-actions { + flex: 0 auto; + display: flex; + flex-direction: row; + margin-top: 24px; + margin-bottom: 6px; + position: relative; + border-bottom: 1px solid transparent; + transition: border-bottom 0.25s; + .action { + flex: 0 auto; + margin-right: 24px; + } + .details-header-actions-rhs { + flex: 1 auto; + display: flex; + align-items: right; + justify-content: flex-end; + a.btn { + z-index: 0; } } - &.image-label { - margin: 8px 0px 0px 30px; - font-size: 10px; - color: @gray-lighter; + } + + .details-header-info { + display: flex; + flex-direction: row; + a { + position: absolute; + right: 30px; + top: -4px; } - &.image { - margin: 5px 0px 0px 16px; - font-size: 14px; - color: @gray-normal; + h1 { + margin: 0; + font-size: 20px; + margin: 0; + color: @gray-darkest; + } + h2 { + &.status { + margin: 8px 0px 0px 16px; + text-transform: uppercase; + font-weight: bold; + font-size: 10px; + &.running { + color: @brand-positive; + } + } + &.image-label { + margin: 8px 0px 0px 30px; + font-size: 10px; + color: @gray-lighter; + } + &.image { + margin: 5px 0px 0px 16px; + font-size: 14px; + color: @gray-normal; + } } } } @@ -306,11 +372,12 @@ flex: 1; overflow: auto; h4 { - margin-top: 30px; + font-size: 14px; + margin-top: 16px; margin-left: 40px; } .logs { - user-select: text; + -webkit-user-select: text; font-family: Menlo; font-size: 12px; padding: 18px 45px; diff --git a/app/styles/header.less b/app/styles/header.less index 63f8f0d92f..4731611def 100644 --- a/app/styles/header.less +++ b/app/styles/header.less @@ -3,9 +3,10 @@ .header { min-width: 100%; flex: 0; - min-height: 48px; + min-height: 50px; -webkit-app-region: drag; -webkit-user-select: none; + // border-bottom: 1px solid #efefef; &.no-drag { -webkit-app-region: no-drag; diff --git a/app/styles/main.less b/app/styles/main.less index c1c9e3037a..62b971b926 100644 --- a/app/styles/main.less +++ b/app/styles/main.less @@ -13,8 +13,13 @@ html, body { width: 100%; overflow: hidden; -webkit-font-smoothing: antialiased; - user-select: none; + -webkit-user-select: none; font-family: 'Clear Sans', sans-serif; + + cursor: default; + img { + pointer-events: none; + } } ::-webkit-scrollbar { @@ -49,8 +54,10 @@ html, body { box-shadow: 0 2px 5px rgba(0, 0, 0, 0.10); border: none; //1px solid #ccc; height: 610px; + display: flex; } .modal-body { + flex: 1 auto; display: flex; flex-direction: row; padding: 32px 32px; @@ -62,19 +69,10 @@ html, body { } section.search { - flex: 0 auto; min-width: 404px; padding-right: 32px; border-right: 1px solid #eee; - .search-icon { - font-size: 20px; - color: @gray-normal; - position: absolute; - top: 40px; - left: 45px; - } - .question { a { transition: all 0.3s ease 0s; @@ -89,31 +87,62 @@ html, body { text-align: right; } - input { - border-radius: 20px; - font-size: 13px; - height: 38px; - padding: 8px 16px 8px 40px; - font-weight: 300; - color: @gray-darkest; - margin-bottom: 3px; - border-color: lighten(@gray-lighter, 10%); - - &:focus { - box-shadow: none; - border-color: @gray-lighter; + .search-bar { + position: relative; + .loading { + position: absolute; + left: 13px; + top: 10px; + width: 20px; + height: 20px; + -webkit-animation-name: spin; + -webkit-animation-duration: 1.8s; + -webkit-animation-iteration-count: infinite; + -webkit-animation-timing-function: linear; } + .search-icon { + font-size: 20px; + color: @gray-lighter; + position: absolute; + top: 9px; + left: 14px; + } + input { + border-radius: 20px; + font-size: 13px; + height: 38px; + padding: 8px 16px 8px 40px; + color: @gray-darkest; + margin-bottom: 3px; + border-color: @gray-lightest; + box-shadow: none; - &::-webkit-input-placeholder { - color: #ddd; - font-weight: 300; + &:focus { + box-shadow: none; + border-color: @gray-lighter; + } + + &::-webkit-input-placeholder { + color: #ddd; + font-weight: 300; + } } } .results { overflow: auto; + .no-results { + text-align: center; + h3 { + color: #ABC0C0; + font-size: 16px; + margin-top: 160px; + } + } + .title { + flex: 0 auto; margin-top: 16px; } @@ -147,7 +176,7 @@ html, body { overflow: hidden; text-overflow: ellipsis; } - .stars { + .properties { color: @gray-lighter; margin-top: 2px; @@ -188,7 +217,7 @@ html, body { height: 100%; } -@-webkit-keyframes translatedownload { +@-webkit-keyframes spin { from { -webkit-transform: rotate(0deg); } diff --git a/app/styles/theme.less b/app/styles/theme.less index 012ae7c8db..da284b8a67 100644 --- a/app/styles/theme.less +++ b/app/styles/theme.less @@ -33,7 +33,7 @@ h4 { // Mixin for generating new styles .btn-styles(@btn-color: @gray-normal) { - transition: all 0.3s ease 0s; + transition: all 0.1s; .reset-filter(); // Disable gradients for IE9 because filter bleeds through rounded corners border-color: @btn-color; color: @btn-color; @@ -42,18 +42,22 @@ h4 { &:focus { border-color: darken(@btn-color, 10%); color: darken(@btn-color, 10%); + cursor: default; + box-shadow: none; } &:active { background-color: lighten(@btn-color, 45%); border-color: darken(@btn-color, 10%); color: darken(@btn-color, 10%); + box-shadow: none; } &.active { background-color: @btn-color; color: white; box-shadow: none; + box-shadow: none; } &:disabled, @@ -71,12 +75,6 @@ h4 { margin-right: 4px; } } - &.only-icon { - padding: 0px 14px 0px 14px; - .icon:before { - top: 5px; - } - } } } @@ -92,37 +90,25 @@ h4 { text-shadow: none; padding: 6px 14px 6px 14px; height: 32px; - .icon:before { - width: 20px; - height: 20px; + cursor: default; + + .content { position: relative; - top: 5px; - font-size: 20px; - margin-right: 4px; + top: -4px; + margin-left: 5px; + margin-right: 5px; } - &.with-icon { - padding: 0px 14px 6px 14px; - } - &.only-icon { - padding: 0px 5px 10px 5px; - .icon:before { - position: relative; - margin-right: 0px; - } - } - .icon-dropdown { - &.icon:before { - font-size: 10px; - position: relative; - top: 1px; - margin-left: 4px; - margin-right: 0px; - } + + .icon { + position: relative; + font-size: 16px; } + // Remove the gradient for the pressed/active state &:active, &.active { background-image: none; + box-shadow: none; } &:focus, diff --git a/browser/main.js b/browser/main.js index ba2c77ebb2..92627b2b7b 100644 --- a/browser/main.js +++ b/browser/main.js @@ -16,6 +16,8 @@ if (argv.test) { console.log('Running tests'); } +process.env.NODE_PATH = __dirname + '/../node_modules'; + app.on('activate-with-no-open-windows', function () { if (mainWindow) { mainWindow.show(); @@ -25,13 +27,14 @@ app.on('activate-with-no-open-windows', function () { app.on('ready', function() { var windowOptions = { - width: 1200, - height: 800, - 'min-width': 960, + width: 1000, + height: 700, + 'min-width': 1000, 'min-height': 700, resizable: true, frame: false }; + mainWindow = new BrowserWindow(windowOptions); mainWindow.hide(); diff --git a/deps b/deps new file mode 100755 index 0000000000..059abaf35c --- /dev/null +++ b/deps @@ -0,0 +1,18 @@ +#!/bin/bash + +BASE="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +export BOOT2DOCKER_CLI_VERSION=$(node -pe "JSON.parse(process.argv[1])['boot2docker-version']" "$(cat $BASE/package.json)") +export BOOT2DOCKER_CLI_VERSION_FILE=boot2docker-$BOOT2DOCKER_CLI_VERSION + +mkdir -p $BASE/cache + +pushd $BASE/resources > /dev/null + +if [ ! -f $BOOT2DOCKER_CLI_VERSION_FILE ]; then + echo "-----> Downloading Boot2docker CLI..." + rm -rf boot2docker-* + curl -L -o $BOOT2DOCKER_CLI_VERSION_FILE https://github.com/boot2docker/boot2docker-cli/releases/download/v${BOOT2DOCKER_CLI_VERSION}/boot2docker-v${BOOT2DOCKER_CLI_VERSION}-darwin-amd64 + chmod +x $BOOT2DOCKER_CLI_VERSION_FILE +fi + +popd > /dev/null diff --git a/gulpfile.js b/gulpfile.js index 3c850a4430..0f7dc7748c 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -4,8 +4,7 @@ var browserify = require('browserify'); var watchify = require('watchify'); var reactify = require('reactify'); var gulpif = require('gulp-if'); -var uglify = require('gulp-uglify'); -var streamify = require('gulp-streamify'); +var uglify = require('gulp-uglifyjs'); var notify = require('gulp-notify'); var concat = require('gulp-concat'); var less = require('gulp-less'); @@ -22,56 +21,29 @@ var ecstatic = require('ecstatic'); var downloadatomshell = require('gulp-download-atom-shell'); var packagejson = require('./package.json'); var http = require('http'); +var react = require('gulp-react'); +var fs = require('fs'); var dependencies = Object.keys(packagejson.dependencies); var devDependencies = Object.keys(packagejson.devDependencies); var options = { - dev: process.argv.indexOf('release') === -1, + dev: process.argv.indexOf('release') === -1 && process.argv.indexOf('test') === -1, test: process.argv.indexOf('test') !== -1, filename: 'Kitematic.app', - name: 'Kitematic', - signing_identity: process.env.XCODE_SIGNING_IDENTITY + name: 'Kitematic' + //signing_identity: fs.readFileSync('./identity') }; gulp.task('js', function () { - var bundler = browserify({ - entries: ['./app/main.js'], // Only need initial file, browserify finds the rest - transform: [reactify], // We want to convert JSX to normal javascript - debug: options.dev, // Gives us sourcemapping - builtins: false, - commondir: false, - insertGlobals: false, - detectGlobals: false, - bundleExternal: false, - cache: {}, packageCache: {}, fullPaths: options.dev // Requirement of watchify - }); - - // We set our dependencies as externals on our app bundler when developing - dependencies.forEach(function (dep) { - bundler.external(dep); - }); - - devDependencies.forEach(function (dep) { - bundler.external(dep); - }); - - bundler.external('./app'); - - var bundle = function () { - return bundler.bundle() - .on('error', gutil.log) - .pipe(source('main.js')) - .pipe(gulpif(!options.dev, streamify(uglify()))) - .pipe(gulp.dest(options.dev ? './build' : './dist/osx/' + options.filename + '/Contents/Resources/app/build')) - .pipe(gulpif(options.dev && !options.test, livereload())); - }; - - if (options.dev) { - bundler = watchify(bundler); - bundler.on('update', bundle); - } - - return bundle(); + gulp.src('./app/**/*.js') + .pipe(plumber(function(error) { + gutil.log(gutil.colors.red('Error (' + error.plugin + '): ' + error.message)); + // emit the end event, to properly end the task + this.emit('end'); + })) + .pipe(react()) + .pipe(gulp.dest(options.dev ? './build' : './dist/osx/' + options.filename + '/Contents/Resources/app/build')) + .pipe(gulpif(options.dev, livereload())); }); gulp.task('specs', function () { @@ -109,7 +81,7 @@ gulp.task('images', function() { svgoPlugins: [{removeViewBox: false}] })) .pipe(gulp.dest(options.dev ? './build' : './dist/osx/' + options.filename + '/Contents/Resources/app/build')) - .pipe(gulpif(options.dev && !options.test, livereload())); + .pipe(gulpif(options.dev, livereload())); }); gulp.task('styles', function () { @@ -138,16 +110,16 @@ gulp.task('download', function (cb) { gulp.task('copy', function () { gulp.src('./app/index.html') .pipe(gulp.dest(options.dev ? './build' : './dist/osx/' + options.filename + '/Contents/Resources/app/build')) - .pipe(gulpif(options.dev && !options.test, livereload())); + .pipe(gulpif(options.dev, livereload())); gulp.src('./app/fonts/**') .pipe(gulp.dest(options.dev ? './build' : './dist/osx/' + options.filename + '/Contents/Resources/app/build')) - .pipe(gulpif(options.dev && !options.test, livereload())); + .pipe(gulpif(options.dev, livereload())); }); gulp.task('dist', function (cb) { var stream = gulp.src('').pipe(shell([ - 'rm -rf ./dist/osx', + 'rm -Rf ./dist', 'mkdir -p ./dist/osx', 'cp -R ./cache/Atom.app ./dist/osx/<%= filename %>', 'mv ./dist/osx/<%= filename %>/Contents/MacOS/Atom ./dist/osx/<%= filename %>/Contents/MacOS/<%= name %>', @@ -213,6 +185,7 @@ gulp.task('test', ['download', 'copy', 'js', 'images', 'styles', 'specs'], funct }); gulp.task('default', ['download', 'copy', 'js', 'images', 'styles'], function () { + gulp.watch('./app/**/*.js', ['js']); gulp.watch('./app/**/*.html', ['copy']); gulp.watch('./app/styles/**/*.less', ['styles']); gulp.watch('./app/images/**', ['images']); diff --git a/package.json b/package.json index 3d8a7f4720..52e6f7c3f8 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,10 @@ }, "bugs": "https://github.com/kitematic/kitematic/issues", "scripts": { - "start": "./script/run" + "start": "gulp", + "preinstall": "./deps", + "test": "gulp test", + "release": ". ./script/identity && gulp release" }, "licenses": [ { @@ -24,26 +27,22 @@ "dependencies": { "ansi-to-html": "0.2.0", "async": "^0.9.0", + "bugsnag-js": "git+https://git@github.com/bugsnag/bugsnag-js", "dockerode": "2.0.4", "exec": "0.1.2", - "flux-react": "^2.6.1", + "gulp-react": "^2.0.0", "jquery": "^2.1.3", - "leveldown": "^1.0.0", - "levelup": "git+https://github.com/kitematic/node-levelup.git", "minimist": "^1.1.0", "moment": "2.8.1", - "ncp": "0.6.0", "node-uuid": "1.4.1", "open": "0.0.5", - "raven": "^0.7.2", - "react": "^0.12.1", + "react": "^0.12.2", "react-bootstrap": "^0.13.2", "react-retina-image": "^1.1.2", "react-router": "^0.11.6", "request": "2.42.0", "request-progress": "0.3.1", "retina.js": "^1.1.0", - "tar": "0.1.20", "underscore": "^1.7.0" }, "devDependencies": { @@ -65,9 +64,9 @@ "gulp-sourcemaps": "^1.2.8", "gulp-streamify": "0.0.5", "gulp-uglify": "^0.3.1", + "gulp-uglifyjs": "^0.5.0", "gulp-util": "^3.0.0", "jasmine-tagged": "^1.1.2", - "object-assign": "^2.0.0", "reactify": "^0.15.2", "run-sequence": "^1.0.2", "vinyl-source-stream": "^0.1.1", diff --git a/script/env b/script/env deleted file mode 100755 index 343e8551df..0000000000 --- a/script/env +++ /dev/null @@ -1,35 +0,0 @@ -#!/bin/bash - -DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -BASE=$DIR/.. -export NODE_VERSION="0.11.14" -export NPM="$BASE/cache/node-v$NODE_VERSION/bin/npm" -export NODE="$BASE/cache/node-v$NODE_VERSION/bin/node" -export PATH="$BASE/cache/node-v$NODE_VERSION/bin/:$BASE/node_modules/.bin:$PATH" -export NODE_PATH="$BASE/node_modules" -export BOOT2DOCKER_CLI_VERSION=$($NODE -pe "JSON.parse(process.argv[1])['boot2docker-version']" "$(cat $BASE/package.json)") -export BOOT2DOCKER_CLI_VERSION_FILE=boot2docker-$BOOT2DOCKER_CLI_VERSION - -mkdir -p $BASE/cache - -pushd $BASE/cache > /dev/null - -if [ ! -f "$NODE" ]; then - curl -L -o node-v$NODE_VERSION-darwin-x64.tar.gz http://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-darwin-x64.tar.gz - mkdir -p node-v$NODE_VERSION - tar -xzf node-v$NODE_VERSION-darwin-x64.tar.gz --strip-components 1 -C node-v$NODE_VERSION - rm -rf node-v$NODE_VERSION-darwin-x64.tar.gz -fi - -popd > /dev/null - -pushd $BASE/resources > /dev/null - -if [ ! -f $BOOT2DOCKER_CLI_VERSION_FILE ]; then - cecho "-----> Downloading Boot2docker CLI..." $purple - rm -rf boot2docker-* - curl -L -o $BOOT2DOCKER_CLI_VERSION_FILE https://github.com/boot2docker/boot2docker-cli/releases/download/v${BOOT2DOCKER_CLI_VERSION}/boot2docker-v${BOOT2DOCKER_CLI_VERSION}-darwin-amd64 - chmod +x $BOOT2DOCKER_CLI_VERSION_FILE -fi - -popd > /dev/null diff --git a/script/gulp b/script/gulp deleted file mode 100755 index c576fd1f81..0000000000 --- a/script/gulp +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash -DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -source $DIR/env - -gulp $* diff --git a/script/npm b/script/npm deleted file mode 100755 index 2399857240..0000000000 --- a/script/npm +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash - -DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -source $DIR/env - -ATOM_SHELL_VERSION=$($NODE -pe "JSON.parse(process.argv[1])['atom-shell-version']" "$(cat package.json)") -export npm_config_disturl=https://gh-contractor-zcbenz.s3.amazonaws.com/atom-shell/dist -export npm_config_target=$ATOM_SHELL_VERSION -export npm_config_arch=ia64 - -HOME=~/.atom-shell-gyp $NPM $* diff --git a/script/release b/script/release deleted file mode 100755 index 8b3692473c..0000000000 --- a/script/release +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash - -DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -source $DIR/env - -if [ -f $DIR/identity ]; then - source $DIR/identity -fi - -gulp release diff --git a/script/test b/script/test deleted file mode 100755 index 79a951fbad..0000000000 --- a/script/test +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash -DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -source $DIR/env - -gulp test