Work in progress: new image view

This commit is contained in:
Joseph Schorr 2015-03-20 17:46:02 -04:00
parent 3959ea2ff9
commit 049148cb87
9 changed files with 325 additions and 92 deletions

View file

@ -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/<repopath: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/<repopath:repository>/image/<image_id>/changes')

View file

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

View file

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

View file

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

View file

@ -0,0 +1,15 @@
<div class="image-view-layer-element" ng-class="getClass()">
<div class="image-id">
<a href="/repository/{{ repository.namespace }}/{{ repository.name }}/image/{{ image.id }}">
{{ image.id.substr(0, 12) }}
</a>
</div>
<div class="image-comment" ng-if="image.comment">{{ image.comment }}</div>
<div class="image-command">
<div class="nondocker-command" ng-if="!getDockerfileCommand(image.command) && image.command.length">{{ image.command.join(' ') }}</div>
<div class="dockerfile-command" command="getDockerfileCommand(image.command)"
ng-if="getDockerfileCommand(image.command)"></div>
</div>
<div class="image-layer-dot"></div>
<div class="image-layer-line"></div>
</div>

View file

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

View file

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

View file

@ -1,82 +1,29 @@
<div class="resource-view" resource="image" error-message="'No image found'">
<div class="cor-container repo repo-image-view">
<div class="header">
<a href="{{ '/repository/' + repo.namespace + '/' + repo.name }}" class="back"><i class="fa fa-chevron-left"></i></a>
<h3>
<span class="repo-circle no-background" repo="repo"></span>
<span class="repo-breadcrumb" repo="repo" image="image.value"></span>
</h3>
<div class="resource-view image-view"
resources="[repositoryResource, imageResource]"
error-message="'Image not found'">
<div class="page-content">
<div class="cor-title">
<span class="cor-title-link">
<a class="back-link" href="/repository/{{ repository.namespace }}/{{ repository.name }}?tab=tags">
<i class="fa fa-hdd-o" style="margin-right: 4px"></i>
{{ repository.namespace }}/{{ repository.name }}
</a>
</span>
<span class="cor-title-content">
<i class="fa fa-database fa-lg" style="margin-right: 10px"></i>
{{ image.id.substr(0, 12) }}
</span>
</div>
<!-- Comment -->
<blockquote ng-show="image.value.comment">
<span class="markdown-view" content="image.value.comment"></span>
</blockquote>
<!-- Information -->
<dl class="dl-normal">
<dt>Full Image ID</dt>
<dd>
<div class="copy-box" value="image.value.id"></div>
</dd>
<dt>Created</dt>
<dd am-time-ago="parseDate(image.value.created)"></dd>
<dt>Compressed Image Size</dt>
<dd><span class="context-tooltip"
data-title="The amount of data sent between Docker and the registry when pushing/pulling"
bs-tooltip="tooltip.title" data-container="body">{{ image.value.size | bytes }}</span>
</dd>
<dt ng-show="image.value.command && image.value.command.length">Command</dt>
<dd ng-show="image.value.command && image.value.command.length">
<pre class="formatted-command">{{ getFormattedCommand(image.value) }}</pre>
</dd>
</dl>
<!-- Changes tabs -->
<div ng-show="combinedChanges.length > 0">
<b>File Changes:</b>
<br>
<br>
<ul class="nav nav-tabs">
<li class="active"><a href="javascript:void(0)" data-toggle="tab" data-target="#filterable">Filterable View</a></li>
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#tree" ng-click="initializeTree()">Tree View</a></li>
</ul>
</div>
<!-- Changes tab content -->
<div class="tab-content" ng-show="combinedChanges.length > 0">
<!-- Filterable view -->
<div class="tab-pane active" id="filterable">
<div class="changes-container full-changes-container">
<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 style="height: 28px;"></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': 'fa fa-plus-square', 'removed': 'fa fa-minus-square', 'changed': 'fa fa-pencil-square'}[change.kind]"></i>
<span data-title="{{change.file}}">
<span style="color: #888;">
<span ng-repeat="folder in getFolders(change.file) track by $index"><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 class="co-main-content-panel">
<div class="image-view-header">
<div class="image-id section"><i class="fa fa-code section-icon" bs-tooltip="tooltip.title" data-title="Full Image ID"></i> {{ image.id }}</div>
<div class="created section"><i class="fa fa-calendar section-icon" bs-tooltip="tooltip.title" data-title="Full Image ID"></i> {{ image.created | amDateFormat:'dddd, MMMM Do YYYY, h:mm:ss a' }}</div>
</div>
<!-- Tree view -->
<div class="tab-pane" id="tree">
<div id="changes-tree-container" class="changes-container" onresize="tree && tree.notifyResized()"></div>
</div>
<div class="image-view-layer" repository="repository" image="image" images="image.history"></div>
<div class="image-view-layer" repository="repository" image="parent" images="image.history"
ng-repeat="parent in reversedHistory"></div>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,82 @@
<div class="resource-view" resource="image" error-message="'No image found'">
<div class="cor-container repo repo-image-view">
<div class="header">
<a href="{{ '/repository/' + repo.namespace + '/' + repo.name }}" class="back"><i class="fa fa-chevron-left"></i></a>
<h3>
<span class="repo-circle no-background" repo="repo"></span>
<span class="repo-breadcrumb" repo="repo" image="image.value"></span>
</h3>
</div>
<!-- Comment -->
<blockquote ng-show="image.value.comment">
<span class="markdown-view" content="image.value.comment"></span>
</blockquote>
<!-- Information -->
<dl class="dl-normal">
<dt>Full Image ID</dt>
<dd>
<div class="copy-box" value="image.value.id"></div>
</dd>
<dt>Created</dt>
<dd am-time-ago="parseDate(image.value.created)"></dd>
<dt>Compressed Image Size</dt>
<dd><span class="context-tooltip"
data-title="The amount of data sent between Docker and the registry when pushing/pulling"
bs-tooltip="tooltip.title" data-container="body">{{ image.value.size | bytes }}</span>
</dd>
<dt ng-show="image.value.command && image.value.command.length">Command</dt>
<dd ng-show="image.value.command && image.value.command.length">
<pre class="formatted-command">{{ getFormattedCommand(image.value) }}</pre>
</dd>
</dl>
<!-- Changes tabs -->
<div ng-show="combinedChanges.length > 0">
<b>File Changes:</b>
<br>
<br>
<ul class="nav nav-tabs">
<li class="active"><a href="javascript:void(0)" data-toggle="tab" data-target="#filterable">Filterable View</a></li>
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#tree" ng-click="initializeTree()">Tree View</a></li>
</ul>
</div>
<!-- Changes tab content -->
<div class="tab-content" ng-show="combinedChanges.length > 0">
<!-- Filterable view -->
<div class="tab-pane active" id="filterable">
<div class="changes-container full-changes-container">
<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 style="height: 28px;"></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': 'fa fa-plus-square', 'removed': 'fa fa-minus-square', 'changed': 'fa fa-pencil-square'}[change.kind]"></i>
<span data-title="{{change.file}}">
<span style="color: #888;">
<span ng-repeat="folder in getFolders(change.file) track by $index"><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>
<!-- Tree view -->
<div class="tab-pane" id="tree">
<div id="changes-tree-container" class="changes-container" onresize="tree && tree.notifyResized()"></div>
</div>
</div>
</div>
</div>