Merge pull request #882 from FrenchBen/local-images

Local images support!
This commit is contained in:
French Ben 2016-04-12 18:20:28 -07:00
commit c1c1922efc
11 changed files with 394 additions and 75 deletions

View File

@ -17,18 +17,18 @@ Before you file an issue or a pull request, read the following tips on how to ke
- [License](#license) - [License](#license)
### Prerequisites for developing Kitematic on Mac ### Prerequisites for developing Kitematic on Mac
You will need to install: You will need to install:
- The [Docker Toolbox](https://docker.com/toolbox) - The [Docker Toolbox](https://docker.com/toolbox)
- [Node.js](https://nodejs.org/) - [Node.js](https://nodejs.org/)
- Wine `brew install wine` (only if you want to generate a Windows release on OS X) - 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 ### Prerequisites for developing Kitematic on Windows
You will need to install: You will need to install:
- The [Docker Toolbox](https://docker.com/toolbox) - The [Docker Toolbox](https://docker.com/toolbox)
- [Node.js](https://nodejs.org/) - [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. - [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/) - [Python](https://www.python.org/downloads/release/python-2710/)

View File

@ -35,8 +35,8 @@ class ContainerActions {
this.dispatch(); this.dispatch();
} }
run (name, repo, tag) { run (name, repo, tag, local=false) {
dockerUtil.run(name, repo, tag); dockerUtil.run(name, repo, tag, local);
} }
active (name) { active (name) {

View File

@ -0,0 +1,16 @@
import alt from '../alt';
import dockerUtil from '../utils/DockerUtil';
class ImageActions {
all () {
this.dispatch({});
dockerUtil.refresh();
}
destroy (image) {
dockerUtil.removeImage(image);
}
}
export default alt.createActions(ImageActions);

View File

@ -0,0 +1,14 @@
import alt from '../alt';
class ImageServerActions {
constructor () {
this.generateActions(
'added',
'updated',
'destroyed',
'error'
);
}
}
export default alt.createActions(ImageServerActions);

View File

@ -6,6 +6,10 @@ class TagActions {
this.dispatch({repo}); this.dispatch({repo});
regHubUtil.tags(repo); regHubUtil.tags(repo);
} }
localTags (repo, tags) {
this.dispatch({repo, tags});
}
} }
export default alt.createActions(TagActions); export default alt.createActions(TagActions);

View File

@ -5,6 +5,7 @@ import shell from 'shell';
import RetinaImage from 'react-retina-image'; import RetinaImage from 'react-retina-image';
import metrics from '../utils/MetricsUtil'; import metrics from '../utils/MetricsUtil';
import containerActions from '../actions/ContainerActions'; import containerActions from '../actions/ContainerActions';
import imageActions from '../actions/ImageActions';
import containerStore from '../stores/ContainerStore'; import containerStore from '../stores/ContainerStore';
import tagStore from '../stores/TagStore'; import tagStore from '../stores/TagStore';
import tagActions from '../actions/TagActions'; import tagActions from '../actions/TagActions';
@ -14,8 +15,8 @@ var ImageCard = React.createClass({
mixins: [Router.Navigation], mixins: [Router.Navigation],
getInitialState: function () { getInitialState: function () {
return { return {
tags: [], tags: this.props.tags || [],
chosenTag: 'latest' chosenTag: this.props.chosenTag || 'latest'
}; };
}, },
componentDidMount: function () { componentDidMount: function () {
@ -49,11 +50,14 @@ var ImageCard = React.createClass({
private: this.props.image.is_private, private: this.props.image.is_private,
official: this.props.image.namespace === 'library', official: this.props.image.namespace === 'library',
userowned: this.props.image.is_user_repo, 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 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; let localImage = this.props.image.is_local || false;
containerActions.run(name, repo, this.state.chosenTag); 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}); this.transitionTo('containerHome', {name});
}, },
handleMenuOverlayClick: function () { handleMenuOverlayClick: function () {
@ -67,7 +71,12 @@ var ImageCard = React.createClass({
handleTagOverlayClick: function () { handleTagOverlayClick: function () {
let $tagOverlay = $(this.getDOMNode()).find('.tag-overlay'); let $tagOverlay = $(this.getDOMNode()).find('.tag-overlay');
$tagOverlay.fadeIn(300); $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 () { handleCloseTagOverlay: function () {
let $menuOverlay = $(this.getDOMNode()).find('.menu-overlay'); let $menuOverlay = $(this.getDOMNode()).find('.menu-overlay');
@ -75,6 +84,11 @@ var ImageCard = React.createClass({
var $tagOverlay = $(this.getDOMNode()).find('.tag-overlay'); var $tagOverlay = $(this.getDOMNode()).find('.tag-overlay');
$tagOverlay.fadeOut(300); $tagOverlay.fadeOut(300);
}, },
handleDeleteImgClick: function (image) {
if (this.state.chosenTag && !this.props.image.inUse) {
imageActions.destroy(image.RepoTags[0].split(':')[0] + ':' + this.state.chosenTag);
}
},
handleRepoClick: function () { handleRepoClick: function () {
var repoUri = 'https://hub.docker.com/'; var repoUri = 'https://hub.docker.com/';
if (this.props.image.namespace === 'library') { if (this.props.image.namespace === 'library') {
@ -108,10 +122,9 @@ var ImageCard = React.createClass({
} else if(this.props.image.short_description){ } else if(this.props.image.short_description){
description = this.props.image.short_description; description = this.props.image.short_description;
} else { } else {
description = "No description."; description = 'No description.';
} }
var logoStyle = { var logoStyle = {
//backgroundImage: `linear-gradient(-180deg, ${this.props.image.gradient_start} 4%, ${this.props.image.gradient_end} 100%)`
backgroundColor: this.props.image.gradient_start backgroundColor: this.props.image.gradient_start
}; };
var imgsrc; var imgsrc;
@ -150,21 +163,74 @@ var ImageCard = React.createClass({
<span className="icon icon-badge-private"></span> <span className="icon icon-badge-private"></span>
); );
} }
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(); let create, overlay;
return ( if (this.props.image.is_local) {
<div className="image-item"> create = (
<div className="actions">
<div className="favorites">
<span className="icon icon-tag"> {this.state.chosenTag}</span>
<span className="text"></span>
</div>
<div className="more-menu" onClick={self.handleMenuOverlayClick}>
<span className="icon icon-more"></span>
</div>
<div className="action" onClick={self.handleClick}>
CREATE
</div>
</div>
);
overlay = (
<div className="overlay menu-overlay"> <div className="overlay menu-overlay">
<div className="menu-item" onClick={this.handleTagOverlayClick.bind(this, this.props.image.name)}> <div className="menu-item" onClick={this.handleTagOverlayClick.bind(this, this.props.image.name)}>
<span className="icon icon-tag"></span><span className="text">SELECTED TAG: <span className="selected-tag">{this.state.chosenTag}</span></span> <span className="icon icon-tag"></span><span className="text">SELECTED TAG: <span className="selected-tag">{this.state.chosenTag}</span></span>
</div> </div>
<div className="menu-item" onClick={this.handleRepoClick}> <div className="remove" onClick={this.handleDeleteImgClick.bind(this, this.props.image)}>
<span className="icon icon-open-external"></span><span className="text">VIEW ON DOCKER HUB</span> <span className="btn btn-delete btn-action has-icon btn-hollow" disabled={this.props.image.inUse ? 'disabled' : null}><span className="icon icon-delete"></span>Delete Tag</span>
</div> </div>
{this.props.image.inUse ? <p className="small">To delete, remove all containers<br/>using the above image</p> : null }
<div className="close-overlay"> <div className="close-overlay">
<a className="btn btn-action circular" onClick={self.handleCloseMenuOverlay}><span className="icon icon-delete"></span></a> <a className="btn btn-action circular" onClick={self.handleCloseMenuOverlay}><span className="icon icon-delete"></span></a>
</div> </div>
</div> </div>
);
} 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 = (
<div className="actions">
<div className="favorites">
<span className="icon icon-favorite"></span>
<span className="text">{favCount}</span>
<span className="icon icon-download"></span>
<span className="text">{pullCount}</span>
</div>
<div className="more-menu" onClick={self.handleMenuOverlayClick}>
<span className="icon icon-more"></span>
</div>
<div className="action" onClick={self.handleClick}>
CREATE
</div>
</div>
);
overlay = (
<div className="overlay menu-overlay">
<div className="menu-item" onClick={this.handleTagOverlayClick.bind(this, this.props.image.name)}>
<span className="icon icon-tag"></span><span className="text">SELECTED TAG: <span className="selected-tag">{this.state.chosenTag}</span></span>
</div>
<div className="menu-item" onClick={this.handleRepoClick}>
<span className="icon icon-open-external"></span><span className="text">VIEW ON DOCKER HUB</span>
</div>
<div className="close-overlay">
<a className="btn btn-action circular" onClick={self.handleCloseMenuOverlay}><span className="icon icon-delete"></span></a>
</div>
</div>
);
}
return (
<div className="image-item">
{overlay}
<div className="overlay tag-overlay"> <div className="overlay tag-overlay">
<p>Please select an image tag.</p> <p>Please select an image tag.</p>
{tags} {tags}
@ -187,20 +253,7 @@ var ImageCard = React.createClass({
{description} {description}
</div> </div>
</div> </div>
<div className="actions"> {create}
<div className="favorites">
<span className="icon icon-favorite"></span>
<span className="text">{favCount}</span>
<span className="icon icon-download"></span>
<span className="text">{pullCount}</span>
</div>
<div className="more-menu" onClick={self.handleMenuOverlayClick}>
<span className="icon icon-more"></span>
</div>
<div className="action" onClick={self.handleClick}>
CREATE
</div>
</div>
</div> </div>
</div> </div>
); );

View File

@ -10,6 +10,8 @@ import repositoryActions from '../actions/RepositoryActions';
import repositoryStore from '../stores/RepositoryStore'; import repositoryStore from '../stores/RepositoryStore';
import accountStore from '../stores/AccountStore'; import accountStore from '../stores/AccountStore';
import accountActions from '../actions/AccountActions'; import accountActions from '../actions/AccountActions';
import imageActions from '../actions/ImageActions';
import imageStore from '../stores/ImageStore';
var _searchPromise = null; var _searchPromise = null;
@ -20,6 +22,8 @@ module.exports = React.createClass({
query: '', query: '',
loading: repositoryStore.loading(), loading: repositoryStore.loading(),
repos: repositoryStore.all(), repos: repositoryStore.all(),
images: imageStore.all(),
imagesErr: imageStore.error,
username: accountStore.getState().username, username: accountStore.getState().username,
verified: accountStore.getState().verified, verified: accountStore.getState().verified,
accountLoading: accountStore.getState().loading, accountLoading: accountStore.getState().loading,
@ -34,6 +38,7 @@ module.exports = React.createClass({
this.refs.searchInput.getDOMNode().focus(); this.refs.searchInput.getDOMNode().focus();
repositoryStore.listen(this.update); repositoryStore.listen(this.update);
accountStore.listen(this.updateAccount); accountStore.listen(this.updateAccount);
imageStore.listen(this.updateImage);
repositoryActions.search(); repositoryActions.search();
}, },
componentWillUnmount: function () { componentWillUnmount: function () {
@ -51,7 +56,14 @@ module.exports = React.createClass({
currentPage: repositoryStore.getState().currentPage, currentPage: repositoryStore.getState().currentPage,
totalPage: repositoryStore.getState().totalPage, totalPage: repositoryStore.getState().totalPage,
previousPage: repositoryStore.getState().previousPage, 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 () { updateAccount: function () {
@ -79,7 +91,8 @@ module.exports = React.createClass({
currentPage: page, currentPage: page,
previousPage: previousPage, previousPage: previousPage,
nextPage: nextPage, nextPage: nextPage,
totalPage: totalPage totalPage: totalPage,
error: null
}); });
_searchPromise = Promise.delay(200).cancellable().then(() => { _searchPromise = Promise.delay(200).cancellable().then(() => {
@ -101,11 +114,17 @@ module.exports = React.createClass({
}, },
handleFilter: function (filter) { handleFilter: function (filter) {
this.setState({error: null});
// If we're clicking on the filter again - refresh // If we're clicking on the filter again - refresh
if (filter === 'userrepos' && this.getQuery().filter === 'userrepos') { if (filter === 'userrepos' && this.getQuery().filter === 'userrepos') {
repositoryActions.repos(); repositoryActions.repos();
} }
if (filter === 'userimages' && this.getQuery().filter === 'userimages') {
imageActions.all();
}
if (filter === 'recommended' && this.getQuery().filter === 'recommended') { if (filter === 'recommended' && this.getQuery().filter === 'recommended') {
repositoryActions.recommended(); repositoryActions.recommended();
} }
@ -187,10 +206,16 @@ module.exports = React.createClass({
</ul> </ul>
</nav> </nav>
) : null; ) : 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) { if (this.state.error) {
results = ( results = (
<div className="no-results"> <div className="no-results">
<h2>There was an error contacting Docker Hub.</h2> <h2 className="error">{errorMsg}</h2>
</div> </div>
); );
paginateResults = null; paginateResults = null;
@ -214,6 +239,34 @@ module.exports = React.createClass({
</div> </div>
); );
paginateResults = null; 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 (<ImageCard key={image.namespace + '/' + image.name + ':' + tags} image={image} chosenTag={image.tags[0]} tags={image.tags} />);
});
let userImageResults = userImageItems.length ? (
<div className="result-grids">
<div>
<h4>My Images</h4>
<div className="result-grid">
{userImageItems}
</div>
</div>
</div>
) : <div className="no-results">
<h2>Cannot find any local image.</h2>
</div>;
results = (
{userImageResults}
);
} else if (this.state.loading) { } else if (this.state.loading) {
results = ( results = (
<div className="no-results"> <div className="no-results">
@ -299,23 +352,30 @@ module.exports = React.createClass({
'icon-search': true, 'icon-search': true,
'search-icon': true 'search-icon': true
}); });
let searchClasses = classNames('search-bar');
if (filter === 'userimages') {
searchClasses = classNames('search-bar', {
hidden: true
});
}
return ( return (
<div className="details"> <div className="details">
<div className="new-container"> <div className="new-container">
<div className="new-container-header"> <div className="new-container-header">
<div className="search"> <div className="search">
<div className="search-bar"> <div className={searchClasses}>
<input type="search" ref="searchInput" className="form-control" placeholder="Search for Docker images from Docker Hub" onChange={this.handleChange}/> <input type="search" ref="searchInput" className="form-control" placeholder="Search for Docker images from Docker Hub" onChange={this.handleChange}/>
<div className={magnifierClasses}></div> <div className={magnifierClasses}></div>
<div className={loadingClasses}><div></div></div> <div className={loadingClasses}><div></div></div>
</div> </div>
</div> </div>
<div className="results-filters"> <div className="results-filters">
<span className="results-filter results-filter-title">FILTER BY</span> <span className="results-filter results-filter-title">FILTER BY</span>
<span className={`results-filter results-all tab ${filter === 'all' ? 'active' : ''}`} onClick={this.handleFilter.bind(this, 'all')}>All</span> <span className={`results-filter results-all tab ${filter === 'all' ? 'active' : ''}`} onClick={this.handleFilter.bind(this, 'all')}>All</span>
<span className={`results-filter results-recommended tab ${filter === 'recommended' ? 'active' : ''}`} onClick={this.handleFilter.bind(this, 'recommended')}>Recommended</span> <span className={`results-filter results-recommended tab ${filter === 'recommended' ? 'active' : ''}`} onClick={this.handleFilter.bind(this, 'recommended')}>Recommended</span>
<span className={`results-filter results-userrepos tab ${filter === 'userrepos' ? 'active' : ''}`} onClick={this.handleFilter.bind(this, 'userrepos')}>My Repos</span> <span className={`results-filter results-userrepos tab ${filter === 'userrepos' ? 'active' : ''}`} onClick={this.handleFilter.bind(this, 'userrepos')}>My Repos</span>
<span className={`results-filter results-userimages tab ${filter === 'userimages' ? 'active' : ''}`} onClick={this.handleFilter.bind(this, 'userimages')}>My Images</span>
</div> </div>
</div> </div>
<div className="results"> <div className="results">

59
src/stores/ImageStore.js Normal file
View File

@ -0,0 +1,59 @@
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);
if (image.inUse) {
finalImages[tags[name]].inUse = image.inUse;
}
} 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);

View File

@ -21,6 +21,15 @@ class TagStore {
this.emitChange(); 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}) { tagsUpdated ({repo, tags}) {
this.tags[repo] = tags; this.tags[repo] = tags;
this.loading[repo] = false; this.loading[repo] = false;

View File

@ -8,20 +8,23 @@ import util from './Util';
import hubUtil from './HubUtil'; import hubUtil from './HubUtil';
import metrics from '../utils/MetricsUtil'; import metrics from '../utils/MetricsUtil';
import containerServerActions from '../actions/ContainerServerActions'; import containerServerActions from '../actions/ContainerServerActions';
import imageServerActions from '../actions/ImageServerActions';
import Promise from 'bluebird';
import rimraf from 'rimraf'; import rimraf from 'rimraf';
import stream from 'stream'; import stream from 'stream';
import JSONStream from 'JSONStream'; import JSONStream from 'JSONStream';
import Promise from 'bluebird';
export default { var DockerUtil = {
host: null, host: null,
client: null, client: null,
placeholders: {}, placeholders: {},
stream: null, stream: null,
eventStream: null, eventStream: null,
activeContainerName: null, activeContainerName: null,
localImages: null,
imagesUsed: [],
setup (ip, name) { setup (ip, name) {
if (!ip && !name) { if (!ip && !name) {
@ -76,7 +79,7 @@ export default {
init () { init () {
this.placeholders = JSON.parse(localStorage.getItem('placeholders')) || {}; this.placeholders = JSON.parse(localStorage.getItem('placeholders')) || {};
this.fetchAllContainers(); this.refresh();
this.listen(); this.listen();
// Resume pulling containers that were previously being pulled // Resume pulling containers that were previously being pulled
@ -166,6 +169,7 @@ export default {
this.startContainer(name); this.startContainer(name);
delete this.placeholders[name]; delete this.placeholders[name];
localStorage.setItem('placeholders', JSON.stringify(this.placeholders)); localStorage.setItem('placeholders', JSON.stringify(this.placeholders));
this.refresh();
}); });
}); });
}); });
@ -189,12 +193,17 @@ export default {
console.error(err); console.error(err);
return; return;
} }
this.imagesUsed = [];
async.map(containers, (container, callback) => { async.map(containers, (container, callback) => {
this.client.getContainer(container.Id).inspect((error, container) => { this.client.getContainer(container.Id).inspect((error, container) => {
if (error) { if (error) {
callback(null, null); callback(null, null);
return; return;
} }
let imgSha = container.Image.replace('sha256:', '');
if (_.indexOf(this.imagesUsed, imgSha) === -1) {
this.imagesUsed.push(imgSha);
}
container.Name = container.Name.replace('/', ''); container.Name = container.Name.replace('/', '');
callback(null, container); callback(null, container);
}); });
@ -206,11 +215,50 @@ export default {
return; return;
} }
containerServerActions.allUpdated({containers: _.indexBy(containers.concat(_.values(this.placeholders)), 'Name')}); containerServerActions.allUpdated({containers: _.indexBy(containers.concat(_.values(this.placeholders)), 'Name')});
this.fetchAllImages();
}); });
}); });
}, },
run (name, repository, tag) { fetchAllImages () {
this.client.listImages((err, list) => {
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);
}
});
},
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);
this.refresh();
}
});
return true;
}
});
});
},
run (name, repository, tag, local = false) {
tag = tag || 'latest'; tag = tag || 'latest';
let imageName = repository + ':' + tag; let imageName = repository + ':' + tag;
@ -231,30 +279,34 @@ export default {
this.placeholders[name] = placeholderData; this.placeholders[name] = placeholderData;
localStorage.setItem('placeholders', JSON.stringify(this.placeholders)); localStorage.setItem('placeholders', JSON.stringify(this.placeholders));
if (local) {
this.pullImage(repository, tag, error => {
if (error) {
containerServerActions.error({name, error});
return;
}
if (!this.placeholders[name]) {
return;
}
this.createContainer(name, {Image: imageName, Tty: true, OpenStdin: true}); this.createContainer(name, {Image: imageName, Tty: true, OpenStdin: true});
}, } else {
this.pullImage(repository, tag, error => {
if (error) {
containerServerActions.error({name, error});
this.refresh();
return;
}
// progress is actually the progression PER LAYER (combined in columns) if (!this.placeholders[name]) {
// not total because it's not accurate enough return;
progress => { }
containerServerActions.progress({name, progress});
}, 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) { updateContainer (name, data) {
@ -262,6 +314,7 @@ export default {
existing.inspect((error, existingData) => { existing.inspect((error, existingData) => {
if (error) { if (error) {
containerServerActions.error({name, error}); containerServerActions.error({name, error});
this.refresh();
return; return;
} }
@ -298,6 +351,7 @@ export default {
if (error) { if (error) {
// TODO: handle error // TODO: handle error
containerServerActions.error({newName, error}); containerServerActions.error({newName, error});
this.refresh();
} }
rimraf(newPath, () => { rimraf(newPath, () => {
if (fs.existsSync(oldPath)) { if (fs.existsSync(oldPath)) {
@ -319,11 +373,13 @@ export default {
this.client.getContainer(name).stop({t: 5}, stopError => { this.client.getContainer(name).stop({t: 5}, stopError => {
if (stopError && stopError.statusCode !== 304) { if (stopError && stopError.statusCode !== 304) {
containerServerActions.error({name, stopError}); containerServerActions.error({name, stopError});
this.refresh();
return; return;
} }
this.client.getContainer(name).start(startError => { this.client.getContainer(name).start(startError => {
if (startError && startError.statusCode !== 304) { if (startError && startError.statusCode !== 304) {
containerServerActions.error({name, startError}); containerServerActions.error({name, startError});
this.refresh();
return; return;
} }
this.fetchContainer(name); this.fetchContainer(name);
@ -335,6 +391,7 @@ export default {
this.client.getContainer(name).stop({t: 5}, error => { this.client.getContainer(name).stop({t: 5}, error => {
if (error && error.statusCode !== 304) { if (error && error.statusCode !== 304) {
containerServerActions.error({name, error}); containerServerActions.error({name, error});
this.refresh();
return; return;
} }
this.fetchContainer(name); this.fetchContainer(name);
@ -345,6 +402,7 @@ export default {
this.client.getContainer(name).start(error => { this.client.getContainer(name).start(error => {
if (error && error.statusCode !== 304) { if (error && error.statusCode !== 304) {
containerServerActions.error({name, error}); containerServerActions.error({name, error});
this.refresh();
return; return;
} }
this.fetchContainer(name); this.fetchContainer(name);
@ -356,15 +414,17 @@ export default {
containerServerActions.destroyed({id: name}); containerServerActions.destroyed({id: name});
delete this.placeholders[name]; delete this.placeholders[name];
localStorage.setItem('placeholders', JSON.stringify(this.placeholders)); localStorage.setItem('placeholders', JSON.stringify(this.placeholders));
this.refresh();
return; return;
} }
let container = this.client.getContainer(name); let container = this.client.getContainer(name);
container.unpause(function () { container.unpause( () => {
container.kill(function () { container.kill( () => {
container.remove(function (error) { container.remove( (error) => {
if (error) { if (error) {
containerServerActions.error({name, error}); containerServerActions.error({name, error});
this.refresh();
return; return;
} }
containerServerActions.destroyed({id: name}); containerServerActions.destroyed({id: name});
@ -372,6 +432,7 @@ export default {
if (fs.existsSync(volumePath)) { if (fs.existsSync(volumePath)) {
rimraf(volumePath, () => {}); rimraf(volumePath, () => {});
} }
this.refresh();
}); });
}); });
}); });
@ -474,9 +535,11 @@ export default {
} }
stream.setEncoding('utf8'); stream.setEncoding('utf8');
stream.pipe(JSONStream.parse()).on('data', data => { stream.on('data', json => {
if (data.status === 'pull' || data.status === 'untag' || data.status === 'delete' || data.status === 'attach') { let data = JSON.parse(json);
return;
if (data.status === 'pull' || data.status === 'untag' || data.status === 'delete' || data.status === 'attach') {
this.refresh();
} }
if (data.status === 'destroy') { if (data.status === 'destroy') {
@ -519,6 +582,7 @@ export default {
this.client.pull(repository + ':' + tag, opts, (err, stream) => { this.client.pull(repository + ':' + tag, opts, (err, stream) => {
if (err) { if (err) {
console.log('Err: %o', err);
callback(err); callback(err);
return; return;
} }
@ -563,7 +627,7 @@ export default {
if (i < leftOverLayers) { if (i < leftOverLayers) {
layerAmount += 1; layerAmount += 1;
} }
columns.progress[i] = {layerIDs: [], nbLayers:0 , maxLayers: layerAmount, value: 0.0}; columns.progress[i] = {layerIDs: [], nbLayers: 0, maxLayers: layerAmount, value: 0.0};
} }
} }
@ -615,5 +679,11 @@ export default {
callback(error); callback(error);
}); });
}); });
},
refresh () {
this.fetchAllContainers();
} }
}; };
module.exports = DockerUtil;

View File

@ -67,6 +67,10 @@
justify-content: center; justify-content: center;
flex-shrink: 0; flex-shrink: 0;
.error {
color: red;
}
img { img {
width: 380px; width: 380px;
} }
@ -160,6 +164,11 @@
font-weight: 500; font-weight: 500;
margin-right: 0.7rem; margin-right: 0.7rem;
} }
.results-userimages {
border-left: 1px solid @gray-lighter;
padding-left: 1.2rem;
padding-right: 1.2rem;
}
} }
} }
} }
@ -227,6 +236,31 @@
bottom: 1rem; bottom: 1rem;
right: 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 { .tag-overlay {
z-index: 1000; z-index: 1000;