зеркало из https://github.com/docker/kitematic.git
Merged latest master
Signed-off-by: French Ben <me+git@frenchben.com>
This commit is contained in:
Коммит
c1f6f80936
|
@ -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/)
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
|
@ -0,0 +1,14 @@
|
|||
import alt from '../alt';
|
||||
|
||||
class ImageServerActions {
|
||||
constructor () {
|
||||
this.generateActions(
|
||||
'added',
|
||||
'updated',
|
||||
'destroyed',
|
||||
'error'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default alt.createActions(ImageServerActions);
|
|
@ -6,6 +6,10 @@ class TagActions {
|
|||
this.dispatch({repo});
|
||||
regHubUtil.tags(repo);
|
||||
}
|
||||
|
||||
localTags (repo, tags) {
|
||||
this.dispatch({repo, tags});
|
||||
}
|
||||
}
|
||||
|
||||
export default alt.createActions(TagActions);
|
||||
|
|
|
@ -66,8 +66,14 @@ setupUtil.setup().then(() => {
|
|||
throw err;
|
||||
});
|
||||
|
||||
|
||||
ipcRenderer.on('application:quitting', () => {
|
||||
docker.detachEvent();
|
||||
if (localStorage.getItem('settings.closeVMOnQuit') === 'true') {
|
||||
machine.stop();
|
||||
}
|
||||
});
|
||||
|
||||
window.onbeforeunload = function () {
|
||||
docker.detachEvent();
|
||||
};
|
||||
|
|
|
@ -47,6 +47,7 @@ app.on('ready', function () {
|
|||
return false;
|
||||
});
|
||||
|
||||
|
||||
if (os.platform() === 'win32') {
|
||||
mainWindow.on('close', function () {
|
||||
mainWindow.webContents.send('application:quitting');
|
||||
|
|
|
@ -51,10 +51,20 @@ var ContainerHome = React.createClass({
|
|||
|
||||
let body;
|
||||
if (this.props.container.Error) {
|
||||
let error = this.props.container.Error.message;
|
||||
console.log('Err: %o - %o', typeof error, error);
|
||||
if (error.indexOf('ETIMEDOUT') !== -1) {
|
||||
error = 'Timeout error - Try and restart your VM by running: \n"docker-machine restart default" in a terminal';
|
||||
}
|
||||
if (error.indexOf('ECONNREFUSED') !== -1) {
|
||||
error = 'Is your VM up and running? Check that "docker ps" works in a terminal.';
|
||||
}
|
||||
body = (
|
||||
<div className="details-progress error">
|
||||
<h2>We're sorry. There seems to be an error:</h2>
|
||||
<p className="error-message">{this.props.container.Error}</p>
|
||||
{error.split('\n').map(i => {
|
||||
return <p className="error-message">{i}</p>;
|
||||
})}
|
||||
<p>If this error is invalid, please file a ticket on our Github repo.</p>
|
||||
<a className="btn btn-action" onClick={this.handleErrorClick}>File Ticket</a>
|
||||
</div>
|
||||
|
|
|
@ -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 && !this.props.image.inUse) {
|
||||
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,74 @@ var ImageCard = React.createClass({
|
|||
<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();
|
||||
return (
|
||||
<div className="image-item">
|
||||
|
||||
let create, overlay;
|
||||
if (this.props.image.is_local) {
|
||||
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="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 className="remove" onClick={this.handleDeleteImgClick.bind(this, this.props.image)}>
|
||||
<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>
|
||||
{this.props.image.inUse ? <p className="small">To delete, remove all containers<br/>using the above image</p> : null }
|
||||
<div className="close-overlay">
|
||||
<a className="btn btn-action circular" onClick={self.handleCloseMenuOverlay}><span className="icon icon-delete"></span></a>
|
||||
</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">
|
||||
<p>Please select an image tag.</p>
|
||||
{tags}
|
||||
|
@ -187,20 +253,7 @@ var ImageCard = React.createClass({
|
|||
{description}
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
{create}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -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({
|
|||
</ul>
|
||||
</nav>
|
||||
) : 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 = (
|
||||
<div className="no-results">
|
||||
<h2>There was an error contacting Docker Hub.</h2>
|
||||
<h2 className="error">{errorMsg}</h2>
|
||||
</div>
|
||||
);
|
||||
paginateResults = null;
|
||||
|
@ -214,6 +239,34 @@ module.exports = React.createClass({
|
|||
</div>
|
||||
);
|
||||
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) {
|
||||
results = (
|
||||
<div className="no-results">
|
||||
|
@ -299,23 +352,30 @@ 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 (
|
||||
<div className="details">
|
||||
<div className="new-container">
|
||||
<div className="new-container-header">
|
||||
<div className="search">
|
||||
<div className="search-bar">
|
||||
<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={loadingClasses}><div></div></div>
|
||||
</div>
|
||||
<div className={searchClasses}>
|
||||
<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={loadingClasses}><div></div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="results-filters">
|
||||
<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-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-userimages tab ${filter === 'userimages' ? 'active' : ''}`} onClick={this.handleFilter.bind(this, 'userimages')}>My Images</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="results">
|
||||
|
|
|
@ -131,6 +131,17 @@ var MenuTemplate = function () {
|
|||
{
|
||||
label: 'View',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Refresh Container List',
|
||||
accelerator: util.CommandOrCtrl() + '+R',
|
||||
enabled: !!docker.host,
|
||||
click: function() {
|
||||
metrics.track('Refreshed Container List', {
|
||||
from: 'menu'
|
||||
});
|
||||
docker.fetchAllContainers();
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Toggle Chromium Developer Tools',
|
||||
accelerator: 'Alt+' + util.CommandOrCtrl() + '+I',
|
||||
|
|
|
@ -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);
|
|
@ -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;
|
||||
|
|
|
@ -33,7 +33,6 @@ var ContainerUtil = {
|
|||
var [dockerPort, portType] = key.split('/');
|
||||
var localUrl = null;
|
||||
var port = null;
|
||||
|
||||
if (value && value.length) {
|
||||
port = value[0].HostPort;
|
||||
}
|
||||
|
|
|
@ -8,19 +8,23 @@ 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';
|
||||
import Promise from 'bluebird';
|
||||
|
||||
|
||||
|
||||
export default {
|
||||
var DockerUtil = {
|
||||
host: null,
|
||||
client: null,
|
||||
placeholders: {},
|
||||
streams: {},
|
||||
stream: null,
|
||||
eventStream: null,
|
||||
activeContainerName: null,
|
||||
localImages: null,
|
||||
imagesUsed: [],
|
||||
|
||||
setup (ip, name) {
|
||||
if (!ip && !name) {
|
||||
|
@ -83,7 +87,7 @@ export default {
|
|||
|
||||
init () {
|
||||
this.placeholders = JSON.parse(localStorage.getItem('placeholders')) || {};
|
||||
this.fetchAllContainers();
|
||||
this.refresh();
|
||||
this.listen();
|
||||
|
||||
// Resume pulling containers that were previously being pulled
|
||||
|
@ -158,7 +162,7 @@ export default {
|
|||
if (image.Config.Cmd) {
|
||||
containerData.Cmd = image.Config.Cmd;
|
||||
} else if (!image.Config.Entrypoint) {
|
||||
containerData.Cmd = 'bash';
|
||||
containerData.Cmd = 'sh';
|
||||
}
|
||||
|
||||
let existing = this.client.getContainer(name);
|
||||
|
@ -173,6 +177,7 @@ export default {
|
|||
this.startContainer(name);
|
||||
delete this.placeholders[name];
|
||||
localStorage.setItem('placeholders', JSON.stringify(this.placeholders));
|
||||
this.refresh();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -196,12 +201,17 @@ export default {
|
|||
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);
|
||||
});
|
||||
|
@ -213,11 +223,51 @@ export default {
|
|||
return;
|
||||
}
|
||||
containerServerActions.allUpdated({containers: _.indexBy(containers.concat(_.values(this.placeholders)), 'Name')});
|
||||
this.logs();
|
||||
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';
|
||||
let imageName = repository + ':' + tag;
|
||||
|
||||
|
@ -238,30 +288,34 @@ 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});
|
||||
this.refresh();
|
||||
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) {
|
||||
|
@ -269,6 +323,7 @@ export default {
|
|||
existing.inspect((error, existingData) => {
|
||||
if (error) {
|
||||
containerServerActions.error({name, error});
|
||||
this.refresh();
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -305,6 +360,7 @@ export default {
|
|||
if (error) {
|
||||
// TODO: handle error
|
||||
containerServerActions.error({newName, error});
|
||||
this.refresh();
|
||||
}
|
||||
rimraf(newPath, () => {
|
||||
if (fs.existsSync(oldPath)) {
|
||||
|
@ -326,11 +382,13 @@ export default {
|
|||
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);
|
||||
|
@ -342,6 +400,7 @@ export default {
|
|||
this.client.getContainer(name).stop({t: 5}, error => {
|
||||
if (error && error.statusCode !== 304) {
|
||||
containerServerActions.error({name, error});
|
||||
this.refresh();
|
||||
return;
|
||||
}
|
||||
this.fetchContainer(name);
|
||||
|
@ -352,6 +411,7 @@ export default {
|
|||
this.client.getContainer(name).start(error => {
|
||||
if (error && error.statusCode !== 304) {
|
||||
containerServerActions.error({name, error});
|
||||
this.refresh();
|
||||
return;
|
||||
}
|
||||
this.fetchContainer(name);
|
||||
|
@ -363,15 +423,17 @@ export default {
|
|||
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});
|
||||
|
@ -379,13 +441,14 @@ export default {
|
|||
if (fs.existsSync(volumePath)) {
|
||||
rimraf(volumePath, () => {});
|
||||
}
|
||||
this.refresh();
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
active (name) {
|
||||
this.detach();
|
||||
this.detachLog();
|
||||
this.activeContainerName = name;
|
||||
|
||||
if (name) {
|
||||
|
@ -408,6 +471,7 @@ export default {
|
|||
if (err) {
|
||||
// socket hang up can be captured
|
||||
console.error(err);
|
||||
containerServerActions.error({name: this.activeContainerName, err});
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -439,9 +503,7 @@ export default {
|
|||
return;
|
||||
}
|
||||
|
||||
if (this.stream) {
|
||||
this.detach();
|
||||
}
|
||||
this.detachLog()
|
||||
this.stream = logStream;
|
||||
|
||||
let timeout = null;
|
||||
|
@ -460,14 +522,22 @@ export default {
|
|||
});
|
||||
},
|
||||
|
||||
detach () {
|
||||
detachLog() {
|
||||
if (this.stream) {
|
||||
this.stream.destroy();
|
||||
this.stream = null;
|
||||
}
|
||||
},
|
||||
detachEvent() {
|
||||
if (this.eventStream) {
|
||||
this.eventStream.destroy();
|
||||
this.eventStream = null;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
listen () {
|
||||
this.detachEvent()
|
||||
this.client.getEvents((error, stream) => {
|
||||
if (error || !stream) {
|
||||
// TODO: Add app-wide error handler
|
||||
|
@ -475,20 +545,22 @@ 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.refresh();
|
||||
}
|
||||
|
||||
if (data.status === 'destroy') {
|
||||
containerServerActions.destroyed({id: data.id});
|
||||
this.detach(data.id);
|
||||
this.detachLog()
|
||||
} else if (data.status === 'kill') {
|
||||
containerServerActions.kill({id: data.id});
|
||||
this.detach(data.id);
|
||||
this.detachLog()
|
||||
} else if (data.status === 'stop') {
|
||||
containerServerActions.stopped({id: data.id});
|
||||
this.detach(data.id);
|
||||
this.detachLog()
|
||||
} else if (data.status === 'create') {
|
||||
this.logs();
|
||||
this.fetchContainer(data.id);
|
||||
|
@ -499,6 +571,7 @@ export default {
|
|||
this.fetchContainer(data.id);
|
||||
}
|
||||
});
|
||||
this.eventStream = stream;
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -519,6 +592,7 @@ export default {
|
|||
|
||||
this.client.pull(repository + ':' + tag, opts, (err, stream) => {
|
||||
if (err) {
|
||||
console.log('Err: %o', err);
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
@ -563,7 +637,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};
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -615,5 +689,11 @@ export default {
|
|||
callback(error);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
refresh () {
|
||||
this.fetchAllContainers();
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = DockerUtil;
|
||||
|
|
|
@ -18,7 +18,6 @@ const precreateCheckExitCode = 3;
|
|||
|
||||
let _retryPromise = null;
|
||||
let _timers = [];
|
||||
let useNative = util.isNative() ? util.isNative() : true;
|
||||
|
||||
export default {
|
||||
simulateProgress (estimateSeconds) {
|
||||
|
@ -40,6 +39,7 @@ export default {
|
|||
async useVbox () {
|
||||
metrics.track('Retried Setup with VBox');
|
||||
router.get().transitionTo('loading');
|
||||
util.native = false;
|
||||
setupServerActions.error({ error: { message: null }});
|
||||
_retryPromise.resolve();
|
||||
},
|
||||
|
|
|
@ -10,6 +10,7 @@ const dialog = remote.dialog;
|
|||
const app = remote.app;
|
||||
|
||||
module.exports = {
|
||||
native: null,
|
||||
execFile: function (args, options) {
|
||||
return new Promise((resolve, reject) => {
|
||||
child_process.execFile(args[0], args.slice(1), options, (error, stdout) => {
|
||||
|
@ -39,34 +40,34 @@ module.exports = {
|
|||
return process.platform === 'linux';
|
||||
},
|
||||
isNative: function () {
|
||||
let native = null;
|
||||
if (this.isWindows()) {
|
||||
native = request.get({
|
||||
url: `http://docker.local:2375/version`
|
||||
}, (error, response, body) => {
|
||||
if (error !== null || response.statusCode !== 200 ) {
|
||||
return false;
|
||||
} else {
|
||||
let data = JSON.parse(body);
|
||||
return data;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
try {
|
||||
// Check if file exists
|
||||
let stats = fs.statSync('/var/run/docker.sock');
|
||||
if (stats.isSocket()) {
|
||||
native = true;
|
||||
}
|
||||
} catch (e) {
|
||||
if (this.isLinux()) {
|
||||
native = true;
|
||||
} else {
|
||||
native = false;
|
||||
if (this.native === null) {
|
||||
if (this.isWindows()) {
|
||||
this.native = request.get({
|
||||
url: `http://docker.local:2375/version`
|
||||
}, (error, response) => {
|
||||
if (error !== null || response.statusCode !== 200 ) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
try {
|
||||
// Check if file exists
|
||||
let stats = fs.statSync('/var/run/docker.sock');
|
||||
if (stats.isSocket()) {
|
||||
this.native = true;
|
||||
}
|
||||
} catch (e) {
|
||||
if (this.isLinux()) {
|
||||
this.native = true;
|
||||
} else {
|
||||
this.native = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return native;
|
||||
return this.native;
|
||||
},
|
||||
binsPath: function () {
|
||||
return this.isWindows() ? path.join(this.home(), 'Kitematic-bins') : path.join('/usr/local/bin');
|
||||
|
|
|
@ -67,6 +67,10 @@
|
|||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
|
||||
.error {
|
||||
color: red;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 380px;
|
||||
}
|
||||
|
@ -160,6 +164,11 @@
|
|||
font-weight: 500;
|
||||
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;
|
||||
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;
|
||||
|
|
Загрузка…
Ссылка в новой задаче