From 049148cb87074ddf0a291810160483075ac9b2d8 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Fri, 20 Mar 2015 17:46:02 -0400 Subject: [PATCH] Work in progress: new image view --- endpoints/api/image.py | 37 ++++--- endpoints/api/tag.py | 2 +- static/css/directives/ui/image-view-layer.css | 76 ++++++++++++++ static/css/pages/image-view.css | 16 +++ static/directives/image-view-layer.html | 15 +++ static/js/directives/ui/image-view-layer.js | 48 +++++++++ static/js/pages/image-view.js | 42 +++++++- static/partials/image-view.html | 99 +++++-------------- static/partials/old-image-view.html | 82 +++++++++++++++ 9 files changed, 325 insertions(+), 92 deletions(-) create mode 100644 static/css/directives/ui/image-view-layer.css create mode 100644 static/css/pages/image-view.css create mode 100644 static/directives/image-view-layer.html create mode 100644 static/js/directives/ui/image-view-layer.js create mode 100644 static/partials/old-image-view.html diff --git a/endpoints/api/image.py b/endpoints/api/image.py index 71171d572..939a87d98 100644 --- a/endpoints/api/image.py +++ b/endpoints/api/image.py @@ -9,7 +9,7 @@ from data import model from util.cache import cache_control_flask_restful -def image_view(image, image_map): +def image_view(image, image_map, include_locations=True, include_ancestors=True): extended_props = image if image.storage and image.storage.id: extended_props = image.storage @@ -20,24 +20,35 @@ def image_view(image, image_map): if not aid or not aid in image_map: return '' - return image_map[aid] + return image_map[aid].docker_image_id - # Calculate the ancestors string, with the DBID's replaced with the docker IDs. - ancestors = [docker_id(a) for a in image.ancestors.split('/')] - ancestors_string = '/'.join(ancestors) - - return { + image_data = { 'id': image.docker_image_id, 'created': format_date(extended_props.created), 'comment': extended_props.comment, 'command': json.loads(command) if command else None, 'size': extended_props.image_size, - 'locations': list(image.storage.locations), 'uploading': image.storage.uploading, - 'ancestors': ancestors_string, - 'sort_index': len(image.ancestors) + 'sort_index': len(image.ancestors), } + if include_locations: + image_data['locations'] = list(image.storage.locations) + + if include_ancestors: + # Calculate the ancestors string, with the DBID's replaced with the docker IDs. + ancestors = [docker_id(a) for a in image.ancestors.split('/')] + image_data['ancestors'] = '/'.join(ancestors) + + return image_data + + +def historical_image_view(image, image_map): + ancestors = [image_map[a] for a in image.ancestors.split('/')[1:-1]] + normal_view = image_view(image, image_map) + normal_view['history'] = [image_view(parent, image_map, False, False) for parent in ancestors] + return normal_view + @resource('/v1/repository//image/') @path_param('repository', 'The full path of the repository. e.g. namespace/name') @@ -62,7 +73,7 @@ class RepositoryImageList(RepositoryParamResource): filtered_images = [] for image in all_images: if str(image.id) in found_image_ids: - image_map[str(image.id)] = image.docker_image_id + image_map[str(image.id)] = image filtered_images.append(image) def add_tags(image_json): @@ -90,9 +101,9 @@ class RepositoryImage(RepositoryParamResource): # Lookup all the ancestor images for the image. image_map = {} for current_image in model.get_parent_images(namespace, repository, image): - image_map[str(current_image.id)] = image.docker_image_id + image_map[str(current_image.id)] = current_image - return image_view(image, image_map) + return historical_image_view(image, image_map) @resource('/v1/repository//image//changes') diff --git a/endpoints/api/tag.py b/endpoints/api/tag.py index 21972fc19..d5d7c94df 100644 --- a/endpoints/api/tag.py +++ b/endpoints/api/tag.py @@ -92,7 +92,7 @@ class RepositoryTagImages(RepositoryParamResource): parent_images = model.get_parent_images(namespace, repository, tag_image) image_map = {} for image in parent_images: - image_map[str(image.id)] = image.docker_image_id + image_map[str(image.id)] = image parents = list(parent_images) parents.reverse() diff --git a/static/css/directives/ui/image-view-layer.css b/static/css/directives/ui/image-view-layer.css new file mode 100644 index 000000000..f0fcd0c82 --- /dev/null +++ b/static/css/directives/ui/image-view-layer.css @@ -0,0 +1,76 @@ +.image-view-layer-element { + position: relative; + padding: 10px; + padding-left: 170px; +} + +.image-view-layer-element .image-id { + font-family: monospace; + position: absolute; + top: 10px; + left: 10px; + width: 110px; + text-align: right; +} + +.image-view-layer-element .image-id a { + color: #aaa; +} + +.image-view-layer-element.first .image-id { + font-weight: bold; + font-size: 110%; +} + +.image-view-layer-element.first .image-id a { + color: black; +} + +.image-view-layer-element .nondocker-command { + font-family: monospace; + padding: 2px; +} + +.image-view-layer-element .nondocker-command:before { + content: "\f120"; + font-family: "FontAwesome"; + font-size: 16px; + margin-right: 6px; + color: #999; +} + +.image-view-layer-element .image-layer-line { + position: absolute; + top: 0px; + bottom: 0px; + left: 140px; + + border-left: 2px solid #428bca; + width: 0px; +} + +.image-view-layer-element.first .image-layer-line { + top: 20px; +} + +.image-view-layer-element.last .image-layer-line { + bottom: 20px; +} + +.image-view-layer-element .image-layer-dot { + position: absolute; + top: 14px; + left: 135px; + border: 2px solid #428bca; + border-radius: 50%; + width: 12px; + height: 12px; + background: white; + + z-index: 2; +} + + +.image-view-layer-element.first .image-layer-dot { + background: #428bca; +} diff --git a/static/css/pages/image-view.css b/static/css/pages/image-view.css new file mode 100644 index 000000000..de8d4f07b --- /dev/null +++ b/static/css/pages/image-view.css @@ -0,0 +1,16 @@ +.image-view .image-view-header { + padding: 10px; + background: #e8f1f6; + margin: -10px; + margin-bottom: 20px; +} + +.image-view .image-view-header .section-icon { + margin-right: 6px; +} + +.image-view .image-view-header .section { + padding: 4px; + display: inline-block; + margin-right: 20px; +} \ No newline at end of file diff --git a/static/directives/image-view-layer.html b/static/directives/image-view-layer.html new file mode 100644 index 000000000..289a2acd6 --- /dev/null +++ b/static/directives/image-view-layer.html @@ -0,0 +1,15 @@ +
+ +
{{ image.comment }}
+
+
{{ image.command.join(' ') }}
+
+
+
+
+
\ No newline at end of file diff --git a/static/js/directives/ui/image-view-layer.js b/static/js/directives/ui/image-view-layer.js new file mode 100644 index 000000000..fa0c346cb --- /dev/null +++ b/static/js/directives/ui/image-view-layer.js @@ -0,0 +1,48 @@ +/** + * An element which displays a single layer representing an image in the image view. + */ +angular.module('quay').directive('imageViewLayer', function () { + var directiveDefinitionObject = { + priority: 0, + templateUrl: '/static/directives/image-view-layer.html', + replace: false, + transclude: true, + restrict: 'C', + scope: { + 'repository': '=repository', + 'image': '=image', + 'images': '=images' + }, + controller: function($scope, $element) { + $scope.getDockerfileCommand = function(command) { + if (!command) { return ''; } + + // ["/bin/sh", "-c", "#(nop) RUN foo"] + var commandPrefix = '#(nop)' + + if (command.length != 3) { return ''; } + if (command[0] != '/bin/sh' || command[1] != '-c') { return ''; } + + var cmd = command[2]; + + if (cmd.substring(0, commandPrefix.length) != commandPrefix) { return ''; } + + return command[2].substr(commandPrefix.length + 1); + }; + + $scope.getClass = function() { + var index = $.inArray($scope.image, $scope.images); + if (index < 0) { + return 'first'; + } + + if (index == $scope.images.length - 1) { + return 'last'; + } + + return ''; + }; + } + }; + return directiveDefinitionObject; +}); \ No newline at end of file diff --git a/static/js/pages/image-view.js b/static/js/pages/image-view.js index a8f04deda..757c030cc 100644 --- a/static/js/pages/image-view.js +++ b/static/js/pages/image-view.js @@ -3,10 +3,48 @@ * Page to view the details of a single image. */ angular.module('quayPages').config(['pages', function(pages) { - pages.create('image-view', 'image-view.html', ImageViewCtrl); + pages.create('image-view', 'image-view.html', ImageViewCtrl, { + 'newLayout': true, + 'title': '{{ image.id }}', + 'description': 'Image {{ image.id }}' + }, ['layout']) + + pages.create('image-view', 'old-image-view.html', OldImageViewCtrl, { + }, ['old-layout']); }]); - function ImageViewCtrl($scope, $routeParams, $rootScope, $timeout, ApiService, ImageMetadataService) { + function ImageViewCtrl($scope, $routeParams, $rootScope, ApiService, ImageMetadataService) { + var namespace = $routeParams.namespace; + var name = $routeParams.name; + var imageid = $routeParams.image; + + var loadImage = function() { + var params = { + 'repository': namespace + '/' + name, + 'image_id': imageid + }; + + $scope.imageResource = ApiService.getImageAsResource(params).get(function(image) { + $scope.image = image; + $scope.reversedHistory = image.history.reverse(); + }); + }; + + var loadRepository = function() { + var params = { + 'repository': namespace + '/' + name + }; + + $scope.repositoryResource = ApiService.getRepoAsResource(params).get(function(repo) { + $scope.repository = repo; + }); + }; + + loadImage(); + loadRepository(); + } + + function OldImageViewCtrl($scope, $routeParams, $rootScope, $timeout, ApiService, ImageMetadataService) { var namespace = $routeParams.namespace; var name = $routeParams.name; var imageid = $routeParams.image; diff --git a/static/partials/image-view.html b/static/partials/image-view.html index a4b8eaabb..4de310026 100644 --- a/static/partials/image-view.html +++ b/static/partials/image-view.html @@ -1,82 +1,29 @@ -
-
-
- -

- - -

+
+
+
+ + + + {{ repository.namespace }}/{{ repository.name }} + + + + + {{ image.id.substr(0, 12) }} +
- -
- -
- - -
-
Full Image ID
-
-
-
-
Created
-
-
Compressed Image Size
-
{{ image.value.size | bytes }} -
- -
Command
-
-
{{ getFormattedCommand(image.value) }}
-
-
- - -
- File Changes: -
-
- -
- - -
- -
-
-
-
- Showing {{(combinedChanges | filter:search | limitTo:50).length}} of {{(combinedChanges | filter:search).length}} results -
-
- -
-
-
-
-
- No matching changes -
-
- - - - {{folder}}/{{getFilename(change.file)}} - -
-
-
+
+
+
{{ image.id }}
+
{{ image.created | amDateFormat:'dddd, MMMM Do YYYY, h:mm:ss a' }}
- -
-
-
+
+
-
+
\ No newline at end of file diff --git a/static/partials/old-image-view.html b/static/partials/old-image-view.html new file mode 100644 index 000000000..a4b8eaabb --- /dev/null +++ b/static/partials/old-image-view.html @@ -0,0 +1,82 @@ +
+
+
+ +

+ + +

+
+ + +
+ +
+ + +
+
Full Image ID
+
+
+
+
Created
+
+
Compressed Image Size
+
{{ image.value.size | bytes }} +
+ +
Command
+
+
{{ getFormattedCommand(image.value) }}
+
+
+ + +
+ File Changes: +
+
+ +
+ + +
+ +
+
+
+
+ Showing {{(combinedChanges | filter:search | limitTo:50).length}} of {{(combinedChanges | filter:search).length}} results +
+
+ +
+
+
+
+
+ No matching changes +
+
+ + + + {{folder}}/{{getFilename(change.file)}} + +
+
+
+
+ + +
+
+
+
+
+