Add image details support with API v2 (#84)

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
This commit is contained in:
Arthur De Kimpe 2016-04-11 09:46:40 +02:00 committed by Konrad Kleine
parent f287a3811b
commit d5dd87d1c9
15 changed files with 267 additions and 63 deletions

View file

@ -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].

View file

@ -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

View file

@ -1 +1 @@
{"browseOnly": true, "defaultRepositoriesPerPage": 20}
{"browseOnly": true, "defaultRepositoriesPerPage": 20 , "defaultTagsPerPage":10}

View file

@ -64,6 +64,10 @@ 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',
@ -71,7 +75,7 @@ angular
when('/about', {
templateUrl: 'about.html',
}).
when('/tag/:repositoryUser/:repositoryName/:tagName/:imageId', {
when('/tag/:repositoryUser/:repositoryName/:tagName/', {
templateUrl: 'tag/tag-detail.html',
controller: 'TagController',
}).

View file

@ -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;
}
});
});
};
*/
}]);

View file

@ -13,7 +13,7 @@
<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">
@ -28,7 +28,7 @@
</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">
@ -52,22 +52,21 @@
<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>
<p class="form-control-static">
{{imageDetails.parentLayer | limitTo: 12}}
</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>
</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">
@ -79,19 +78,27 @@
</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>

View file

@ -69,7 +69,7 @@
<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 -->

View file

@ -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 = [];

View file

@ -23,3 +23,38 @@
</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>

View file

@ -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;

View file

@ -38,7 +38,7 @@
<!--<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>

View file

@ -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({
@ -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;
}
}
});
}]);
}]) */ ;

View file

@ -8,8 +8,8 @@
* Controller of the docker-registry-frontend
*/
angular.module('tag-controller', ['registry-services'])
.controller('TagController', ['$scope', '$route', '$routeParams', '$location', '$log', '$filter', 'Tag', 'filterFilter', '$modal',
function($scope, $route, $routeParams, $location, $log, $filter, Tag, filterFilter, $modal){
.controller('TagController', ['$scope', '$route', '$routeParams', '$location', '$log', '$filter', 'Manifest', 'Tag', 'filterFilter', '$modal',
function($scope, $route, $routeParams, $location, $log, $filter, Manifest, Tag, filterFilter, $modal){
$scope.$route = $route;
$scope.$location = $location;
@ -20,16 +20,50 @@ angular.module('tag-controller', ['registry-services'])
$scope.repositoryName = $route.current.params.repositoryName;
$scope.repository = $scope.repositoryUser + '/' + $scope.repositoryName;
$scope.tagName = $route.current.params.tagName;
$scope.tagsPerPage = $route.current.params.tagsPerPage;
// How to query the tags
// Fetch tags
$scope.tags = Tag.query({
repoUser: $scope.repositoryUser,
repoName: $scope.repositoryName
}, function(result){
// Determine the number of pages
$scope.maxTagsPage = parseInt(Math.ceil(parseFloat(result.length)/parseFloat($scope.tagsPerPage)));
// Compute the right current page number
$scope.tagsCurrentPage = $route.current.params.tagPage;
if(! $scope.tagsCurrentPage){
$scope.tagsCurrentPage = 1;
}else{
$scope.tagsCurrentPage = parseInt($scope.tagsCurrentPage)
if($scope.tagsCurrentPage > $scope.maxTagsPage || $scope.tagsCurrentPage < 1){
$scope.tagsCurrentPage = 1;
}
}
// Select wanted tags
var idxShift = 0;
$scope.displayedTags = $scope.tags;
if($scope.tagsPerPage){
idxShift = ($scope.tagsCurrentPage - 1) * $scope.tagsPerPage;
$scope.displayedTags = $scope.displayedTags.slice(idxShift, ($scope.tagsCurrentPage ) * $scope.tagsPerPage );
}
var tmpIdx;
// Fetch wanted manifests
for (var idx in $scope.displayedTags){
if(!isNaN(idx)){
tmpIdx = parseInt(idx) + idxShift;
if ( result[tmpIdx].hasOwnProperty('name') ) {
result[tmpIdx].details = Manifest.query({repoUser: $scope.repositoryUser, repoName: $scope.repositoryName, tagName: result[tmpIdx].name});
}
}
}
});
// Copy collection for rendering in a smart-table
$scope.displayedTags = [].concat($scope.tags);
// selected tags
$scope.selection = [];
@ -38,13 +72,6 @@ angular.module('tag-controller', ['registry-services'])
return filterFilter($scope.displayedTags, { selected: true });
};
// watch fruits for changes
$scope.$watch('tags|filter:{selected:true}', function(nv) {
$scope.selection = nv.map(function (tag) {
return $scope.repository + ':' + tag.name;
});
}, true);
$scope.openConfirmTagDeletionDialog = function(size) {
var modalInstance = $modal.open({
animation: true,
@ -67,4 +94,4 @@ angular.module('tag-controller', ['registry-services'])
});
};
}]);
}]);

