Merge pull request #155 from crosbymichael/labels

Labels and Image tags
pull/2/head
Kevan Ahlquist 2015-12-06 15:01:02 -06:00
commit d881d97c06
9 changed files with 155 additions and 35 deletions

View File

@ -54,7 +54,7 @@
<tbody> <tbody>
<tr> <tr>
<td>Created:</td> <td>Created:</td>
<td>{{ container.Created }}</td> <td>{{ container.Created | date: 'medium' }}</td>
</tr> </tr>
<tr> <tr>
<td>Path:</td> <td>Path:</td>
@ -62,7 +62,9 @@
</tr> </tr>
<tr> <tr>
<td>Args:</td> <td>Args:</td>
<td>{{ container.Args }}</td> <td>
<pre>{{ container.Args.join(' ') || 'None' }}</pre>
</td>
</tr> </tr>
<tr> <tr>
<td>Exposed Ports:</td> <td>Exposed Ports:</td>
@ -80,6 +82,21 @@
</ul> </ul>
</td> </td>
</tr> </tr>
<tr>
<td>Labels:</td>
<td>
<table role="table" class="table">
<tr>
<th>Key</th>
<th>Value</th>
</tr>
<tr ng-repeat="(k, v) in container.Config.Labels">
<td>{{ k }}</td>
<td>{{ v }}</td>
</tr>
</table>
</td>
</tr>
<tr> <tr>
<td>Publish All:</td> <td>Publish All:</td>
@ -110,7 +127,9 @@
</tr> </tr>
<tr> <tr>
<td>Entrypoint:</td> <td>Entrypoint:</td>
<td>{{ container.Config.Entrypoint }}</td> <td>
<pre>{{ container.Config.Entrypoint.join(' ') }}</pre>
</td>
</tr> </tr>
<tr> <tr>
<td>Volumes:</td> <td>Volumes:</td>
@ -127,7 +146,15 @@
</tr> </tr>
<tr> <tr>
<td>State:</td> <td>State:</td>
<td><span class="label {{ container.State|getstatelabel }}">{{ container.State|getstatetext }}</span></td> <td>
<accordion close-others="true">
<accordion-group heading="{{ container.State|getstatetext }}">
<ul>
<li ng-repeat="(key, val) in container.State">{{key}} : {{ val }}</li>
</ul>
</accordion-group>
</accordion>
</td>
</tr> </tr>
<tr> <tr>
<td>Logs:</td> <td>Logs:</td>

View File

@ -6,10 +6,10 @@
<div class="detail"> <div class="detail">
<h4>Image: {{ tag }}</h4> <h4>Image: {{ id }}</h4>
<div class="btn-group detail"> <div class="btn-group detail">
<button class="btn btn-success" data-toggle="modal" data-target="#create-modal">Create</button> <button class="btn btn-success" data-toggle="modal" data-target="#create-modal">Start Container</button>
</div> </div>
<div> <div>
@ -22,9 +22,19 @@
<table class="table table-striped"> <table class="table table-striped">
<tbody> <tbody>
<tr>
<td>Tags:</td>
<td>
<ul>
<li ng-repeat="tag in RepoTags">{{ tag }}
<button ng-click="removeImage(tag)" class="btn btn-sm btn-danger">Remove tag</button>
</li>
</ul>
</td>
</tr>
<tr> <tr>
<td>Created:</td> <td>Created:</td>
<td>{{ image.Created }}</td> <td>{{ image.Created | date: 'medium'}}</td>
</tr> </tr>
<tr> <tr>
<td>Parent:</td> <td>Parent:</td>
@ -89,14 +99,15 @@
<legend>Tag image</legend> <legend>Tag image</legend>
<div class="form-group"> <div class="form-group">
<label>Tag:</label> <label>Tag:</label>
<input type="text" placeholder="repo..." ng-model="tag.repo" class="form-control"> <input type="text" placeholder="repo" ng-model="tagInfo.repo" class="form-control">
<input type="text" placeholder="version" ng-model="tagInfo.version" class="form-control">
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="checkbox"> <label class="checkbox">
<input type="checkbox" ng-model="tag.force" class="form-control"/> Force? <input type="checkbox" ng-model="tagInfo.force" class="form-control"/> Force?
</label> </label>
</div> </div>
<input type="button" ng-click="updateTag()" value="Tag" class="btn btn-primary"/> <input type="button" ng-click="addTag()" value="Add Tag" class="btn btn-primary"/>
</fieldset> </fieldset>
</form> </form>
</div> </div>
@ -104,6 +115,6 @@
<hr/> <hr/>
<div class="btn-remove"> <div class="btn-remove">
<button class="btn btn-large btn-block btn-primary btn-danger" ng-click="remove()">Remove Image</button> <button class="btn btn-large btn-block btn-primary btn-danger" ng-click="removeImage(id)">Remove Image</button>
</div> </div>
</div> </div>

