diff --git a/data/model.py b/data/model.py index 5a1a3ff57..15827cca4 100644 --- a/data/model.py +++ b/data/model.py @@ -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, diff --git a/endpoints/api.py b/endpoints/api.py index ae76eac16..edb49aa0e 100644 --- a/endpoints/api.py +++ b/endpoints/api.py @@ -364,10 +364,24 @@ def list_repository_images(namespace, repository): abort(403) +@app.route('/api/repository//image/', + 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//image//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) diff --git a/static/css/quay.css b/static/css/quay.css index 44bd80a98..cb2578b52 100644 --- a/static/css/quay.css +++ b/static/css/quay.css @@ -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); } diff --git a/static/js/app.js b/static/js/app.js index e534c2a97..d05d5b384 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -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}). diff --git a/static/js/controllers.js b/static/js/controllers.js index 6274ee88c..251699988 100644 --- a/static/js/controllers.js +++ b/static/js/controllers.js @@ -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; diff --git a/static/partials/image-view.html b/static/partials/image-view.html new file mode 100644 index 000000000..333e6c08b --- /dev/null +++ b/static/partials/image-view.html @@ -0,0 +1,73 @@ +
+ No image found +
+ +
+ +
+ +
+
+ +

+ + {{repo.namespace}} + / + {{repo.name}} + / + {{image.id.substr(0, 12)}} +

+
+ + +
+
Full Image ID
+
+
+
+
+ + + + +
+
+ + +
+
+
Created
+
+
+ + +
+ + +
+ File Changes: +
+
+ Showing {{(combinedChanges | filter:search | limitTo:50).length}} of {{(combinedChanges | filter:search).length}} results +
+
+ +
+
+
+
+ No matching changes +
+
+ + + + {{folder}}/{{getFilename(change.file)}} + +
+
+
+ +
diff --git a/static/partials/view-repo.html b/static/partials/view-repo.html index bf94714ec..08d164549 100644 --- a/static/partials/view-repo.html +++ b/static/partials/view-repo.html @@ -124,7 +124,7 @@ -