Add the image view page with the changes view, filterable by typing into a field. Still needs pagination or some other mechanism for getting an overview
This commit is contained in:
parent
8274d9af97
commit
0afea3a779
7 changed files with 239 additions and 7 deletions
|
@ -254,6 +254,18 @@ def get_repository(namespace_name, repository_name):
|
|||
return None
|
||||
|
||||
|
||||
def get_repo_image(namespace_name, repository_name, image_id):
|
||||
joined = Image.select().join(Repository)
|
||||
query = joined.where(Repository.name == repository_name,
|
||||
Repository.namespace == namespace_name,
|
||||
Image.docker_image_id == image_id).limit(1)
|
||||
result = list(query)
|
||||
if not result:
|
||||
return None
|
||||
|
||||
return result[0]
|
||||
|
||||
|
||||
def repository_is_public(namespace_name, repository_name):
|
||||
joined = Repository.select().join(Visibility)
|
||||
query = joined.where(Repository.namespace == namespace_name,
|
||||
|
|
|
@ -364,10 +364,24 @@ def list_repository_images(namespace, repository):
|
|||
abort(403)
|
||||
|
||||
|
||||
@app.route('/api/repository/<path:repository>/image/<image_id>',
|
||||
methods=['GET'])
|
||||
@parse_repository_name
|
||||
def get_image(namespace, repository, image_id):
|
||||
permission = ReadRepositoryPermission(namespace, repository)
|
||||
if permission.can() or model.repository_is_public(namespace, repository):
|
||||
image = model.get_repo_image(namespace, repository, image_id)
|
||||
if not image:
|
||||
abort(404)
|
||||
|
||||
return jsonify(image_view(image))
|
||||
abort(403)
|
||||
|
||||
|
||||
@app.route('/api/repository/<path:repository>/image/<image_id>/changes',
|
||||
methods=['GET'])
|
||||
@parse_repository_name
|
||||
def get_repository_changes(namespace, repository, image_id):
|
||||
def get_image_changes(namespace, repository, image_id):
|
||||
permission = ReadRepositoryPermission(namespace, repository)
|
||||
if permission.can() or model.repository_is_public(namespace, repository):
|
||||
diffs_path = store.image_file_diffs_path(namespace, repository, image_id)
|
||||
|
|
|
@ -489,6 +489,8 @@ p.editable:hover i {
|
|||
.repo dl.dl-normal dd {
|
||||
padding-left: 14px;
|
||||
margin-bottom: 10px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.repo .header h3 {
|
||||
|
@ -534,6 +536,46 @@ p.editable:hover i {
|
|||
top: 40px;
|
||||
}
|
||||
|
||||
.repo-image-view .id-container {
|
||||
display: inline-block;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.repo-image-view .id-container input {
|
||||
background: #fefefe;
|
||||
}
|
||||
|
||||
.repo-image-view .id-container .input-group {
|
||||
width: 542px;
|
||||
}
|
||||
|
||||
.repo-image-view #clipboardCopied {
|
||||
position: relative;
|
||||
top: -14px;
|
||||
}
|
||||
|
||||
.repo-image-view .changes-container .change-side-controls {
|
||||
float: right;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.repo-image-view .changes-container .filter-input {
|
||||
display: inline-block;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.repo-image-view .changes-container .result-count {
|
||||
display: inline-block;
|
||||
margin-right: 10px;
|
||||
font-size: 14px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.repo-image-view .changes-container .changes-list {
|
||||
padding: 10px;
|
||||
margin-top: 28px;
|
||||
}
|
||||
|
||||
.repo #clipboardCopied {
|
||||
font-size: 0.8em;
|
||||
display: inline-block;
|
||||
|
@ -665,7 +707,7 @@ p.editable:hover i {
|
|||
margin: 0px;
|
||||
}
|
||||
|
||||
.repo .changes-container:before {
|
||||
.repo .small-changes-container:before {
|
||||
content: "File Changes: ";
|
||||
display: inline-block;
|
||||
margin-right: 10px;
|
||||
|
@ -684,15 +726,15 @@ p.editable:hover i {
|
|||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.repo .changes-container .added i {
|
||||
.repo .changes-container i.icon-plus-sign-alt {
|
||||
color: rgb(73, 209, 73);
|
||||
}
|
||||
|
||||
.repo .change-container .removed i {
|
||||
.repo .changes-container i.icon-minus-sign-alt {
|
||||
color: rgb(209, 73, 73);
|
||||
}
|
||||
|
||||
.repo .changes-container .changed i {
|
||||
.repo .changes-container i.icon-edit-sign {
|
||||
color: rgb(73, 100, 209);
|
||||
}
|
||||
|
||||
|
|
|
@ -148,6 +148,7 @@ quayApp = angular.module('quay', ['restangular', 'angularMoment', 'angulartics',
|
|||
$routeProvider.
|
||||
when('/repository/:namespace/:name', {templateUrl: '/static/partials/view-repo.html', controller: RepoCtrl, reloadOnSearch: false}).
|
||||
when('/repository/:namespace/:name/tag/:tag', {templateUrl: '/static/partials/view-repo.html', controller: RepoCtrl}).
|
||||
when('/repository/:namespace/:name/image/:image', {templateUrl: '/static/partials/image-view.html', controller: ImageViewCtrl}).
|
||||
when('/repository/:namespace/:name/admin', {templateUrl: '/static/partials/repo-admin.html', controller:RepoAdminCtrl}).
|
||||
when('/repository/', {title: 'Repositories', templateUrl: '/static/partials/repo-list.html', controller: RepoListCtrl}).
|
||||
when('/user/', {title: 'User Admin', templateUrl: '/static/partials/user-admin.html', controller: UserAdminCtrl}).
|
||||
|
|
|
@ -782,6 +782,96 @@ function UserAdminCtrl($scope, $timeout, Restangular, PlanService, UserService,
|
|||
};
|
||||
}
|
||||
|
||||
function ImageViewCtrl($scope, $routeParams, $rootScope, Restangular) {
|
||||
var namespace = $routeParams.namespace;
|
||||
var name = $routeParams.name;
|
||||
var imageid = $routeParams.image;
|
||||
|
||||
$('#copyClipboard').clipboardCopy();
|
||||
|
||||
$scope.getMarkedDown = function(string) {
|
||||
if (!string) { return ''; }
|
||||
return getMarkedDown(string);
|
||||
};
|
||||
|
||||
$scope.parseDate = function(dateString) {
|
||||
return Date.parse(dateString);
|
||||
};
|
||||
|
||||
$scope.getFolder = function(filepath) {
|
||||
var index = filepath.lastIndexOf('/');
|
||||
if (index < 0) {
|
||||
return '';
|
||||
}
|
||||
return filepath.substr(0, index + 1);
|
||||
};
|
||||
|
||||
$scope.getFolders = function(filepath) {
|
||||
var index = filepath.lastIndexOf('/');
|
||||
if (index < 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return filepath.substr(0, index).split('/');
|
||||
};
|
||||
|
||||
$scope.getFilename = function(filepath) {
|
||||
var index = filepath.lastIndexOf('/');
|
||||
if (index < 0) {
|
||||
return filepath;
|
||||
}
|
||||
return filepath.substr(index + 1);
|
||||
};
|
||||
|
||||
$scope.setFolderFilter = function(folderPath, index) {
|
||||
var parts = folderPath.split('/');
|
||||
parts = parts.slice(0, index + 1);
|
||||
$scope.setFilter(parts.join('/'));
|
||||
};
|
||||
|
||||
$scope.setFilter = function(filter) {
|
||||
$scope.search = {};
|
||||
$scope.search['$'] = filter;
|
||||
document.getElementById('change-filter').value = filter;
|
||||
};
|
||||
|
||||
// Fetch the image.
|
||||
var imageFetch = Restangular.one('repository/' + namespace + '/' + name + '/image/' + imageid);
|
||||
imageFetch.get().then(function(image) {
|
||||
$scope.loading = false;
|
||||
$scope.repo = {
|
||||
'name': name,
|
||||
'namespace': namespace
|
||||
};
|
||||
$scope.image = image;
|
||||
$rootScope.title = 'View Image - ' + image.id;
|
||||
}, function() {
|
||||
$rootScope.title = 'Unknown Image';
|
||||
$scope.loading = false;
|
||||
});
|
||||
|
||||
// Fetch the image changes.
|
||||
var changesFetch = Restangular.one('repository/' + namespace + '/' + name + '/image/' + imageid + '/changes');
|
||||
changesFetch.get().then(function(changes) {
|
||||
var combinedChanges = [];
|
||||
var addCombinedChanges = function(c, kind) {
|
||||
for (var i = 0; i < c.length; ++i) {
|
||||
combinedChanges.push({
|
||||
'kind': kind,
|
||||
'file': c[i]
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
addCombinedChanges(changes.added, 'added');
|
||||
addCombinedChanges(changes.removed, 'removed');
|
||||
addCombinedChanges(changes.changed, 'changed');
|
||||
|
||||
$scope.combinedChanges = combinedChanges;
|
||||
$scope.imageChanges = changes;
|
||||
});
|
||||
}
|
||||
|
||||
function V1Ctrl($scope, UserService) {
|
||||
$scope.$watch( function () { return UserService.currentUser(); }, function (currentUser) {
|
||||
$scope.user = currentUser;
|
||||
|
|
73
static/partials/image-view.html
Normal file
73
static/partials/image-view.html
Normal file
|
@ -0,0 +1,73 @@
|
|||
<div class="container" ng-show="!loading && !image">
|
||||
No image found
|
||||
</div>
|
||||
|
||||
<div class="loading" ng-show="loading">
|
||||
<i class="icon-spinner icon-spin icon-3x"></i>
|
||||
</div>
|
||||
|
||||
<div class="container repo repo-image-view" ng-show="!loading && image">
|
||||
<div class="header">
|
||||
<a href="{{ '/repository/' + repo.namespace + '/' + repo.name }}" class="back"><i class="icon-chevron-left"></i></a>
|
||||
<h3>
|
||||
<i class="icon-archive icon-large" style="color: #aaa; margin-right: 10px;"></i>
|
||||
<span style="color: #aaa;"> {{repo.namespace}}</span>
|
||||
<span style="color: #ccc">/</span>
|
||||
<span style="color: #666;">{{repo.name}}</span>
|
||||
<span style="color: #ccc">/</span>
|
||||
<span>{{image.id.substr(0, 12)}}</span>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<!-- Information -->
|
||||
<dl class="dl-normal">
|
||||
<dt>Full Image ID</dt>
|
||||
<dd>
|
||||
<div>
|
||||
<div class="id-container">
|
||||
<div class="input-group">
|
||||
<input id="full-id" type="text" class="form-control" value="{{ image.id }}" readonly>
|
||||
<span id="copyClipboard" class="input-group-addon" title="Copy to Clipboard" data-clipboard-target="full-id">
|
||||
<i class="icon-copy"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="clipboardCopied" style="display: none">
|
||||
Copied to clipboard
|
||||
</div>
|
||||
</div>
|
||||
</dd>
|
||||
<dt>Created</dt>
|
||||
<dd am-time-ago="parseDate(image.created)"></dd>
|
||||
</dl>
|
||||
|
||||
<!-- Comment -->
|
||||
<blockquote ng-show="image.comment" ng-bind-html-unsafe="getMarkedDown(image.comment)"></blockquote>
|
||||
|
||||
<!-- Changes -->
|
||||
<div class="changes-container full-changes-container" ng-show="combinedChanges.length > 0">
|
||||
<b>File Changes:</b>
|
||||
<div class="change-side-controls">
|
||||
<div class="result-count">
|
||||
Showing {{(combinedChanges | filter:search | limitTo:50).length}} of {{(combinedChanges | filter:search).length}} results
|
||||
</div>
|
||||
<div class="filter-input">
|
||||
<input id="change-filter" class="form-control" placeholder="Filter Changes" type="text" ng-model="search.$">
|
||||
</div>
|
||||
</div>
|
||||
<div class="changes-list well well-sm">
|
||||
<div ng-show="(combinedChanges | filter:search | limitTo:1).length == 0">
|
||||
No matching changes
|
||||
</div>
|
||||
<div class="change" ng-repeat="change in combinedChanges | filter:search | limitTo:50">
|
||||
<i ng-class="{'added': 'icon-plus-sign-alt', 'removed': 'icon-minus-sign-alt', 'changed': 'icon-edit-sign'}[change.kind]"></i>
|
||||
<span title="{{change.file}}">
|
||||
<span style="color: #888;">
|
||||
<span ng-repeat="folder in getFolders(change.file)"><a href="javascript:void(0)" ng-click="setFolderFilter(getFolder(change.file), $index)">{{folder}}</a>/</span></span><span>{{getFilename(change.file)}}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
|
@ -124,7 +124,7 @@
|
|||
<i class="icon-spinner icon-spin icon-3x"></i>
|
||||
</div>
|
||||
|
||||
<div class="changes-container" ng-show="currentImageChanges.changed">
|
||||
<div class="changes-container small-changes-container" ng-show="currentImageChanges.changed">
|
||||
<div class="changes-count-container accordion-toggle" data-toggle="collapse" data-parent="#accordion" data-target="#collapseChanges">
|
||||
<span class="change-count added" ng-show="currentImageChanges.added.length > 0" title="Files Added">
|
||||
<i class="icon-plus-sign-alt"></i>
|
||||
|
@ -156,7 +156,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="more-changes" ng-show="getMoreCount(currentImageChanges) > 0">
|
||||
<a href="">And {{getMoreCount(currentImageChanges)}} more...</a>
|
||||
<a href="{{'/repository/' + repo.namespace + '/' + repo.name + '/image/' + currentImage.id}}">And {{getMoreCount(currentImageChanges)}} more...</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
Reference in a new issue