View File

@ -2,11 +2,22 @@ angular.module('image', [])
.controller('ImageController', ['$scope', '$q', '$routeParams', '$location', 'Image', 'Container', 'Messages', 'LineChart', .controller('ImageController', ['$scope', '$q', '$routeParams', '$location', 'Image', 'Container', 'Messages', 'LineChart',
function ($scope, $q, $routeParams, $location, Image, Container, Messages, LineChart) { function ($scope, $q, $routeParams, $location, Image, Container, Messages, LineChart) {
$scope.history = []; $scope.history = [];
$scope.tag = {repo: '', force: false}; $scope.tagInfo = {repo: '', version: '', force: false};
$scope.id = '';
$scope.repoTags = [];
$scope.remove = function () { $scope.removeImage = function (id) {
Image.remove({id: $routeParams.id}, function (d) { Image.remove({id: id}, function (d) {
Messages.send("Image Removed", $routeParams.id); d.forEach(function(msg){
var key = Object.keys(msg)[0];
Messages.send(key, msg[key]);
});
// If last message key is 'Deleted' then assume the image is gone and send to images page
if (d[d.length-1].Deleted) {
$location.path('/images');
} else {
$location.path('/images/' + $scope.id); // Refresh the current page.
}
}, function (e) { }, function (e) {
$scope.error = e.data; $scope.error = e.data;
$('#error-message').show(); $('#error-message').show();
@ -19,24 +30,30 @@ angular.module('image', [])
}); });
}; };
$scope.updateTag = function () { $scope.addTag = function () {
var tag = $scope.tag; var tag = $scope.tagInfo;
Image.tag({id: $routeParams.id, repo: tag.repo, force: tag.force ? 1 : 0}, function (d) { Image.tag({
id: $routeParams.id,
repo: tag.repo,
tag: tag.version,
force: tag.force ? 1 : 0
}, function (d) {
Messages.send("Tag Added", $routeParams.id); Messages.send("Tag Added", $routeParams.id);
$location.path('/images/' + $scope.id);
}, function (e) { }, function (e) {
$scope.error = e.data; $scope.error = e.data;
$('#error-message').show(); $('#error-message').show();
}); });
}; };
function getContainersFromImage($q, Container, tag) { function getContainersFromImage($q, Container, imageId) {
var defer = $q.defer(); var defer = $q.defer();
Container.query({all: 1, notruc: 1}, function (d) { Container.query({all: 1, notruc: 1}, function (d) {
var containers = []; var containers = [];
for (var i = 0; i < d.length; i++) { for (var i = 0; i < d.length; i++) {
var c = d[i]; var c = d[i];
if (c.Image === tag) { if (c.ImageID === imageId) {
containers.push(new ContainerViewModel(c)); containers.push(new ContainerViewModel(c));
} }
} }
@ -48,18 +65,14 @@ angular.module('image', [])
Image.get({id: $routeParams.id}, function (d) { Image.get({id: $routeParams.id}, function (d) {
$scope.image = d; $scope.image = d;
$scope.tag = d.id; $scope.id = d.Id;
var t = $routeParams.tag; $scope.RepoTags = d.RepoTags;
if (t && t !== ":") {
$scope.tag = t;
var promise = getContainersFromImage($q, Container, t);
promise.then(function (containers) { getContainersFromImage($q, Container, $scope.id).then(function (containers) {
LineChart.build('#containers-started-chart', containers, function (c) { LineChart.build('#containers-started-chart', containers, function (c) {
return new Date(c.Created * 1000).toLocaleDateString(); return new Date(c.Created * 1000).toLocaleDateString();
});
}); });
} });
}, function (e) { }, function (e) {
if (e.status === 404) { if (e.status === 404) {
$('.detail').hide(); $('.detail').hide();

View File

@ -11,6 +11,7 @@ angular.module('startContainer', ['ui.bootstrap'])
$scope.config = { $scope.config = {
Env: [], Env: [],
Labels: [],
Volumes: [], Volumes: [],
SecurityOpts: [], SecurityOpts: [],
HostConfig: { HostConfig: {
@ -66,6 +67,11 @@ angular.module('startContainer', ['ui.bootstrap'])
config.Env = config.Env.map(function (envar) { config.Env = config.Env.map(function (envar) {
return envar.name + '=' + envar.value; return envar.name + '=' + envar.value;
}); });
var labels = {};
config.Labels = config.Labels.forEach(function(label) {
labels[label.key] = label.value;
});
config.Labels = labels;
config.Volumes = getNames(config.Volumes); config.Volumes = getNames(config.Volumes);
config.SecurityOpts = getNames(config.SecurityOpts); config.SecurityOpts = getNames(config.SecurityOpts);

View File

@ -148,6 +148,32 @@
variable variable
</button> </button>
</div> </div>
<div class="form-group">
<label>Labels:</label>
<div ng-repeat="label in config.Labels">
<div class="form-group form-inline">
<div class="form-group">
<label class="sr-only">Key:</label>
<input type="text" ng-model="label.key" class="form-control"
placeholder="key"/>
</div>
<div class="form-group">
<label class="sr-only">Value:</label>
<input type="text" ng-model="label.value" class="form-control"
placeholder="value"/>
</div>
<div class="form-group">
<button class="btn btn-danger btn-xs form-control"
ng-click="rmEntry(config.Labels, label)">Remove
</button>
</div>
</div>
</div>
<button type="button" class="btn btn-success btn-sm"
ng-click="addEntry(config.Labels, {key: '', value: ''})">Add Label
</button>
</div>
</fieldset> </fieldset>
</accordion-group> </accordion-group>
<accordion-group heading="HostConfig options" is-open="menuStatus.hostConfigOpen"> <accordion-group heading="HostConfig options" is-open="menuStatus.hostConfigOpen">

View File

@ -51,7 +51,7 @@ angular.module('dockerui.filters', [])
'use strict'; 'use strict';
return function (state) { return function (state) {
if (state === undefined) { if (state === undefined) {
return ''; return 'label-default';
} }
if (state.Ghost && state.Running) { if (state.Ghost && state.Running) {
@ -60,7 +60,7 @@ angular.module('dockerui.filters', [])
if (state.Running) { if (state.Running) {
return 'label-success'; return 'label-success';
} }
return ''; return 'label-default';
}; };
}) })
.filter('humansize', function () { .filter('humansize', function () {

View File

@ -91,7 +91,7 @@ angular.module('dockerui.services', ['ngResource'])
}, },
insert: {method: 'POST', params: {id: '@id', action: 'insert'}}, insert: {method: 'POST', params: {id: '@id', action: 'insert'}},
push: {method: 'POST', params: {id: '@id', action: 'push'}}, push: {method: 'POST', params: {id: '@id', action: 'push'}},
tag: {method: 'POST', params: {id: '@id', action: 'tag', force: 0, repo: '@repo'}}, tag: {method: 'POST', params: {id: '@id', action: 'tag', force: 0, repo: '@repo', tag: '@tag'}},
remove: {method: 'DELETE', params: {id: '@id'}, isArray: true} remove: {method: 'DELETE', params: {id: '@id'}, isArray: true}
}); });
}]) }])

View File

@ -111,6 +111,43 @@ describe('startContainerController', function () {
}); });
}); });
describe('Create and start a container with labels', function () {
it('should issue a correct create request to the Docker remote API', function () {
var controller = createController();
var id = '6abd8bfba81cf8a05a76a4bdefcb36c4b66cd02265f4bfcd0e236468696ebc6c';
var expectedBody = {
'name': 'container-name',
'Labels': {
"org.foo.bar": "Baz",
"com.biz.baz": "Boo"
}
};
expectGetContainers();
$httpBackend.expectPOST('dockerapi/containers/create?name=container-name', expectedBody).respond({
'Id': id,
'Warnings': null
});
$httpBackend.expectPOST('dockerapi/containers/' + id + '/start').respond({
'id': id,
'Warnings': null
});
scope.config.name = 'container-name';
scope.config.Labels = [{
key: 'org.foo.bar',
value: 'Baz'
}, {
key: 'com.biz.baz',
value: 'Boo'
}];
scope.create();
$httpBackend.flush();
});
});
describe('Create and start a container with volumesFrom', function () { describe('Create and start a container with volumesFrom', function () {
it('should issue a correct create request to the Docker remote API', function () { it('should issue a correct create request to the Docker remote API', function () {
var controller = createController(); var controller = createController();

View File

@ -73,8 +73,8 @@ describe('filters', function () {
}); });
describe('getstatelabel', function () { describe('getstatelabel', function () {
it('should return an empty string when state is undefined', inject(function (getstatelabelFilter) { it('should return default when state is undefined', inject(function (getstatelabelFilter) {
expect(getstatelabelFilter(undefined)).toBe(''); expect(getstatelabelFilter(undefined)).toBe('label-default');
})); }));
it('should return label-important when a ghost state is detected', inject(function (getstatelabelFilter) { it('should return label-important when a ghost state is detected', inject(function (getstatelabelFilter) {