Skip to content

Commit

Permalink
Add image details support with API v2 (#84)
Browse files Browse the repository at this point in the history
Revert changes on default projet file

Add detailed information for tag listing

Removed useless comments, divs, etc -> refactoring

Prevent bugs from future changes of API

Add digest attribute to Manifest query response

Add basic pagination support to tag listing

Add environment variable for tags per page

Fix bug of image history without config + disabled parent id

Fix tags pagination system - Fetch infos for all pages

add missing comma

 Fetch infos for all pages -> simpler fix

Disable apache directory listing feature

Update tag pagination system to make it feel more like repository pagination system
  • Loading branch information
Arthur De Kimpe authored and kwk committed Apr 11, 2016
1 parent f287a38 commit d5dd87d
Show file tree
Hide file tree
Showing 15 changed files with 267 additions and 63 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,12 @@ By default 20 repositories will be listed per page. To adjust this number, to
let's say 50 pass `-e ENV_DEFAULT_REPOSITORIES_PER_PAGE=50` to your `docker run`
command.

# Default tags per page

By default 10 tags will be listed per page. To adjust this number, to
let's say 5 pass `-e ENV_DEFAULT_TAGS_PER_PAGE=5` to your `docker run`
command. Note that providing a big number will result in a heavy load on browsers.

# Contributions are welcome!

If you like the application, I invite you to contribute and report bugs or feature request on the project's github page: [https://github.com/kwk/docker-registry-frontend][3].
Expand Down
1 change: 1 addition & 0 deletions apache-site.conf
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

# See https://github.com/angular-ui/ui-router/wiki/Frequently-Asked-Questions#how-to-configure-your-server-to-work-with-html5mode
<Directory /var/www/html>
Options -Indexes
RewriteEngine on

# Don't rewrite files or directories
Expand Down
2 changes: 1 addition & 1 deletion app/app-mode.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"browseOnly": true, "defaultRepositoriesPerPage": 20}
{"browseOnly": true, "defaultRepositoriesPerPage": 20 , "defaultTagsPerPage":10}
6 changes: 5 additions & 1 deletion app/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,14 +64,18 @@ angular
templateUrl: 'repository/repository-detail.html',
controller: 'RepositoryDetailController',
}).
when('/repository/:repositoryUser/:repositoryName/:tagsPerPage?/:tagPage?', {
templateUrl: 'repository/repository-detail.html',
controller: 'RepositoryDetailController'
}).
when('/repository/:repositoryUser/:repositoryName/tags/:searchName?', {
templateUrl: 'repository/repository-detail.html',
controller: 'RepositoryController',
}).
when('/about', {
templateUrl: 'about.html',
}).
when('/tag/:repositoryUser/:repositoryName/:tagName/:imageId', {
when('/tag/:repositoryUser/:repositoryName/:tagName/', {
templateUrl: 'tag/tag-detail.html',
controller: 'TagController',
}).
Expand Down
35 changes: 24 additions & 11 deletions app/image/image-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,26 +8,39 @@
* Controller of the docker-registry-frontend
*/
angular.module('image-controller', ['registry-services', 'app-mode-services'])
.controller('ImageController', ['$scope', '$route', '$routeParams', '$location', '$log', '$filter', 'Image', 'Ancestry', 'AppMode',
function($scope, $route, $routeParams, $location, $log, $filter, Image, Ancestry, AppMode){
$scope.imageId = $route.current.params.imageId;
$scope.imageDetails = Image.query( {imageId: $scope.imageId} );
$scope.imageAncestry = Ancestry.query( {imageId: $scope.imageId} );
.controller('ImageController', ['$scope', '$route', '$routeParams', '$location', '$log', '$filter', 'Manifest', 'AppMode',
function($scope, $route, $routeParams, $location, $log, $filter, Manifest, AppMode){


$scope.appMode = AppMode.query();
$scope.totalImageSize = 0;
$scope.imageDetails = Manifest.query({repoUser: $scope.repositoryUser, repoName: $scope.repositoryName, tagName: $scope.tagName});




// This is not totally working right now (problem with big layers)
/**
* Calculates the total download size for the image based on
* it's ancestry.
* it's layers.
*/
/*
$scope.totalImageSize = null;
$scope.calculateTotalImageSize = function() {
$scope.totalImageSize = 0;
angular.forEach($scope.imageAncestry, function (id, key) {
/* We have to use the $promise object here to be sure the result is accessible */
Image.get( {imageId: id} ).$promise.then(function (result) {
if (!isNaN(result.Size-0)) {
$scope.totalImageSize += result.Size;
var size;
angular.forEach($scope.imageDetails.fsLayers, function (id, key) {
Blob.query({repoUser: $scope.repositoryUser, repoName: $scope.repositoryName, digest: id.blobSum}).$promise.then( function(data, headers){
size = data;
console.log(data)
console.log(size)
if(!isNaN(data.contentLength-0)){
$scope.totalImageSize += data.contentLength;
}
});
});
};
*/

}]);
51 changes: 29 additions & 22 deletions app/image/image-details-directive.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ <h2>
<tab-heading>
General information
</tab-heading>
<form class="form-horizontal" role="form">
<form class="form-horizontal" role="form">
<div class="form-group">
<label class="col-sm-2 control-label"><span class="glyphicon glyphicon-user"></span> Author</label>
<div class="col-sm-10">
Expand All @@ -28,7 +28,7 @@ <h2>
</p>
</div>
</div>
<div class="form-group">
<div class="form-group" >
<label class="col-sm-2 control-label"><span class="glyphicon glyphicon-calendar"></span> Created</label>
<div class="col-sm-10">
<p class="form-control-static">
Expand All @@ -52,22 +52,21 @@ <h2>
<div class="form-group">
<label class="col-sm-2 control-label"><span class="glyphicon glyphicon-qrcode"></span> ID</label>
<div class="col-sm-10">
<p class="form-control-static"><a href="image/{{imageDetails.id}}">{{imageDetails.id | limitTo: 12}}</a></p>
<p class="form-control-static">
{{imageDetails.id | limitTo: 12}}
</p>
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label"><span class="glyphicon glyphicon-arrow-up"></span> Parent's ID</label>
<!-- <div class="form-group">
<label class="col-sm-2 control-label"><span class="glyphicon glyphicon-arrow-up"></span> Parent's layer ID</label>
<div class="col-sm-10">
<p class="form-control-static"><a href="image/{{imageDetails.parent}}">{{imageDetails.parent | limitTo: 12}}</a></p>
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label"><span class="glyphicon glyphicon-compressed"></span> Size <small>(not including base image sizes)</small></label>
<div class="col-sm-10">
<p class="form-control-static">{{imageDetails.Size/1024/1024 | number: 2}} <b>MB</b> {{imageDetails.Size / 1024 | number: 2}} <b>KB</b> {{imageDetails.Size}} <b>B</b></p>
<p class="form-control-static">
{{imageDetails.parentLayer | limitTo: 12}}
</p>
</div>
</div>
<div class="form-group">
</div> -->

<!-- <div class="form-group">
<label class="col-sm-2 control-label"><span class="glyphicon glyphicon-compressed"></span> Size <small>(including base image sizes)</small></label>
<div class="col-sm-10">
<p class="form-control-static">
Expand All @@ -79,19 +78,27 @@ <h2>
</button>
</p>
</div>
</div>
</div> -->
</form>
</tab>
<tab>
<tab-heading>
Image Ancestry
Labels
</tab-heading>
<div class="list-group">
<a ng-repeat="img in imageAncestry" href="image/{{img}}" class="list-group-item" ng-class="{active: imageDetails.id==img}">
<span class="glyphicon" ng-class="{'glyphicon-arrow-down': ($first&&!$last)||$middle}"></span>
{{img | limitTo:12}}
</a>
</div>
<table class="table">
<thead>
<tr>
<th>Key</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="(key, value) in imageDetails.labels">
<td>{{key}}</td>
<td>{{value}}</td>
</tr>
</tbody>
</table>
</tab>
</tabset>

2 changes: 1 addition & 1 deletion app/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ <h3 class="text-muted">Docker Registry Frontend</h3>
<script src="bower_components/moment/moment.js"></script>
<script src="bower_components/angular-moment/angular-moment.js"></script>
<script src="bower_components/angular-smart-table/dist/smart-table.min.js"></script>
<script src="bower_components/angular-filter/dist/angular-filter.js"></script>
<script src="bower_components/angular-filter/dist/angular-filter.min.js"></script>
<script src="bower_components/angular-bootstrap-checkbox/angular-bootstrap-checkbox.js"></script>
<!-- endbower -->
<!-- endbuild -->
Expand Down
15 changes: 15 additions & 0 deletions app/repository/repository-detail-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,22 @@ angular.module('repository-detail-controller', ['registry-services', 'app-mode-s
$scope.repository = $scope.repositoryUser + '/' + $scope.repositoryName;

$scope.appMode = AppMode.query();
$scope.maxTagsPage = undefined;

// Method used to disable next & previous links
$scope.getNextHref = function (){
if($scope.maxTagsPage > $scope.tagsCurrentPage){
var nextPageNumber = $scope.tagsCurrentPage + 1;
return '/repository/'+$scope.repository+'/'+ $scope.tagsPerPage +'/' +nextPageNumber;
}
return '#'
}
$scope.getFirstHref = function (){
if($scope.tagsCurrentPage > 1){
return '/repository/'+$scope.repository+'/' + $scope.tagsPerPage +'/1';
}
return '#'
}
// selected repos
$scope.selectedRepositories = [];

Expand Down
35 changes: 35 additions & 0 deletions app/repository/repository-detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,38 @@ <h1>
</h1>

<tag-list></tag-list>
<nav>
<ul class="pager">
<li class="previous" ng-class="{disabled: tagsCurrentPage <= 1}">
<a href="{{getFirstHref()}}" >
<span aria-hidden="true">&larr;</span>
First Page
</a>
</li>
<li>
<div class="btn-group" role="group">
<a href="#" class="btn btn-default dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
<span class="glyphicon glyphicon-book" aria-hidden="true"> </span> / page:
<span ng-show="tagsPerPage">{{tagsPerPage}}</span>
<span ng-show="!tagsPerPage">all</span>
<span class="caret"></span>
</a>
<ul class="dropdown-menu">
<li><a href="repository/{{repository}}">Show all</a></li>
<li role="separator" class="divider"></li>
<li><a href="repository/{{repository}}/10/1">10</a></li>
<li><a href="repository/{{repository}}/20/1">20</a></li>
<li><a href="repository/{{repository}}/40/1">40</a></li>
<li><a href="repository/{{repository}}/60/1">60</a></li>
<li><a href="repository/{{repository}}/80/1">80</a></li>
<li><a href="repository/{{repository}}/100/1">100</a></li>
</ul>
</div>
</li>
<li class="next" ng-class="{disabled: maxTagsPage <= tagsCurrentPage || !tagsPerPage}">
<a href="{{getNextHref()}}">
Next <span aria-hidden="true">&rarr;</span>
</a>
</li>
</ul>
</nav>
5 changes: 3 additions & 2 deletions app/repository/repository-list-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ angular.module('repository-list-controller', ['registry-services', 'app-mode-ser
$scope.repositoryName = $route.current.params.repositoryName;
$scope.repository = $scope.repositoryUser + '/' + $scope.repositoryName;

$scope.appMode = AppMode.query();

$scope.appMode = AppMode.query( function (result){
$scope.defaultTagsPerPage = result.defaultTagsPerPage
});
// How to query the repository
$scope.reposPerPage = $route.current.params.reposPerPage;
$scope.lastNamespace = $route.current.params.lastNamespace;
Expand Down
2 changes: 1 addition & 1 deletion app/repository/repository-list.html
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ <h1>Repositories</h1>
<!--<input type="checkbox" name="selectedRepos[]" value="{{repo.name}}" ng-model="repo.selected" ng-hide="appMode.browseOnly">-->
</td>
<td class="grow">
<a href="repository/{{repo.name}}"><!--<span class="glyphicon glyphicon-book"></span>--> {{repo.name|trim:username+'/'}}</a>
<a href="repository/{{repo.name}}/{{defaultTagsPerPage}}"><!--<span class="glyphicon glyphicon-book"></span>--> {{repo.name|trim:username+'/'}}</a>
</td>
</tr>
</tbody>
Expand Down
89 changes: 81 additions & 8 deletions app/services/registry-services.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,6 @@ angular.module('registry-services', ['ngResource'])
isArray: true,
transformResponse: function(data/*, headers*/){
var res = [];
console.log(data);
var resp = angular.fromJson(data);
for (var idx in resp.tags){
res.push({
Expand Down Expand Up @@ -142,13 +141,87 @@ angular.module('registry-services', ['ngResource'])
},
});
}])
.factory('Image', ['$resource', function($resource){
return $resource('/v1/images/:imageId/json', {}, {
'query': { method:'GET', isArray: false},
.factory('Manifest', ['$resource', function($resource){

return $resource('/v2/:repoUser/:repoName/manifests/:tagName', {}, {
// Response example:
// {
// "schemaVersion": 1,
// "name": "arthur/busybox",
// "tag": "demo",
// "architecture": "amd64",
// "fsLayers": [
// {
// "blobSum": "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"
// },
// {
// "blobSum": "sha256:d7e8ec85c5abc60edf74bd4b8d68049350127e4102a084f22060f7321eac3586"
// }
// ],
// "history": [
// {
// "v1Compatibility": "{\"id\":\"3e1018ee907f25aef8c50016296ab33624796511fdbfdbbdeca6a3ed2d0ba4e2\",\"parent\":\"176dfc9032a1ec3ac8586b383e325e1a65d1f5b5e6f46c2a55052b5aea8310f7\",\"created\":\"2016-01-12T17:47:39.251310827Z\",\"container\":\"2732d16efa11ab7da6393645e85a7f2070af94941a782a69e86457a2284f4a69\",\"container_config\":{\"Hostname\":\"ea7fe68f39fd\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\"],\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) LABEL awesome=Not yet!\"],\"Image\":\"176dfc9032a1ec3ac8586b383e325e1a65d1f5b5e6f46c2a55052b5aea8310f7\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"OnBuild\":[],\"Labels\":{\"awesome\":\"Not yet!\",\"test\":\"yes\",\"working\":\"true\"}},\"docker_version\":\"1.9.1\",\"author\":\"Arthur\",\"config\":{\"Hostname\":\"ea7fe68f39fd\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\"],\"Cmd\":[\"sh\"],\"Image\":\"176dfc9032a1ec3ac8586b383e325e1a65d1f5b5e6f46c2a55052b5aea8310f7\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"OnBuild\":[],\"Labels\":{\"awesome\":\"Not yet!\",\"test\":\"yes\",\"working\":\"true\"}},\"architecture\":\"amd64\",\"os\":\"linux\"}"
// },
// {
// "v1Compatibility": "{\"id\":\"5c5fb281b01ee091a0fffa5b4a4c7fb7d358e7fb7c49c263d6d7a4e35d199fd0\",\"created\":\"2015-12-08T18:31:50.979824705Z\",\"container\":\"ea7fe68f39fd0df314e841247fb940ddef4c02ab7b5edb0ee724adc3174bc8d9\",\"container_config\":{\"Hostname\":\"ea7fe68f39fd\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) ADD file:c295b0748bf05d4527f500b62ff269bfd0037f7515f1375d2ee474b830bad382 in /\"],\"Image\":\"\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"OnBuild\":null,\"Labels\":null},\"docker_version\":\"1.8.3\",\"config\":{\"Hostname\":\"ea7fe68f39fd\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":null,\"Image\":\"\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"OnBuild\":null,\"Labels\":null},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":1113436}"
// }
// ],
// }
'query': {
method:'GET',
isArray: false,
transformResponse: function(data, headers){
var res = {};
var history = [];
var tmp;
var resp = angular.fromJson(data);
var v1Compatibility = undefined;

for (var idx in resp.history){

v1Compatibility = angular.fromJson(resp.history[idx].v1Compatibility);

if(v1Compatibility !== undefined){
tmp = {
id : v1Compatibility.id,
os : v1Compatibility.os,
docker_version: v1Compatibility.docker_version,
created: v1Compatibility.created,
// parentLayer: v1Compatibility.parent
};
if(v1Compatibility.author){
tmp.author = v1Compatibility.author;
}
if(v1Compatibility.config && v1Compatibility.config.Labels){
tmp.labels = v1Compatibility.config.Labels;
}
history.push(tmp);
}
}
if(history.length > 0){
res = history[0];
res.history = history;
}
res.fsLayers = resp.fsLayers;
res.digest = headers('docker-content-digest');
res.architecture = resp.architecture;
return res;
},
}
});
}])
.factory('Ancestry', ['$resource', function($resource){
return $resource('/v1/images/:imageId/ancestry', {}, {
'query': { method:'GET', isArray: true},
// This is not totally working right now (problem with big layers)
/*
.factory('Blob', ['$resource', function($resource){
return $resource('/v2/:repoUser/:repoName/blobs/:digest', {}, {
'query': {
method:'HEAD',
interceptor: function(data, headers){
var res = {contentLength: parseInt(headers('content-length'))};
return res;
}
}
});
}]);
}]) */ ;
Loading

0 comments on commit d5dd87d

Please sign in to comment.