From 76980a6e5baa546eb2528189b10edee533cef465 Mon Sep 17 00:00:00 2001 From: FrenchBen Date: Fri, 17 Jul 2015 19:06:15 -0400 Subject: [PATCH 1/2] Added support for creating containers from local images Signed-off-by: FrenchBen --- CONTRIBUTING.md | 12 +-- src/actions/ContainerActions.js | 4 +- src/actions/ImageActions.js | 16 +++ src/actions/ImageServerActions.js | 14 +++ src/actions/TagActions.js | 4 + src/components/ImageCard.react.js | 110 +++++++++++++++------ src/components/NewContainerSearch.react.js | 58 ++++++++++- src/stores/ImageStore.js | 56 +++++++++++ src/stores/TagStore.js | 9 ++ src/utils/DockerUtil.js | 94 +++++++++++++----- styles/new-container.less | 33 +++++++ 11 files changed, 345 insertions(+), 65 deletions(-) create mode 100644 src/actions/ImageActions.js create mode 100644 src/actions/ImageServerActions.js create mode 100644 src/stores/ImageStore.js diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 21febb49bd..59c8d9f32b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,18 +17,18 @@ Before you file an issue or a pull request, read the following tips on how to ke - [License](#license) -### Prerequisites for developing Kitematic on Mac -You will need to install: +### Prerequisites for developing Kitematic on Mac +You will need to install: - The [Docker Toolbox](https://docker.com/toolbox) - [Node.js](https://nodejs.org/) - Wine `brew install wine` (only if you want to generate a Windows release on OS X) -- The latest Xcode from the Apple App Store. +- The latest Xcode from the Apple App Store. -### Prerequisites for developing Kitematic on Windows -You will need to install: +### Prerequisites for developing Kitematic on Windows +You will need to install: - The [Docker Toolbox](https://docker.com/toolbox) - [Node.js](https://nodejs.org/) -- Open a command prompt (`cmd`) and run the command `mkdir ~/AppData/Roaming/npm` +- Open a command prompt (`cmd`) and run the command `mkdir ~/AppData/Roaming/npm` - [Visual Studio 2013 Community](https://www.visualstudio.com/en-us/products/visual-studio-community-vs.aspx) (or similar) - You do not need to install any optional packages during install. - [Python](https://www.python.org/downloads/release/python-2710/) diff --git a/src/actions/ContainerActions.js b/src/actions/ContainerActions.js index a9c2f4a61c..5470a241b6 100644 --- a/src/actions/ContainerActions.js +++ b/src/actions/ContainerActions.js @@ -35,8 +35,8 @@ class ContainerActions { this.dispatch(); } - run (name, repo, tag) { - dockerUtil.run(name, repo, tag); + run (name, repo, tag, local=false) { + dockerUtil.run(name, repo, tag, local); } active (name) { diff --git a/src/actions/ImageActions.js b/src/actions/ImageActions.js new file mode 100644 index 0000000000..518f8aaa07 --- /dev/null +++ b/src/actions/ImageActions.js @@ -0,0 +1,16 @@ +import alt from '../alt'; +import dockerUtil from '../utils/DockerUtil'; + +class ImageActions { + + all () { + this.dispatch({}); + dockerUtil.fetchAllImages(); + } + + destroy (image) { + dockerUtil.removeImage(image); + } +} + +export default alt.createActions(ImageActions); diff --git a/src/actions/ImageServerActions.js b/src/actions/ImageServerActions.js new file mode 100644 index 0000000000..e5c49e7bbd --- /dev/null +++ b/src/actions/ImageServerActions.js @@ -0,0 +1,14 @@ +import alt from '../alt'; + +class ImageServerActions { + constructor () { + this.generateActions( + 'added', + 'updated', + 'destroyed', + 'error' + ); + } +} + +export default alt.createActions(ImageServerActions); diff --git a/src/actions/TagActions.js b/src/actions/TagActions.js index 36ffd843e1..27e071cdd2 100644 --- a/src/actions/TagActions.js +++ b/src/actions/TagActions.js @@ -6,6 +6,10 @@ class TagActions { this.dispatch({repo}); regHubUtil.tags(repo); } + + localTags (repo, tags) { + this.dispatch({repo, tags}); + } } export default alt.createActions(TagActions); diff --git a/src/components/ImageCard.react.js b/src/components/ImageCard.react.js index fe979c9ef1..ed55b17020 100644 --- a/src/components/ImageCard.react.js +++ b/src/components/ImageCard.react.js @@ -5,6 +5,7 @@ import shell from 'shell'; import RetinaImage from 'react-retina-image'; import metrics from '../utils/MetricsUtil'; import containerActions from '../actions/ContainerActions'; +import imageActions from '../actions/ImageActions'; import containerStore from '../stores/ContainerStore'; import tagStore from '../stores/TagStore'; import tagActions from '../actions/TagActions'; @@ -14,8 +15,8 @@ var ImageCard = React.createClass({ mixins: [Router.Navigation], getInitialState: function () { return { - tags: [], - chosenTag: 'latest' + tags: this.props.tags || [], + chosenTag: this.props.chosenTag || 'latest' }; }, componentDidMount: function () { @@ -49,11 +50,14 @@ var ImageCard = React.createClass({ private: this.props.image.is_private, official: this.props.image.namespace === 'library', userowned: this.props.image.is_user_repo, - recommended: this.props.image.is_recommended + recommended: this.props.image.is_recommended, + local: this.props.image.is_local || false }); let name = containerStore.generateName(this.props.image.name); - let repo = this.props.image.namespace === 'library' ? this.props.image.name : this.props.image.namespace + '/' + this.props.image.name; - containerActions.run(name, repo, this.state.chosenTag); + let localImage = this.props.image.is_local || false; + let repo = (this.props.image.namespace === 'library' || this.props.image.namespace === 'local') ? this.props.image.name : this.props.image.namespace + '/' + this.props.image.name; + + containerActions.run(name, repo, this.state.chosenTag, localImage); this.transitionTo('containerHome', {name}); }, handleMenuOverlayClick: function () { @@ -67,7 +71,12 @@ var ImageCard = React.createClass({ handleTagOverlayClick: function () { let $tagOverlay = $(this.getDOMNode()).find('.tag-overlay'); $tagOverlay.fadeIn(300); - tagActions.tags(this.props.image.namespace + '/' + this.props.image.name); + let localImage = this.props.image.is_local || false; + if (localImage) { + tagActions.localTags(this.props.image.namespace + '/' + this.props.image.name, this.props.tags); + } else { + tagActions.tags(this.props.image.namespace + '/' + this.props.image.name); + } }, handleCloseTagOverlay: function () { let $menuOverlay = $(this.getDOMNode()).find('.menu-overlay'); @@ -75,6 +84,11 @@ var ImageCard = React.createClass({ var $tagOverlay = $(this.getDOMNode()).find('.tag-overlay'); $tagOverlay.fadeOut(300); }, + handleDeleteImgClick: function (image) { + if (this.state.chosenTag) { + imageActions.destroy(image.RepoTags[0].split(':')[0] + ':' + this.state.chosenTag); + } + }, handleRepoClick: function () { var repoUri = 'https://hub.docker.com/'; if (this.props.image.namespace === 'library') { @@ -108,10 +122,9 @@ var ImageCard = React.createClass({ } else if(this.props.image.short_description){ description = this.props.image.short_description; } else { - description = "No description."; + description = 'No description.'; } var logoStyle = { - //backgroundImage: `linear-gradient(-180deg, ${this.props.image.gradient_start} 4%, ${this.props.image.gradient_end} 100%)` backgroundColor: this.props.image.gradient_start }; var imgsrc; @@ -150,21 +163,75 @@ var ImageCard = React.createClass({ ); } - let favCount = (this.props.image.star_count < 1000) ? numeral(this.props.image.star_count).value() : numeral(this.props.image.star_count).format('0.0a').toUpperCase(); - let pullCount = (this.props.image.pull_count < 1000) ? numeral(this.props.image.pull_count).value() : numeral(this.props.image.pull_count).format('0a').toUpperCase(); - return ( -
+ + let create; + let overlay; + if (this.props.image.is_local) { + create = ( +
+
+ {this.state.chosenTag} + +
+
+ +
+
+ CREATE +
+
+ ); + overlay = (
SELECTED TAG: {this.state.chosenTag}
-
- VIEW ON DOCKER HUB +
+ Delete Tag
+

Prior to delete, stop all containers
using the above tag

+ ); + } else { + let favCount = (this.props.image.star_count < 1000) ? numeral(this.props.image.star_count).value() : numeral(this.props.image.star_count).format('0.0a').toUpperCase(); + let pullCount = (this.props.image.pull_count < 1000) ? numeral(this.props.image.pull_count).value() : numeral(this.props.image.pull_count).format('0a').toUpperCase(); + create = ( +
+
+ + {favCount} + + {pullCount} +
+
+ +
+
+ CREATE +
+
+ ); + + overlay = ( +
+
+ SELECTED TAG: {this.state.chosenTag} +
+
+ VIEW ON DOCKER HUB +
+
+ +
+
+ ); + } + return ( +
+ {overlay}

Please select an image tag.

{tags} @@ -187,20 +254,7 @@ var ImageCard = React.createClass({ {description}
-
-
- - {favCount} - - {pullCount} -
-
- -
-
- CREATE -
-
+ {create}
); diff --git a/src/components/NewContainerSearch.react.js b/src/components/NewContainerSearch.react.js index 410b4e43ff..23550d06a1 100644 --- a/src/components/NewContainerSearch.react.js +++ b/src/components/NewContainerSearch.react.js @@ -10,6 +10,8 @@ import repositoryActions from '../actions/RepositoryActions'; import repositoryStore from '../stores/RepositoryStore'; import accountStore from '../stores/AccountStore'; import accountActions from '../actions/AccountActions'; +import imageActions from '../actions/ImageActions'; +import imageStore from '../stores/ImageStore'; var _searchPromise = null; @@ -20,6 +22,8 @@ module.exports = React.createClass({ query: '', loading: repositoryStore.loading(), repos: repositoryStore.all(), + images: imageStore.all(), + imagesErr: imageStore.error, username: accountStore.getState().username, verified: accountStore.getState().verified, accountLoading: accountStore.getState().loading, @@ -34,6 +38,7 @@ module.exports = React.createClass({ this.refs.searchInput.getDOMNode().focus(); repositoryStore.listen(this.update); accountStore.listen(this.updateAccount); + imageStore.listen(this.updateImage); repositoryActions.search(); }, componentWillUnmount: function () { @@ -51,7 +56,14 @@ module.exports = React.createClass({ currentPage: repositoryStore.getState().currentPage, totalPage: repositoryStore.getState().totalPage, previousPage: repositoryStore.getState().previousPage, - nextPage: repositoryStore.getState().nextPage + nextPage: repositoryStore.getState().nextPage, + error: repositoryStore.getState().error + }); + }, + updateImage: function (imgStore) { + this.setState({ + images: imgStore.images, + error: imgStore.error }); }, updateAccount: function () { @@ -79,7 +91,8 @@ module.exports = React.createClass({ currentPage: page, previousPage: previousPage, nextPage: nextPage, - totalPage: totalPage + totalPage: totalPage, + error: null }); _searchPromise = Promise.delay(200).cancellable().then(() => { @@ -101,11 +114,17 @@ module.exports = React.createClass({ }, handleFilter: function (filter) { + this.setState({error: null}); + // If we're clicking on the filter again - refresh if (filter === 'userrepos' && this.getQuery().filter === 'userrepos') { repositoryActions.repos(); } + if (filter === 'userimages' && this.getQuery().filter === 'userimages') { + imageActions.all(); + } + if (filter === 'recommended' && this.getQuery().filter === 'recommended') { repositoryActions.recommended(); } @@ -187,10 +206,16 @@ module.exports = React.createClass({ ) : null; + let errorMsg = null; + if (this.state.error === null || this.state.error.message.indexOf('getaddrinfo ENOTFOUND') !== -1) { + errorMsg = 'There was an error contacting Docker Hub.'; + } else { + errorMsg = this.state.error.message.replace('HTTP code is 409 which indicates error: conflict - ', ''); + } if (this.state.error) { results = (
-

There was an error contacting Docker Hub.

+

{errorMsg}

); paginateResults = null; @@ -268,6 +293,32 @@ module.exports = React.createClass({ {otherResults} ); + } else if (filter === 'userimages') { + let userImageItems = this.state.images.map(image => { + let repo = image.RepoTags[0].split(':')[0]; + if (repo.indexOf('/') === -1) { + repo = 'local/' + repo; + } + [image.namespace, image.name] = repo.split('/'); + image.description = null; + let tags = image.tags.join('-'); + image.star_count = 0; + image.is_local = true; + return (); + }); + let userImageResults = userImageItems.length ? ( +
+

My Images

+
+ {userImageItems} +
+
+ ) : null; + results = ( +
+ {userImageResults} +
+ ); } else { if (this.state.query.length) { results = ( @@ -316,6 +367,7 @@ module.exports = React.createClass({ All Recommended My Repos + My Images
diff --git a/src/stores/ImageStore.js b/src/stores/ImageStore.js new file mode 100644 index 0000000000..984a085545 --- /dev/null +++ b/src/stores/ImageStore.js @@ -0,0 +1,56 @@ +import alt from '../alt'; +import imageActions from '../actions/ImageActions'; +import imageServerActions from '../actions/ImageServerActions'; + +class ImageStore { + constructor () { + this.bindActions(imageActions); + this.bindActions(imageServerActions); + this.results = []; + this.images = []; + this.imagesLoading = false; + this.resultsLoading = false; + this.error = null; + } + + error (error) { + this.setState({error: error, imagesLoading: false, resultsLoading: false}); + } + + clearError () { + this.setState({error: null}); + } + + destroyed (data) { + let images = this.images; + if ((data && data[1] && data[1].Deleted)) { + delete images[data[1].Deleted]; + } + this.setState({error: null}); + } + + updated (images) { + let tags = {}; + let finalImages = []; + images.map((image) => { + image.RepoTags.map(repoTags => { + let [name, tag] = repoTags.split(':'); + if (typeof tags[name] !== 'undefined') { + finalImages[tags[name]].tags.push(tag); + } else { + image.tags = [tag]; + tags[name] = finalImages.length; + finalImages.push(image); + } + }); + }); + this.setState({error: null, images: finalImages, imagesLoading: false}); + } + + static all () { + let state = this.getState(); + return state.images; + } +} + +export default alt.createStore(ImageStore); diff --git a/src/stores/TagStore.js b/src/stores/TagStore.js index 13a2b980cf..4ddb4c5a05 100644 --- a/src/stores/TagStore.js +++ b/src/stores/TagStore.js @@ -21,6 +21,15 @@ class TagStore { this.emitChange(); } + localTags ({repo, tags}) { + let data = []; + tags.map((value) => { + data.push({'name': value}); + }); + this.loading[repo] = true; + this.tagsUpdated({repo, tags: data || []}); + } + tagsUpdated ({repo, tags}) { this.tags[repo] = tags; this.loading[repo] = false; diff --git a/src/utils/DockerUtil.js b/src/utils/DockerUtil.js index f6b92f6feb..f107f3558f 100644 --- a/src/utils/DockerUtil.js +++ b/src/utils/DockerUtil.js @@ -8,6 +8,8 @@ import util from './Util'; import hubUtil from './HubUtil'; import metrics from '../utils/MetricsUtil'; import containerServerActions from '../actions/ContainerServerActions'; +import imageServerActions from '../actions/ImageServerActions'; +import Promise from 'bluebird'; import rimraf from 'rimraf'; import stream from 'stream'; import JSONStream from 'JSONStream'; @@ -15,13 +17,14 @@ import Promise from 'bluebird'; -export default { +var DockerUtil = { host: null, client: null, placeholders: {}, stream: null, eventStream: null, activeContainerName: null, + localImages: null, setup (ip, name) { if (!ip && !name) { @@ -77,6 +80,7 @@ export default { init () { this.placeholders = JSON.parse(localStorage.getItem('placeholders')) || {}; this.fetchAllContainers(); + this.fetchAllImages(); this.listen(); // Resume pulling containers that were previously being pulled @@ -170,6 +174,7 @@ export default { }); }); }); + this.fetchAllImages(); }, fetchContainer (id) { @@ -210,7 +215,36 @@ export default { }); }, - run (name, repository, tag) { + fetchAllImages () { + this.client.listImages((err, list) => { + if (err) { + imageServerActions.error(err); + } else { + this.localImages = list; + imageServerActions.updated(list); + } + }); + }, + + removeImage (selectedRepoTag) { + this.localImages.some((image) => { + image.RepoTags.map(repoTag => { + if (repoTag === selectedRepoTag) { + this.client.getImage(selectedRepoTag).remove({'force': true}, (err, data) => { + if (err) { + console.error(err); + imageServerActions.error(err); + } else { + imageServerActions.destroyed(data); + } + }); + return true; + } + }); + }); + }, + + run (name, repository, tag, local = false) { tag = tag || 'latest'; let imageName = repository + ':' + tag; @@ -231,30 +265,33 @@ export default { this.placeholders[name] = placeholderData; localStorage.setItem('placeholders', JSON.stringify(this.placeholders)); - - this.pullImage(repository, tag, error => { - if (error) { - containerServerActions.error({name, error}); - return; - } - - if (!this.placeholders[name]) { - return; - } - + if (local) { this.createContainer(name, {Image: imageName, Tty: true, OpenStdin: true}); - }, + } else { + this.pullImage(repository, tag, error => { + if (error) { + containerServerActions.error({name, error}); + return; + } - // progress is actually the progression PER LAYER (combined in columns) - // not total because it's not accurate enough - progress => { - containerServerActions.progress({name, progress}); - }, + if (!this.placeholders[name]) { + return; + } + + this.createContainer(name, {Image: imageName, Tty: true, OpenStdin: true}); + }, + + // progress is actually the progression PER LAYER (combined in columns) + // not total because it's not accurate enough + progress => { + containerServerActions.progress({name, progress}); + }, - () => { - containerServerActions.waiting({name, waiting: true}); - }); + () => { + containerServerActions.waiting({name, waiting: true}); + }); + } }, updateContainer (name, data) { @@ -474,9 +511,11 @@ export default { } stream.setEncoding('utf8'); - stream.pipe(JSONStream.parse()).on('data', data => { - if (data.status === 'pull' || data.status === 'untag' || data.status === 'delete' || data.status === 'attach') { - return; + stream.on('data', json => { + let data = JSON.parse(json); + + if (data.status === 'pull' || data.status === 'untag' || data.status === 'delete' || data.status === 'attach') { + this.fetchAllImages(); } if (data.status === 'destroy') { @@ -519,6 +558,7 @@ export default { this.client.pull(repository + ':' + tag, opts, (err, stream) => { if (err) { + console.log('Err: %o', err); callback(err); return; } @@ -563,7 +603,7 @@ export default { if (i < leftOverLayers) { layerAmount += 1; } - columns.progress[i] = {layerIDs: [], nbLayers:0 , maxLayers: layerAmount, value: 0.0}; + columns.progress[i] = {layerIDs: [], nbLayers: 0, maxLayers: layerAmount, value: 0.0}; } } @@ -617,3 +657,5 @@ export default { }); } }; + +module.exports = DockerUtil; diff --git a/styles/new-container.less b/styles/new-container.less index 7ca9247a3d..1a08ce9807 100644 --- a/styles/new-container.less +++ b/styles/new-container.less @@ -67,6 +67,10 @@ justify-content: center; flex-shrink: 0; + .error { + color: red; + } + img { width: 380px; } @@ -160,6 +164,10 @@ font-weight: 500; margin-right: 0.7rem; } + .results-userimages { + border-left: 1px solid @gray-lighter; + padding-left: 1.2rem; + } } } } @@ -227,6 +235,31 @@ bottom: 1rem; right: 1rem; } + .remove { + display: flex; + flex: 1 auto; + justify-content: center; + margin: 0.8rem 0 0 0; + a { + display: block; + text-decoration: none; + cursor: default; + &:focus { + outline: 0; + } + &.active { + .btn-delete { + opacity: 0.3; + } + } + } + } + .small { + color: red; + text-align: center; + padding-top: 5px; + font-size: 75%; + } } .tag-overlay { z-index: 1000; From db5d9e015eb67c0ac8dda36f23b263933d7bcd13 Mon Sep 17 00:00:00 2001 From: French Ben Date: Fri, 5 Feb 2016 15:29:38 -0800 Subject: [PATCH 2/2] Merged master and updated logic for local images Signed-off-by: French Ben --- src/actions/ImageActions.js | 2 +- src/components/ImageCard.react.js | 9 ++- src/components/NewContainerSearch.react.js | 70 ++++++++++++---------- src/stores/ImageStore.js | 3 + src/utils/DockerUtil.js | 44 +++++++++++--- styles/new-container.less | 1 + 6 files changed, 84 insertions(+), 45 deletions(-) diff --git a/src/actions/ImageActions.js b/src/actions/ImageActions.js index 518f8aaa07..fe91c7d9b9 100644 --- a/src/actions/ImageActions.js +++ b/src/actions/ImageActions.js @@ -5,7 +5,7 @@ class ImageActions { all () { this.dispatch({}); - dockerUtil.fetchAllImages(); + dockerUtil.refresh(); } destroy (image) { diff --git a/src/components/ImageCard.react.js b/src/components/ImageCard.react.js index ed55b17020..2816757695 100644 --- a/src/components/ImageCard.react.js +++ b/src/components/ImageCard.react.js @@ -85,7 +85,7 @@ var ImageCard = React.createClass({ $tagOverlay.fadeOut(300); }, handleDeleteImgClick: function (image) { - if (this.state.chosenTag) { + if (this.state.chosenTag && !this.props.image.inUse) { imageActions.destroy(image.RepoTags[0].split(':')[0] + ':' + this.state.chosenTag); } }, @@ -164,8 +164,7 @@ var ImageCard = React.createClass({ ); } - let create; - let overlay; + let create, overlay; if (this.props.image.is_local) { create = (
@@ -187,9 +186,9 @@ var ImageCard = React.createClass({ SELECTED TAG: {this.state.chosenTag}
- Delete Tag + Delete Tag
-

Prior to delete, stop all containers
using the above tag

+ {this.props.image.inUse ?

To delete, remove all containers
using the above image

: null }
diff --git a/src/components/NewContainerSearch.react.js b/src/components/NewContainerSearch.react.js index 23550d06a1..886ad17817 100644 --- a/src/components/NewContainerSearch.react.js +++ b/src/components/NewContainerSearch.react.js @@ -239,6 +239,34 @@ module.exports = React.createClass({
); paginateResults = null; + } else if (filter === 'userimages') { + let userImageItems = this.state.images.map(image => { + let repo = image.RepoTags[0].split(':')[0]; + if (repo.indexOf('/') === -1) { + repo = 'local/' + repo; + } + [image.namespace, image.name] = repo.split('/'); + image.description = null; + let tags = image.tags.join('-'); + image.star_count = 0; + image.is_local = true; + return (); + }); + let userImageResults = userImageItems.length ? ( +
+
+

My Images

+
+ {userImageItems} +
+
+
+ ) :
+

Cannot find any local image.

+
; + results = ( + {userImageResults} + ); } else if (this.state.loading) { results = (
@@ -293,32 +321,6 @@ module.exports = React.createClass({ {otherResults}
); - } else if (filter === 'userimages') { - let userImageItems = this.state.images.map(image => { - let repo = image.RepoTags[0].split(':')[0]; - if (repo.indexOf('/') === -1) { - repo = 'local/' + repo; - } - [image.namespace, image.name] = repo.split('/'); - image.description = null; - let tags = image.tags.join('-'); - image.star_count = 0; - image.is_local = true; - return (); - }); - let userImageResults = userImageItems.length ? ( -
-

My Images

-
- {userImageItems} -
-
- ) : null; - results = ( -
- {userImageResults} -
- ); } else { if (this.state.query.length) { results = ( @@ -350,17 +352,23 @@ module.exports = React.createClass({ 'icon-search': true, 'search-icon': true }); + let searchClasses = classNames('search-bar'); + if (filter === 'userimages') { + searchClasses = classNames('search-bar', { + hidden: true + }); + } return (
-
- -
-
-
+
+ +
+
+
FILTER BY diff --git a/src/stores/ImageStore.js b/src/stores/ImageStore.js index 984a085545..397b3c3c7e 100644 --- a/src/stores/ImageStore.js +++ b/src/stores/ImageStore.js @@ -37,6 +37,9 @@ class ImageStore { let [name, tag] = repoTags.split(':'); if (typeof tags[name] !== 'undefined') { finalImages[tags[name]].tags.push(tag); + if (image.inUse) { + finalImages[tags[name]].inUse = image.inUse; + } } else { image.tags = [tag]; tags[name] = finalImages.length; diff --git a/src/utils/DockerUtil.js b/src/utils/DockerUtil.js index f107f3558f..c61d00f8c5 100644 --- a/src/utils/DockerUtil.js +++ b/src/utils/DockerUtil.js @@ -13,7 +13,6 @@ import Promise from 'bluebird'; import rimraf from 'rimraf'; import stream from 'stream'; import JSONStream from 'JSONStream'; -import Promise from 'bluebird'; @@ -25,6 +24,7 @@ var DockerUtil = { eventStream: null, activeContainerName: null, localImages: null, + imagesUsed: [], setup (ip, name) { if (!ip && !name) { @@ -79,8 +79,7 @@ var DockerUtil = { init () { this.placeholders = JSON.parse(localStorage.getItem('placeholders')) || {}; - this.fetchAllContainers(); - this.fetchAllImages(); + this.refresh(); this.listen(); // Resume pulling containers that were previously being pulled @@ -170,11 +169,11 @@ var DockerUtil = { this.startContainer(name); delete this.placeholders[name]; localStorage.setItem('placeholders', JSON.stringify(this.placeholders)); + this.refresh(); }); }); }); }); - this.fetchAllImages(); }, fetchContainer (id) { @@ -194,12 +193,17 @@ var DockerUtil = { console.error(err); return; } + this.imagesUsed = []; async.map(containers, (container, callback) => { this.client.getContainer(container.Id).inspect((error, container) => { if (error) { callback(null, null); return; } + let imgSha = container.Image.replace('sha256:', ''); + if (_.indexOf(this.imagesUsed, imgSha) === -1) { + this.imagesUsed.push(imgSha); + } container.Name = container.Name.replace('/', ''); callback(null, container); }); @@ -211,6 +215,7 @@ var DockerUtil = { return; } containerServerActions.allUpdated({containers: _.indexBy(containers.concat(_.values(this.placeholders)), 'Name')}); + this.fetchAllImages(); }); }); }, @@ -220,6 +225,14 @@ var DockerUtil = { if (err) { imageServerActions.error(err); } else { + list.map((image, idx) => { + let imgSha = image.Id.replace('sha256:', ''); + if (_.indexOf(this.imagesUsed, imgSha) !== -1) { + list[idx].inUse = true; + } else { + list[idx].inUse = false; + } + }); this.localImages = list; imageServerActions.updated(list); } @@ -236,6 +249,7 @@ var DockerUtil = { imageServerActions.error(err); } else { imageServerActions.destroyed(data); + this.refresh(); } }); return true; @@ -271,6 +285,7 @@ var DockerUtil = { this.pullImage(repository, tag, error => { if (error) { containerServerActions.error({name, error}); + this.refresh(); return; } @@ -299,6 +314,7 @@ var DockerUtil = { existing.inspect((error, existingData) => { if (error) { containerServerActions.error({name, error}); + this.refresh(); return; } @@ -335,6 +351,7 @@ var DockerUtil = { if (error) { // TODO: handle error containerServerActions.error({newName, error}); + this.refresh(); } rimraf(newPath, () => { if (fs.existsSync(oldPath)) { @@ -356,11 +373,13 @@ var DockerUtil = { this.client.getContainer(name).stop({t: 5}, stopError => { if (stopError && stopError.statusCode !== 304) { containerServerActions.error({name, stopError}); + this.refresh(); return; } this.client.getContainer(name).start(startError => { if (startError && startError.statusCode !== 304) { containerServerActions.error({name, startError}); + this.refresh(); return; } this.fetchContainer(name); @@ -372,6 +391,7 @@ var DockerUtil = { this.client.getContainer(name).stop({t: 5}, error => { if (error && error.statusCode !== 304) { containerServerActions.error({name, error}); + this.refresh(); return; } this.fetchContainer(name); @@ -382,6 +402,7 @@ var DockerUtil = { this.client.getContainer(name).start(error => { if (error && error.statusCode !== 304) { containerServerActions.error({name, error}); + this.refresh(); return; } this.fetchContainer(name); @@ -393,15 +414,17 @@ var DockerUtil = { containerServerActions.destroyed({id: name}); delete this.placeholders[name]; localStorage.setItem('placeholders', JSON.stringify(this.placeholders)); + this.refresh(); return; } let container = this.client.getContainer(name); - container.unpause(function () { - container.kill(function () { - container.remove(function (error) { + container.unpause( () => { + container.kill( () => { + container.remove( (error) => { if (error) { containerServerActions.error({name, error}); + this.refresh(); return; } containerServerActions.destroyed({id: name}); @@ -409,6 +432,7 @@ var DockerUtil = { if (fs.existsSync(volumePath)) { rimraf(volumePath, () => {}); } + this.refresh(); }); }); }); @@ -515,7 +539,7 @@ var DockerUtil = { let data = JSON.parse(json); if (data.status === 'pull' || data.status === 'untag' || data.status === 'delete' || data.status === 'attach') { - this.fetchAllImages(); + this.refresh(); } if (data.status === 'destroy') { @@ -655,6 +679,10 @@ var DockerUtil = { callback(error); }); }); + }, + + refresh () { + this.fetchAllContainers(); } }; diff --git a/styles/new-container.less b/styles/new-container.less index 1a08ce9807..8885123910 100644 --- a/styles/new-container.less +++ b/styles/new-container.less @@ -167,6 +167,7 @@ .results-userimages { border-left: 1px solid @gray-lighter; padding-left: 1.2rem; + padding-right: 1.2rem; } } }