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:
Joseph Schorr 2013-10-18 22:28:46 -04:00
parent 8274d9af97
commit 0afea3a779
7 changed files with 239 additions and 7 deletions

View file

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

View file

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

View file

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

View file

@ -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}).

View file

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

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

View file

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