diff --git a/data/database.py b/data/database.py
index b3bbfe66d..2ddd2fada 100644
--- a/data/database.py
+++ b/data/database.py
@@ -401,6 +401,7 @@ class ImageStorage(BaseModel):
command = TextField(null=True)
image_size = BigIntegerField(null=True)
uncompressed_size = BigIntegerField(null=True)
+ aggregate_size = BigIntegerField(null=True)
uploading = BooleanField(default=True, null=True)
diff --git a/data/model/legacy.py b/data/model/legacy.py
index 059cc308b..e24a2675e 100644
--- a/data/model/legacy.py
+++ b/data/model/legacy.py
@@ -1450,6 +1450,18 @@ def set_image_size(docker_image_id, namespace_name, repository_name, image_size,
image.storage.image_size = image_size
image.storage.uncompressed_size = uncompressed_size
+
+ ancestors = image.ancestors.split('/')[1:-1]
+ if ancestors:
+ try:
+ parent_image = Image.get(Image.id == ancestors[-1])
+ total_size = image_size + parent_image.storage.aggregate_size
+ image.storage.aggregate_size = total_size
+ except Image.DoesNotExist:
+ pass
+ else:
+ image.storage.aggregate_size = image_size
+
image.storage.save()
return image
@@ -2209,8 +2221,8 @@ def log_action(kind_name, user_or_organization_name, performer=None,
kind = LogEntryKind.get(LogEntryKind.name == kind_name)
account = User.get(User.username == user_or_organization_name)
LogEntry.create(kind=kind, account=account, performer=performer,
- repository=repository, ip=ip,
- metadata_json=json.dumps(metadata), datetime=timestamp)
+ repository=repository, ip=ip, metadata_json=json.dumps(metadata),
+ datetime=timestamp)
def create_build_trigger(repo, service_name, auth_token, user, pull_robot=None):
diff --git a/endpoints/api/repository.py b/endpoints/api/repository.py
index 3fe5301d3..49369e61f 100644
--- a/endpoints/api/repository.py
+++ b/endpoints/api/repository.py
@@ -1,5 +1,6 @@
import logging
import json
+import datetime
from flask import request
@@ -177,7 +178,9 @@ class Repository(RepositoryParamResource):
def tag_view(tag):
return {
'name': tag.name,
- 'image_id': tag.image.docker_image_id
+ 'image_id': tag.image.docker_image_id,
+ 'last_modified': format_date(datetime.datetime.fromtimestamp(tag.lifetime_start_ts)),
+ 'size': tag.image.storage.aggregate_size
}
organization = None
@@ -196,6 +199,9 @@ class Repository(RepositoryParamResource):
active_builds = model.list_repository_builds(namespace, repository, 1,
include_inactive=False)
+ is_starred = (model.repository_is_starred(get_authenticated_user(), repo)
+ if get_authenticated_user() else False)
+
return {
'namespace': namespace,
'name': repository,
@@ -206,6 +212,7 @@ class Repository(RepositoryParamResource):
'is_public': is_public,
'is_building': len(list(active_builds)) > 0,
'is_organization': bool(organization),
+ 'is_starred': is_starred,
'status_token': repo.badge_token if not is_public else ''
}
diff --git a/initdb.py b/initdb.py
index 4076eff09..f35b77111 100644
--- a/initdb.py
+++ b/initdb.py
@@ -75,7 +75,7 @@ def __create_subtree(repo, structure, creator_username, parent):
new_image.storage.checksum = checksum
new_image.storage.save()
- creation_time = REFERENCE_DATE + timedelta(days=image_num)
+ creation_time = REFERENCE_DATE + timedelta(weeks=image_num) + timedelta(days=i)
command_list = SAMPLE_CMDS[image_num % len(SAMPLE_CMDS)]
command = json.dumps(command_list) if command_list else None
new_image = model.set_image_metadata(docker_image_id, repo.namespace_user.username, repo.name,
@@ -364,6 +364,14 @@ def populate_database():
__generate_repository(new_user_1, 'simple', 'Simple repository.', False,
[], (4, [], ['latest', 'prod']))
+ __generate_repository(new_user_1, 'sharedtags',
+ 'Shared tags repository',
+ False, [(new_user_2, 'read'), (dtrobot[0], 'read')],
+ (2, [(3, [], ['v2.0', 'v2.1', 'v2.2']),
+ (1, [(1, [(1, [], ['prod', '581a284'])],
+ ['staging', '8423b58']),
+ (1, [], None)], None)], None))
+
__generate_repository(new_user_1, 'history', 'Historical repository.', False,
[], (4, [(2, [], 'latest'), (3, [], '#latest')], None))
diff --git a/static/css/core-ui.css b/static/css/core-ui.css
index 91c0b6607..6d029372a 100644
--- a/static/css/core-ui.css
+++ b/static/css/core-ui.css
@@ -754,3 +754,136 @@
vertical-align: middle;
font-family: FontAwesome;
}
+
+.co-table {
+ width: 100%;
+}
+
+.co-table td {
+ border-bottom: 1px solid #eee;
+ padding: 10px;
+}
+
+.co-table thead td {
+ text-transform: uppercase;
+ font-size: 16px;
+ color: #666;
+}
+
+.co-table thead td a {
+ color: #666;
+}
+
+.co-table thead td:after {
+ content: "\f175";
+ font-family: FontAwesome;
+ font-size: 12px;
+ margin-left: 10px;
+ visibility: hidden;
+}
+
+.co-table thead td.current:after {
+ content: "\f175";
+ visibility: visible;
+}
+
+.co-table thead td.current.reversed:after {
+ content: "\f176";
+ visibility: visible;
+}
+
+.co-table thead td.current a {
+ color: #337ab7;
+}
+
+.co-table .checkbox-col {
+ width: 24px;
+}
+
+.co-table td.options-col {
+ width: 30px;
+}
+
+.cor-checkable-menu {
+ display: inline-block;
+}
+
+.co-checkable-menu .co-checkable-menu-state {
+ display: inline-block;
+ margin-left: -1px;
+ margin-right: 4px;
+}
+
+.co-checkable-menu .dropdown {
+ display: inline-block;
+}
+
+.co-checkable-item, .co-checkable-menu-state {
+ width: 18px;
+ height: 18px;
+ cursor: pointer;
+ border: 1px solid #ddd;
+ display: inline-block;
+ vertical-align: middle;
+
+ position: relative
+}
+
+.co-checkable-item:after, .co-checkable-menu-state:after {
+ content: "\f00c";
+ font-family: FontAwesome;
+ color: #ccc;
+
+ position: absolute;
+ top: -1px;
+ left: 1px;
+
+ visibility: hidden;
+}
+
+.co-checkable-menu-state.some:after {
+ content: "-";
+ font-size: 35px;
+ top: -19px;
+ left: 2px;
+}
+
+.co-checkable-item:hover:after {
+ visibility: visible;
+}
+
+.co-checkable-item.checked:after, .co-checkable-menu-state.all:after, .co-checkable-menu-state.some:after {
+ visibility: visible;
+ color: #428bca;
+}
+
+.co-table .co-checkable-row.checked {
+ background: #F6FCFF;
+}
+
+.co-check-bar {
+ margin-bottom: 10px;
+}
+
+.co-check-bar .co-checked-actions {
+ display: inline-block;
+ border-left: 1px solid #eee;
+ margin-left: 10px;
+ padding-left: 4px;
+}
+
+.co-check-bar .co-checked-actions .btn {
+ margin-left: 6px;
+}
+
+.co-check-bar .co-checked-actions .btn .fa {
+ margin-right: 4px;
+}
+
+.co-check-bar .co-filter-box {
+ float: right;
+}
+
+.co-check-bar .co-filter-box input {
+ width: 300px;
+}
diff --git a/static/css/directives/repo-view/repo-panel-tags.css b/static/css/directives/repo-view/repo-panel-tags.css
new file mode 100644
index 000000000..83d520929
--- /dev/null
+++ b/static/css/directives/repo-view/repo-panel-tags.css
@@ -0,0 +1,60 @@
+.repo-panel-tags-element .fa-tag {
+ margin-right: 10px;
+}
+
+.repo-panel-tags-element .empty {
+ margin-top: 20px;
+}
+
+.repo-panel-tags-element .image-track {
+ width: 20px;
+ position: relative;
+}
+
+.repo-panel-tags-element .image-track-dot:after {
+ content: "\f10c";
+ font-family: FontAwesome;
+
+ display: inline-block;
+
+ position: absolute;
+ top: 15px;
+ left: 0px;
+ width: 17px;
+
+ font-size: 11px;
+ text-align: center;
+
+ background: white;
+ z-index: 300;
+ height: 13px;
+}
+
+.repo-panel-tags-element .image-track-line {
+ position: absolute;
+ top: 0px;
+ bottom: -11px;
+ left: 7px;
+ width: 0px;
+ display: inline-block;
+ height: 100%;
+ border-left: 2px solid black;
+
+ display: none;
+ z-index: 200;
+}
+
+.repo-panel-tags-element .image-track-line.start {
+ top: 18px;
+ display: block;
+}
+
+.repo-panel-tags-element .image-track-line.middle {
+ display: block;
+}
+
+.repo-panel-tags-element .image-track-line.end {
+ top: -1px;
+ height: 16px;
+ display: block;
+}
diff --git a/static/css/directives/repo-list-grid.css b/static/css/directives/ui/repo-list-grid.css
similarity index 96%
rename from static/css/directives/repo-list-grid.css
rename to static/css/directives/ui/repo-list-grid.css
index 886f6e881..8e253edd2 100644
--- a/static/css/directives/repo-list-grid.css
+++ b/static/css/directives/ui/repo-list-grid.css
@@ -86,7 +86,7 @@
background: linear-gradient(to bottom, rgba(255,240,188,1) 0%,rgba(255,255,255,0.5) 5%,rgba(255,255,255,0.49) 51%,rgba(255,255,255,0) 100%); /* W3C */
}
-.star-icon {
+.repo-list-grid .star-icon {
color: #ddd;
display: block;
font-size: 1.2em;
@@ -94,15 +94,6 @@
line-height: 2em;
}
-.star-icon:hover {
- cursor: pointer;
- cursor: hand;
-}
-
-.star-icon.starred {
- color: #ffba6d;
-}
-
.new-repo-listing {
display: block;
font-size: 14px;
diff --git a/static/css/directives/repo-list.css b/static/css/directives/ui/repo-list.css
similarity index 100%
rename from static/css/directives/repo-list.css
rename to static/css/directives/ui/repo-list.css
diff --git a/static/css/directives/ui/repo-star.css b/static/css/directives/ui/repo-star.css
new file mode 100644
index 000000000..205b765fc
--- /dev/null
+++ b/static/css/directives/ui/repo-star.css
@@ -0,0 +1,8 @@
+.repo-star .star-icon:hover {
+ cursor: pointer;
+ cursor: hand;
+}
+
+.repo-star .star-icon.starred {
+ color: #ffba6d;
+}
\ No newline at end of file
diff --git a/static/css/pages/repo-view.css b/static/css/pages/repo-view.css
new file mode 100644
index 000000000..e713ace27
--- /dev/null
+++ b/static/css/pages/repo-view.css
@@ -0,0 +1,16 @@
+.repository-view .repo-circle {
+ margin-right: 10px;
+}
+
+.repository-view .cor-title-content .repo-star {
+ margin-left: 20px;
+}
+
+.repository-view .repo-star {
+ color: #aaa;
+}
+
+.repository-view .tab-header {
+ margin-top: 0px;
+ margin-bottom: 20px;
+}
\ No newline at end of file
diff --git a/static/directives/cor-checkable-item.html b/static/directives/cor-checkable-item.html
new file mode 100644
index 000000000..f3e65e39b
--- /dev/null
+++ b/static/directives/cor-checkable-item.html
@@ -0,0 +1,3 @@
+
+
\ No newline at end of file
diff --git a/static/directives/cor-checkable-menu-item.html b/static/directives/cor-checkable-menu-item.html
new file mode 100644
index 000000000..452e37ea7
--- /dev/null
+++ b/static/directives/cor-checkable-menu-item.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/static/directives/cor-checkable-menu.html b/static/directives/cor-checkable-menu.html
new file mode 100644
index 000000000..2c6fce8f4
--- /dev/null
+++ b/static/directives/cor-checkable-menu.html
@@ -0,0 +1,12 @@
+
\ No newline at end of file
diff --git a/static/directives/cor-confirm-dialog.html b/static/directives/cor-confirm-dialog.html
new file mode 100644
index 000000000..330729390
--- /dev/null
+++ b/static/directives/cor-confirm-dialog.html
@@ -0,0 +1,25 @@
+
\ No newline at end of file
diff --git a/static/directives/repo-list-grid.html b/static/directives/repo-list-grid.html
index d5e04d442..edad56285 100644
--- a/static/directives/repo-list-grid.html
+++ b/static/directives/repo-list-grid.html
@@ -30,7 +30,8 @@
-
+
diff --git a/static/directives/repo-star.html b/static/directives/repo-star.html
new file mode 100644
index 000000000..1152b8338
--- /dev/null
+++ b/static/directives/repo-star.html
@@ -0,0 +1,5 @@
+
+
+
+
\ No newline at end of file
diff --git a/static/directives/repo-view/repo-panel-info.html b/static/directives/repo-view/repo-panel-info.html
new file mode 100644
index 000000000..c9e415043
--- /dev/null
+++ b/static/directives/repo-view/repo-panel-info.html
@@ -0,0 +1,9 @@
+
\ No newline at end of file
diff --git a/static/directives/repo-view/repo-panel-tags.html b/static/directives/repo-view/repo-panel-tags.html
new file mode 100644
index 000000000..6f71896bd
--- /dev/null
+++ b/static/directives/repo-view/repo-panel-tags.html
@@ -0,0 +1,116 @@
+
+
+
+
+
+ Are you sure you want to delete the following tags:
+
+
+ {{ tag_info.name }}
+
+
+
+
+ Note: This operation can take several minutes.
+
+
+
+
+
+ Are you sure you want to delete tag
+
{{ deleteTagInfo.tag }} ?
+
+
+ The following images and any other images not referenced by a tag will be deleted:
+
+
\ No newline at end of file
diff --git a/static/js/core-ui.js b/static/js/core-ui.js
index ac74f7b4c..8ce025d9c 100644
--- a/static/js/core-ui.js
+++ b/static/js/core-ui.js
@@ -162,6 +162,51 @@ angular.module("core-ui", [])
return directiveDefinitionObject;
})
+ .directive('corConfirmDialog', function() {
+ var directiveDefinitionObject = {
+ priority: 1,
+ templateUrl: '/static/directives/cor-confirm-dialog.html',
+ replace: false,
+ transclude: true,
+ restrict: 'C',
+ scope: {
+ 'dialogTitle': '@dialogTitle',
+ 'dialogActionTitle': '@dialogActionTitle',
+
+ 'dialogContext': '=dialogContext',
+ 'dialogAction': '&dialogAction'
+ },
+ controller: function($rootScope, $scope, $element) {
+ $scope.working = false;
+
+ $scope.$watch('dialogContext', function(dc) {
+ if (!dc) { return; }
+ $scope.show();
+ });
+
+ $scope.performAction = function() {
+ $scope.working = true;
+ $scope.dialogAction({
+ 'info': $scope.dialogContext,
+ 'callback': function(result) {
+ $scope.hide();
+ }
+ });
+ };
+
+ $scope.show = function() {
+ $scope.working = false;
+ $element.find('.modal').modal({});
+ };
+
+ $scope.hide = function() {
+ $element.find('.modal').modal('hide');
+ };
+ }
+ };
+ return directiveDefinitionObject;
+ })
+
.directive('corTabPanel', function() {
var directiveDefinitionObject = {
priority: 1,
@@ -581,4 +626,84 @@ angular.module("core-ui", [])
}
};
return directiveDefinitionObject;
+ })
+
+ .directive('corCheckableMenu', function() {
+ var directiveDefinitionObject = {
+ priority: 1,
+ templateUrl: '/static/directives/cor-checkable-menu.html',
+ replace: false,
+ transclude: true,
+ restrict: 'C',
+ scope: {
+ 'controller': '=controller'
+ },
+ controller: function($rootScope, $scope, $element) {
+ $scope.getClass = function(items, checked) {
+ if (checked.length == 0) {
+ return 'none';
+ }
+
+ if (checked.length == items.length) {
+ return 'all';
+ }
+
+ return 'some';
+ };
+
+ $scope.toggleItems = function($event) {
+ $event.stopPropagation();
+ $scope.controller.toggleItems();
+ };
+
+ this.checkByFilter = function(filter) {
+ $scope.controller.checkByFilter(filter);
+ };
+ }
+ };
+ return directiveDefinitionObject;
+ })
+
+ .directive('corCheckableMenuItem', function() {
+ var directiveDefinitionObject = {
+ priority: 1,
+ templateUrl: '/static/directives/cor-checkable-menu-item.html',
+ replace: true,
+ transclude: true,
+ restrict: 'C',
+ require: '^corCheckableMenu',
+ scope: {
+ 'itemFilter': '=itemFilter'
+ },
+ link: function($scope, $element, $attr, parent) {
+ $scope.parent = parent;
+ },
+
+ controller: function($rootScope, $scope, $element) {
+ $scope.selected = function() {
+ $scope.parent.checkByFilter(this.itemFilter);
+ };
+ }
+ };
+ return directiveDefinitionObject;
+ })
+
+ .directive('corCheckableItem', function() {
+ var directiveDefinitionObject = {
+ priority: 1,
+ templateUrl: '/static/directives/cor-checkable-item.html',
+ replace: false,
+ transclude: false,
+ restrict: 'C',
+ scope: {
+ 'item': '=item',
+ 'controller': '=controller'
+ },
+ controller: function($rootScope, $scope, $element) {
+ $scope.toggleItem = function() {
+ $scope.controller.toggleItem($scope.item);
+ };
+ }
+ };
+ return directiveDefinitionObject;
});
\ No newline at end of file
diff --git a/static/js/directives/repo-view/repo-panel-info.js b/static/js/directives/repo-view/repo-panel-info.js
new file mode 100644
index 000000000..b72a59862
--- /dev/null
+++ b/static/js/directives/repo-view/repo-panel-info.js
@@ -0,0 +1,23 @@
+/**
+ * An element which displays the information panel for a repository view.
+ */
+angular.module('quay').directive('repoPanelInfo', function () {
+ var directiveDefinitionObject = {
+ priority: 0,
+ templateUrl: '/static/directives/repo-view/repo-panel-info.html',
+ replace: false,
+ transclude: false,
+ restrict: 'C',
+ scope: {
+ 'repository': '=repository'
+ },
+ controller: function($scope, $element, ApiService) {
+ $scope.updateDescription = function(content) {
+ $scope.repository.description = content;
+ $scope.repository.put();
+ };
+ }
+ };
+ return directiveDefinitionObject;
+});
+
diff --git a/static/js/directives/repo-view/repo-panel-tags.js b/static/js/directives/repo-view/repo-panel-tags.js
new file mode 100644
index 000000000..f9bcbf9e2
--- /dev/null
+++ b/static/js/directives/repo-view/repo-panel-tags.js
@@ -0,0 +1,222 @@
+/**
+ * An element which displays the tags panel for a repository view.
+ */
+angular.module('quay').directive('repoPanelTags', function () {
+ var directiveDefinitionObject = {
+ priority: 0,
+ templateUrl: '/static/directives/repo-view/repo-panel-tags.html',
+ replace: false,
+ transclude: false,
+ restrict: 'C',
+ scope: {
+ 'repository': '=repository',
+ 'repositoryUpdated': '&repositoryUpdated'
+ },
+ controller: function($scope, $element, $filter, ApiService, UIService) {
+ var orderBy = $filter('orderBy');
+
+ $scope.checkedTags = UIService.createCheckStateController([]);
+ $scope.options = {
+ 'predicate': 'last_modified_datetime',
+ 'reverse': false
+ };
+
+ $scope.iterationState = {};
+
+ var loadImages = function() {
+ var params = {
+ 'repository': $scope.repository.namespace + '/' + $scope.repository.name
+ };
+
+ $scope.imagesResource = ApiService.listRepositoryImagesAsResource(params).get(function(resp) {
+ $scope.images = resp.images;
+ });
+ };
+
+ var setTagState = function() {
+ if (!$scope.repository) { return; }
+
+ var tags = [];
+ var allTags = [];
+ var imageMap = {};
+ var imageTracks = [];
+
+ // Build a list of tags and filtered tags.
+ for (var tag in $scope.repository.tags) {
+ if (!$scope.repository.tags.hasOwnProperty(tag)) { continue; }
+
+ var tagInfo = $.extend($scope.repository.tags[tag], {
+ 'name': tag,
+ 'last_modified_datetime': new Date($scope.repository.tags[tag].last_modified)
+ });
+
+ allTags.push(tagInfo);
+
+ if (!$scope.options.tagFilter || tag.indexOf($scope.options.tagFilter) >= 0 ||
+ tagInfo.image_id.indexOf($scope.options.tagFilter) >= 0) {
+ tags.push(tagInfo);
+ }
+ }
+
+ // Sort the tags by the predicate and the reverse, and map the information.
+ var ordered = orderBy(tags, $scope.options.predicate, $scope.options.reverse);
+ for (var i = 0; i < ordered.length; ++i) {
+ var tagInfo = ordered[i];
+ if (!imageMap[tagInfo.image_id]) {
+ imageMap[tagInfo.image_id] = [];
+ }
+
+ imageMap[tagInfo.image_id].push(tagInfo);
+ };
+
+ // Calculate the image tracks.
+ var colors = d3.scale.category10();
+ var index = 0;
+
+ for (var image_id in imageMap) {
+ if (imageMap[image_id].length >= 2){
+ imageTracks.push({
+ 'image_id': image_id,
+ 'color': colors(index),
+ 'count': imageMap[image_id].length,
+ 'tags': imageMap[image_id]
+ });
+ ++index;
+ }
+ }
+
+ $scope.imageMap = imageMap;
+ $scope.imageTracks = imageTracks;
+ $scope.tags = ordered;
+ $scope.checkedTags = UIService.createCheckStateController(ordered);
+ $scope.allTags = allTags;
+ $scope.iterationState = {};
+ }
+
+ $scope.$watch('options.predicate', setTagState);
+ $scope.$watch('options.reverse', setTagState);
+ $scope.$watch('options.tagFilter', setTagState);
+ $scope.$watch('repository', function(repository) {
+ if (!repository) { return; }
+
+ // Load the repository's images.
+ loadImages();
+
+ // Process each of the tags.
+ setTagState();
+ });
+
+ $scope.trackLineClass = function(index, track_info) {
+ var startIndex = $.inArray(track_info.tags[0], $scope.tags);
+ var endIndex = $.inArray(track_info.tags[track_info.tags.length - 1], $scope.tags);
+
+ if (index == startIndex) {
+ return 'start';
+ }
+
+ if (index == endIndex) {
+ return 'end';
+ }
+
+ if (index > startIndex && index < endIndex) {
+ return 'middle';
+ }
+
+ if (index < startIndex) {
+ return 'before';
+ }
+
+ if (index > endIndex) {
+ return 'after';
+ }
+ };
+
+ $scope.tablePredicateClass = function(name, predicate, reverse) {
+ if (name != predicate) {
+ return '';
+ }
+
+ return 'current ' + (reverse ? 'reversed' : '');
+ };
+
+ $scope.askDeleteTag = function(tag) {
+ $scope.deleteTagInfo = {
+ 'tag': tag
+ };
+ };
+
+ $scope.askDeleteMultipleTags = function(tags) {
+ $scope.deleteMultipleTagsInfo = {
+ 'tags': tags
+ };
+ };
+
+ $scope.deleteMultipleTags = function(tags, callback) {
+ var count = tags.length;
+ var perform = function(index) {
+ if (index >= count) {
+ callback(true);
+ return;
+ }
+
+ var tag_info = tags[index];
+ $scope.deleteTag(tag_info.name, function(result) {
+ if (!result) {
+ callback(false);
+ return;
+ }
+
+ perform(index + 1);
+ });
+ };
+
+ perform(0);
+ };
+
+ $scope.deleteTag = function(tag, callback) {
+ if (!$scope.repository.can_admin) { return; }
+
+ var params = {
+ 'repository': $scope.repository.namespace + '/' + $scope.repository.name,
+ 'tag': tag
+ };
+
+ var errorHandler = ApiService.errorDisplay('Cannot delete tag', callback);
+ ApiService.deleteFullTag(null, params).then(function() {
+ callback(true);
+ $scope.tags = $.grep($scope.tags, function(tagInfo) {
+ return tagInfo.name != tag;
+ });
+
+ $scope.checkedTags = UIService.createCheckStateController($scope.tags);
+ $scope.repositoryUpdated({});
+ }, errorHandler);
+ };
+
+ $scope.orderBy = function(predicate) {
+ if (predicate == $scope.options.predicate) {
+ $scope.options.reverse = !$scope.options.reverse;
+ return;
+ }
+
+ $scope.options.reverse = false;
+ $scope.options.predicate = predicate;
+ };
+
+ $scope.commitTagFilter = function(tag) {
+ var r = new RegExp('^[0-9a-f]{7}$');
+ return tag.name.match(r);
+ };
+
+ $scope.allTagFilter = function(tag) {
+ return true;
+ };
+
+ $scope.noTagFilter = function(tag) {
+ return false;
+ };
+ }
+ };
+ return directiveDefinitionObject;
+});
+
diff --git a/static/js/directives/ui/repo-list-grid.js b/static/js/directives/ui/repo-list-grid.js
index 236a9c609..aa54bfda0 100644
--- a/static/js/directives/ui/repo-list-grid.js
+++ b/static/js/directives/ui/repo-list-grid.js
@@ -13,7 +13,7 @@ angular.module('quay').directive('repoListGrid', function () {
starred: '=starred',
user: "=user",
namespace: '=namespace',
- toggleStar: '&toggleStar'
+ starToggled: '&starToggled'
},
controller: function($scope, $element, UserService) {
$scope.isOrganization = function(namespace) {
diff --git a/static/js/directives/ui/repo-star.js b/static/js/directives/ui/repo-star.js
new file mode 100644
index 000000000..6160222d8
--- /dev/null
+++ b/static/js/directives/ui/repo-star.js
@@ -0,0 +1,53 @@
+/**
+ * An element that displays the star status of a repository and allows it to be toggled.
+ */
+angular.module('quay').directive('repoStar', function () {
+ var directiveDefinitionObject = {
+ priority: 0,
+ templateUrl: '/static/directives/repo-star.html',
+ replace: false,
+ transclude: true,
+ restrict: 'C',
+ scope: {
+ repository: '=repository',
+ starToggled: '&starToggled'
+ },
+ controller: function($scope, $element, UserService, ApiService) {
+ // Star a repository or unstar a repository.
+ $scope.toggleStar = function() {
+ if ($scope.repository.is_starred) {
+ unstarRepo();
+ } else {
+ starRepo();
+ }
+ };
+
+ // Star a repository and update the UI.
+ var starRepo = function() {
+ var data = {
+ 'namespace': $scope.repository.namespace,
+ 'repository': $scope.repository.name
+ };
+
+ ApiService.createStar(data).then(function(result) {
+ $scope.repository.is_starred = true;
+ $scope.starToggled({'repository': $scope.repository});
+ }, ApiService.errorDisplay('Could not star repository'));
+ };
+
+ // Unstar a repository and update the UI.
+ var unstarRepo = function(repo) {
+ var data = {
+ 'repository': $scope.repository.namespace + '/' + $scope.repository.name
+ };
+
+ ApiService.deleteStar(null, data).then(function(result) {
+ $scope.repository.is_starred = false;
+ $scope.starToggled({'repository': $scope.repository});
+ }, ApiService.errorDisplay('Could not unstar repository'));
+ };
+ }
+ };
+
+ return directiveDefinitionObject;
+});
\ No newline at end of file
diff --git a/static/js/directives/ui/resource-view.js b/static/js/directives/ui/resource-view.js
index c7fc2e4c1..e423cd003 100644
--- a/static/js/directives/ui/resource-view.js
+++ b/static/js/directives/ui/resource-view.js
@@ -27,7 +27,7 @@ angular.module('quay').directive('resourceView', function () {
return 'loading';
}
- if (current.error) {
+ if (current.hasError) {
return 'error';
}
}
diff --git a/static/js/pages/repo-list.js b/static/js/pages/repo-list.js
index 08fd658f0..9ea73e774 100644
--- a/static/js/pages/repo-list.js
+++ b/static/js/pages/repo-list.js
@@ -52,38 +52,14 @@
return !!UserService.getOrganization(namespace);
};
- // Star a repository or unstar a repository.
- $scope.toggleStar = function(repo) {
+ $scope.starToggled = function(repo) {
if (repo.is_starred) {
- unstarRepo(repo);
- } else {
- starRepo(repo);
- }
- }
-
- // Star a repository and update the UI.
- var starRepo = function(repo) {
- var data = {
- 'namespace': repo.namespace,
- 'repository': repo.name
- };
- ApiService.createStar(data).then(function(result) {
- repo.is_starred = true;
$scope.starred_repositories.value.push(repo);
- }, ApiService.errorDisplay('Could not star repository'));
- };
-
- // Unstar a repository and update the UI.
- var unstarRepo = function(repo) {
- var data = {
- 'repository': repo.namespace + '/' + repo.name
- };
- ApiService.deleteStar(null, data).then(function(result) {
- repo.is_starred = false;
+ } else {
$scope.starred_repositories.value = $scope.starred_repositories.value.filter(function(repo) {
return repo.is_starred;
});
- }, ApiService.errorDisplay('Could not unstar repository'));
+ }
};
// Finds a duplicate repo if it exists. If it doesn't, inserts the repo.
diff --git a/static/js/pages/repo-view.js b/static/js/pages/repo-view.js
index 5aaa1b225..19ce0cd97 100644
--- a/static/js/pages/repo-view.js
+++ b/static/js/pages/repo-view.js
@@ -3,10 +3,49 @@
* Repository view page.
*/
angular.module('quayPages').config(['pages', function(pages) {
- pages.create('repo-view', 'repo-view.html', RepoCtrl);
+ pages.create('repo-view', 'repo-view.html', RepoViewCtrl, {
+ 'newLayout': true,
+ 'title': '{{ namespace }}/{{ name }}',
+ 'description': 'Repository {{ namespace }}/{{ name }}'
+ }, ['layout'])
+
+ pages.create('repo-view', 'old-repo-view.html', OldRepoViewCtrl, {
+ }, ['old-layout']);
}]);
- function RepoCtrl($scope, $sanitize, Restangular, ImageMetadataService, ApiService, $routeParams, $rootScope, $location, $timeout, Config, UtilService) {
+ function RepoViewCtrl($scope, $routeParams, ApiService, UserService) {
+ $scope.namespace = $routeParams.namespace;
+ $scope.name = $routeParams.name;
+
+ $scope.logsShown = 0;
+
+ // Make sure we track the current user.
+ UserService.updateUserIn($scope);
+
+ var loadRepository = function() {
+ var params = {
+ 'repository': $scope.namespace + '/' + $scope.name
+ };
+
+ $scope.repositoryResource = ApiService.getRepoAsResource(params).get(function(repo) {
+ $scope.repository = repo;
+ $scope.setTag($routeParams.tag);
+ });
+ };
+
+ // Load the repository.
+ loadRepository();
+
+ $scope.setTag = function(tagName) {
+ window.console.log('set tag')
+ };
+
+ $scope.showLogs = function() {
+ $scope.logsShown++;
+ };
+ }
+
+ function OldRepoViewCtrl($scope, $sanitize, Restangular, ImageMetadataService, ApiService, $routeParams, $rootScope, $location, $timeout, Config, UtilService) {
$scope.Config = Config;
var namespace = $routeParams.namespace;
diff --git a/static/js/services/ui-service.js b/static/js/services/ui-service.js
index f6872a5ba..1f084c6d6 100644
--- a/static/js/services/ui-service.js
+++ b/static/js/services/ui-service.js
@@ -2,6 +2,46 @@
* Service which provides helper methods for performing some simple UI operations.
*/
angular.module('quay').factory('UIService', [function() {
+ var CheckStateController = function(items) {
+ this.items = items;
+ this.checked = [];
+ };
+
+ CheckStateController.prototype.isChecked = function(item) {
+ return $.inArray(item, this.checked) >= 0;
+ };
+
+ CheckStateController.prototype.toggleItem = function(item) {
+ if (this.isChecked(item)) {
+ this.uncheckItem(item);
+ } else {
+ this.checkItem(item);
+ }
+ };
+
+ CheckStateController.prototype.toggleItems = function() {
+ if (this.checked.length) {
+ this.checked = [];
+ } else {
+ this.checked = this.items.slice();
+ }
+ };
+
+ CheckStateController.prototype.checkByFilter = function(filter) {
+ this.checked = $.grep(this.items, filter);
+ };
+
+ CheckStateController.prototype.checkItem = function(item) {
+ this.checked.push(item);
+ };
+
+ CheckStateController.prototype.uncheckItem = function(item) {
+ this.checked = $.grep(this.checked, function(cItem) {
+ return cItem != item;
+ });
+ };
+
+
var uiService = {};
uiService.hidePopover = function(elem) {
@@ -33,5 +73,9 @@ angular.module('quay').factory('UIService', [function() {
}
};
+ uiService.createCheckStateController = function(items) {
+ return new CheckStateController(items);
+ };
+
return uiService;
}]);
diff --git a/static/partials/old-repo-view.html b/static/partials/old-repo-view.html
new file mode 100644
index 000000000..df3030803
--- /dev/null
+++ b/static/partials/old-repo-view.html
@@ -0,0 +1,425 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ This repository is empty
+
+
+
+
+
How to push a new image to this repository:
+
+ First login to the registry (if you have not done so already):
+
sudo docker login {{ Config.getDomain() }}
+
+ Tag an image to this repository:
+
sudo docker tag 0u123imageidgoeshere {{ Config.getDomain() }}/{{repo.namespace}}/{{repo.name}}
+
+ Push the image to this repository:
+
sudo docker push {{ Config.getDomain() }}/{{repo.namespace}}/{{repo.name}}
+
+
+
+
+
+
+
+
+ A build is currently processing. If this takes longer than an hour, please
contact us
+
+
+
+
+
+
+ A push to this repository is in progress.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{getTagCount(repo)}}
+
+
+ {{imageHistory.value.length}}
+
+
+
+
+
+
+
+
+ Last Modified
+
+
+
+
+
+
+
+ Total Compressed Size
+ {{ getTotalSize(currentTag) | bytes }}
+
+
+
+
+
+
+
+ Delete Tag
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ getFormattedCommand(currentImage) }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ currentImage.size | bytes }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{currentImageChanges.added.length}}
+
+
+
+ {{currentImageChanges.removed.length}}
+
+
+
+ {{currentImageChanges.changed.length}}
+
+
+
+
+
+
+ {{file}}
+
+
+
+ {{file}}
+
+
+
+ {{file}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Are you sure you want to delete tag
+
+ {{ tagToDelete }}
+ ?
+
+
+ The following images and any other images not referenced by a tag will be deleted:
+
+
+
+
+
+
+
diff --git a/static/partials/repo-list.html b/static/partials/repo-list.html
index f0ee139ba..d5ef953ec 100644
--- a/static/partials/repo-list.html
+++ b/static/partials/repo-list.html
@@ -50,14 +50,15 @@
+ starred="true"
+ star-toggled="starToggled(repository)">
+ star-toggled="starToggled(repository)">
diff --git a/static/partials/repo-view.html b/static/partials/repo-view.html
index df3030803..b61ee10b5 100644
--- a/static/partials/repo-view.html
+++ b/static/partials/repo-view.html
@@ -1,425 +1,76 @@
-
+
+
+
+
+
+
+ {{ namespace }} / {{ name }}
+
+
+
-