Work in progress: new image view
This commit is contained in:
parent
3959ea2ff9
commit
049148cb87
9 changed files with 325 additions and 92 deletions
|
@ -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')
|
||||
|
|
|
@ -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()
|
||||
|
|
76
static/css/directives/ui/image-view-layer.css
Normal file
76
static/css/directives/ui/image-view-layer.css
Normal 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;
|
||||
}
|
16
static/css/pages/image-view.css
Normal file
16
static/css/pages/image-view.css
Normal 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;
|
||||
}
|
15
static/directives/image-view-layer.html
Normal file
15
static/directives/image-view-layer.html
Normal 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>
|
48
static/js/directives/ui/image-view-layer.js
Normal file
48
static/js/directives/ui/image-view-layer.js
Normal 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;
|
||||
});
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
82
static/partials/old-image-view.html
Normal file
82
static/partials/old-image-view.html
Normal 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>
|
Reference in a new issue