View file

@ -7,21 +7,42 @@
</button>
<div class="table-responsive">
<table class="table table-hover">
<table class="table table-hover">
<thead>
<tr>
<th class="" ng-class="{strike: deleted}"><span class="glyphicon glyphicon-tag"></span> Tag</th>
<th ng-class="{strike: deleted}"><span class="glyphicon glyphicon-tag"></span> Tag</th>
<th ><span class="glyphicon glyphicon-qrcode"></span> Image ID</th>
<th ><span class="glyphicon glyphicon-calendar"></span> Created</th>
<th ><span class="glyphicon glyphicon-user"></span> Author</th>
<th ><span class="glyphicon glyphicon-eye-open"></span> Docker version</th>
<!-- <th ><span class="glyphicon glyphicon-arrow-up"></span> Parent's ID</th> -->
<!-- <th><span class="glyphicon glyphicon-compressed"></span> Size (MB)</th> -->
</tr>
<tr>
<th><input class="input-sm form-control" placeholder="Filter tags on this page" type="search" ng-model="search.name"/></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="tag in tags | filter:search ">
<tr ng-repeat="tag in displayedTags | filter:search ">
<td>
<span ng-bind-html="tag.name"></span>
<a ng-bind-html="tag.name" href="tag/{{repositoryUser}}/{{repositoryName}}/{{tag.name}}">{{tag.name}}</a>
</td>
<td><span ng-bind-html="tag.details.id | limitTo: 12"></span></td>
<td am-time-ago="tag.details.created"></td>
<td><span ng-bind-html="tag.details.author | linky"></span></td>
<td ng-bind-html="tag.details.docker_version"></td>
<!-- <td><span ng-bind-html="tag.details.parent | limitTo: 12"></span></td> -->
<!-- <td>
<button type="submit" class="btn btn-info btn-xs" ng-click="calculateTotalImageSize()" ng-show="totalImageSize===null" title="Calculate total image size">
<span class="glyphicon glyphicon-stats"></span>
</button>
<span ng-show="totalImageSize===null" ng-bind-html="tag.details.Size / 1024 / 1024 | number: 2"></span>
<span ng-show="totalImageSize!==null">
{{totalImageSize / 1024 / 1024 | number: 2}}
</span>
</td> -->
</tr>
</tbody>
</table>
</div>

View file

@ -40,7 +40,8 @@ echo "{\"host\": \"$ENV_REGISTRY_PROXY_FQDN\", \"port\": $ENV_REGISTRY_PROXY_POR
# Overwrite browse-only option for now since only browse-only is working right now
ENV_MODE_BROWSE_ONLY=true
[[ -z "$ENV_DEFAULT_REPOSITORIES_PER_PAGE" ]] && ENV_DEFAULT_REPOSITORIES_PER_PAGE=20
echo "{\"browseOnly\":$ENV_MODE_BROWSE_ONLY, \"defaultRepositoriesPerPage\":$ENV_DEFAULT_REPOSITORIES_PER_PAGE}" > /var/www/html/app-mode.json
[[ -z "$ENV_DEFAULT_TAGS_PER_PAGE" ]] && ENV_DEFAULT_TAGS_PER_PAGE=10
echo "{\"browseOnly\":$ENV_MODE_BROWSE_ONLY, \"defaultRepositoriesPerPage\":$ENV_DEFAULT_REPOSITORIES_PER_PAGE , \"defaultTagsPerPage\":$ENV_DEFAULT_TAGS_PER_PAGE }" > /var/www/html/app-mode.json
if [ "$ENV_MODE_BROWSE_ONLY" == "true" ]; then
echo "export APACHE_ARGUMENTS='-D FRONTEND_BROWSE_ONLY_MODE'" >> /etc/apache2/envvars
fi