diff --git a/buildman/jobutil/buildjob.py b/buildman/jobutil/buildjob.py
index 3c00a3bc3..a8a5e2b80 100644
--- a/buildman/jobutil/buildjob.py
+++ b/buildman/jobutil/buildjob.py
@@ -104,7 +104,9 @@ class BuildJob(object):
return None
# Build an in-memory tree of the full heirarchy of images in the repository.
- all_images = model.get_repository_images(repo_namespace, repo_name)
+ all_images = model.get_repository_images_without_placements(repo_build.repository,
+ with_ancestor=base_image)
+
all_tags = model.list_repository_tags(repo_namespace, repo_name)
tree = ImageTree(all_images, all_tags, base_filter=base_image.id)
diff --git a/data/migrations/versions/37c47a7af956_add_custom_git_trigger_type_to_database.py b/data/migrations/versions/37c47a7af956_add_custom_git_trigger_type_to_database.py
new file mode 100644
index 000000000..ef2f9efa3
--- /dev/null
+++ b/data/migrations/versions/37c47a7af956_add_custom_git_trigger_type_to_database.py
@@ -0,0 +1,25 @@
+"""add custom-git trigger type to database
+
+Revision ID: 37c47a7af956
+Revises: 3fee6f979c2a
+Create Date: 2015-04-24 14:50:26.275516
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = '37c47a7af956'
+down_revision = '3fee6f979c2a'
+
+from alembic import op
+import sqlalchemy as sa
+
+
+def upgrade(tables):
+ op.bulk_insert(tables.buildtriggerservice, [{'id': 2, 'name': 'custom-git'}])
+
+
+def downgrade(tables):
+ op.execute(
+ tables.buildtriggerservice.delete()
+ .where(tables.buildtriggerservice.c.name == op.inline_literal('custom-git'))
+ )
diff --git a/data/model/legacy.py b/data/model/legacy.py
index cd158e6a6..a3b9a3d2c 100644
--- a/data/model/legacy.py
+++ b/data/model/legacy.py
@@ -1751,6 +1751,21 @@ def get_matching_repository_images(namespace_name, repository_name, docker_image
return _get_repository_images_base(namespace_name, repository_name, modify_query)
+
+def get_repository_images_without_placements(repository, with_ancestor=None):
+ query = (Image
+ .select(Image, ImageStorage)
+ .join(ImageStorage)
+ .where(Image.repository == repository))
+
+ if with_ancestor:
+ ancestors_string = '%s%s/' % (with_ancestor.ancestors, with_ancestor.id)
+ query = query.where((Image.ancestors ** (ancestors_string + '%')) |
+ (Image.id == with_ancestor.id))
+
+ return query
+
+
def get_repository_images(namespace_name, repository_name):
return _get_repository_images_base(namespace_name, repository_name, lambda q: q)
diff --git a/endpoints/api/user.py b/endpoints/api/user.py
index 93e77f47c..b03c5f87b 100644
--- a/endpoints/api/user.py
+++ b/endpoints/api/user.py
@@ -444,19 +444,19 @@ class ConvertToOrganization(ApiResource):
user = get_authenticated_user()
convert_data = request.get_json()
- # Ensure that the new admin user is the not user being converted.
- admin_username = convert_data['adminUser']
- if admin_username == user.username:
- raise request_error(reason='invaliduser',
- message='The admin user is not valid')
-
# Ensure that the sign in credentials work.
+ admin_username = convert_data['adminUser']
admin_password = convert_data['adminPassword']
(admin_user, error_message) = authentication.verify_user(admin_username, admin_password)
if not admin_user:
raise request_error(reason='invaliduser',
message='The admin user credentials are not valid')
+ # Ensure that the new admin user is the not user being converted.
+ if admin_user.id == user.id:
+ raise request_error(reason='invaliduser',
+ message='The admin user is not valid')
+
# Subscribe the organization to the new plan.
if features.BILLING:
plan = convert_data.get('plan', 'free')
diff --git a/endpoints/verbs.py b/endpoints/verbs.py
index 619da283f..88d9dc7b1 100644
--- a/endpoints/verbs.py
+++ b/endpoints/verbs.py
@@ -140,7 +140,7 @@ def _repo_verb_signature(namespace, repository, tag, verb, checker=None, **kwarg
# Lookup the derived image storage for the verb.
derived = model.find_derived_storage(repo_image.storage, verb)
if derived is None or derived.uploading:
- abort(404)
+ return make_response('', 202)
# Check if we have a valid signer configured.
if not signer.name:
diff --git a/endpoints/web.py b/endpoints/web.py
index 3dda9998d..9d552fe1c 100644
--- a/endpoints/web.py
+++ b/endpoints/web.py
@@ -28,6 +28,7 @@ from util.systemlogs import build_logs_archive
from auth import scopes
import features
+import json
logger = logging.getLogger(__name__)
@@ -432,16 +433,16 @@ def request_authorization_code():
# Load the application information.
oauth_app = provider.get_application_for_client_id(client_id)
- app_email = oauth_app.email or organization.email
+ app_email = oauth_app.avatar_email or oauth_app.organization.email
oauth_app_view = {
'name': oauth_app.name,
'description': oauth_app.description,
'url': oauth_app.application_uri,
- 'avatar': avatar.get_data(oauth_app.name, app_email, 'app'),
+ 'avatar': json.dumps(avatar.get_data(oauth_app.name, app_email, 'app')),
'organization': {
'name': oauth_app.organization.username,
- 'avatar': avatar.get_data_for_org(oauth_app.organization)
+ 'avatar': json.dumps(avatar.get_data_for_org(oauth_app.organization))
}
}
@@ -562,6 +563,9 @@ def redirect_to_repository(namespace, reponame, tag):
permission = ReadRepositoryPermission(namespace, reponame)
is_public = model.repository_is_public(namespace, reponame)
+ if request.args.get('ac-discovery', 0) == 1:
+ return index('')
+
if permission.can() or is_public:
repository_name = '/'.join([namespace, reponame])
return redirect(url_for('web.repository', path=repository_name, tag=tag))
diff --git a/external_libraries.py b/external_libraries.py
index 72da2f90e..1e0e2c73c 100644
--- a/external_libraries.py
+++ b/external_libraries.py
@@ -20,7 +20,7 @@ EXTERNAL_JS = [
EXTERNAL_CSS = [
'netdna.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.css',
'netdna.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css',
- 'fonts.googleapis.com/css?family=Source+Sans+Pro:400,700',
+ 'fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,700',
's3.amazonaws.com/cdn.core-os.net/icons/core-icons.css'
]
diff --git a/static/css/core-ui.css b/static/css/core-ui.css
index 691d6a74f..4c401c8a2 100644
--- a/static/css/core-ui.css
+++ b/static/css/core-ui.css
@@ -1,3 +1,10 @@
+a:active {
+ outline: none !important;
+}
+
+a:focus {
+ outline: none !important;
+}
.co-options-menu .fa-gear {
color: #999;
@@ -879,6 +886,7 @@
}
.cor-title-link {
+ font-weight: 300;
line-height: 30px;
margin-top: 22px;
margin-bottom: 10px;
diff --git a/static/css/directives/repo-view/repo-panel-changes.css b/static/css/directives/repo-view/repo-panel-changes.css
index 14838ee1d..8c4310a47 100644
--- a/static/css/directives/repo-view/repo-panel-changes.css
+++ b/static/css/directives/repo-view/repo-panel-changes.css
@@ -26,3 +26,9 @@
margin-right: 8px;
vertical-align: middle;
}
+
+.repo-panel-changes .multiselect-dropdown {
+ display: inline-block;
+ margin-left: 10px;
+ min-width: 200px;
+}
\ No newline at end of file
diff --git a/static/css/directives/repo-view/repo-panel-info.css b/static/css/directives/repo-view/repo-panel-info.css
index 496d32a2f..1b39b3a6b 100644
--- a/static/css/directives/repo-view/repo-panel-info.css
+++ b/static/css/directives/repo-view/repo-panel-info.css
@@ -67,4 +67,8 @@
.repo-panel-info-element .builds-list {
min-height: 200px;
+}
+
+.repo-panel-info-element .copy-box {
+ vertical-align: middle;
}
\ No newline at end of file
diff --git a/static/css/directives/ui/header-bar.css b/static/css/directives/ui/header-bar.css
index db72b4e73..7fa08bf22 100644
--- a/static/css/directives/ui/header-bar.css
+++ b/static/css/directives/ui/header-bar.css
@@ -48,7 +48,7 @@ nav.navbar-default .navbar-nav>li>a.active {
right: 0px;
top: -50px;
z-index: 4;
- height: 83px;
+ height: 56px;
transition: top 0.3s cubic-bezier(.23,.88,.72,.98);
background: white;
box-shadow: 0px 1px 16px #444;
@@ -71,7 +71,7 @@ nav.navbar-default .navbar-nav>li>a.active {
color: #ccc;
margin-right: 10px;
position: absolute;
- top: 34px;
+ top: 20px;
left: 14px;
}
@@ -84,9 +84,9 @@ nav.navbar-default .navbar-nav>li>a.active {
}
.header-bar-element .search-box .search-box-wrapper input {
- font-size: 28px;
+ font-size: 18px;
width: 100%;
- padding: 10px;
+ padding: 6px;
border: 0px;
}
@@ -94,7 +94,7 @@ nav.navbar-default .navbar-nav>li>a.active {
position: absolute;
left: 0px;
right: 0px;
- top: -130px;
+ top: -106px;
z-index: 3;
transition: top 0.4s cubic-bezier(.23,.88,.72,.98), height 0.25s ease-in-out;
@@ -104,7 +104,7 @@ nav.navbar-default .navbar-nav>li>a.active {
}
.header-bar-element .search-results.loading, .header-bar-element .search-results.results {
- top: 130px;
+ top: 106px;
}
.header-bar-element .search-results.loading {
@@ -153,7 +153,7 @@ nav.navbar-default .navbar-nav>li>a.active {
margin-right: 4px;
}
-.header-bar-element .search-results li .description {
+.header-bar-element .search-results li .result-description {
overflow: hidden;
text-overflow: ellipsis;
max-height: 24px;
@@ -161,6 +161,11 @@ nav.navbar-default .navbar-nav>li>a.active {
display: inline-block;
color: #aaa;
vertical-align: middle;
+ margin-top: 2px;
+}
+
+.header-bar-element .search-results li .description img {
+ display: none;
}
.header-bar-element .search-results li .score:before {
diff --git a/static/css/directives/ui/logs-view.css b/static/css/directives/ui/logs-view.css
index 198bad42f..db77c9b23 100644
--- a/static/css/directives/ui/logs-view.css
+++ b/static/css/directives/ui/logs-view.css
@@ -94,7 +94,12 @@
white-space: nowrap;
}
+.logs-view-element .side-controls .filter-input {
+ vertical-align: middle;
+}
+
.logs-view-element .side-controls {
+ float: none !important;
text-align: right;
margin-bottom: 20px;
}
\ No newline at end of file
diff --git a/static/css/directives/ui/multiselect-dropdown.css b/static/css/directives/ui/multiselect-dropdown.css
new file mode 100644
index 000000000..76f29f3dd
--- /dev/null
+++ b/static/css/directives/ui/multiselect-dropdown.css
@@ -0,0 +1,42 @@
+.multiselect-dropdown .dropdown,
+.multiselect-dropdown .dropdown .btn-dropdown,
+.multiselect-dropdown .dropdown .dropdown-menu {
+ width: 100%;
+}
+
+.multiselect-dropdown .dropdown .btn-dropdown {
+ text-align: left;
+ position: relative;
+ padding-right: 16px;
+}
+
+.multiselect-dropdown .dropdown .btn-dropdown .caret {
+ position: absolute;
+ top: 14px;
+ right: 10px;
+}
+
+.multiselect-dropdown .none {
+ color: #ccc;
+ margin-right: 10px;
+}
+
+.multiselect-dropdown .dropdown-menu {
+ padding: 10px;
+}
+
+.multiselect-dropdown .dropdown-menu .menu-item {
+ padding: 4px;
+}
+
+.multiselect-dropdown .dropdown-menu .menu-item .co-checkable-item {
+ margin-right: 6px;
+}
+
+.multiselect-dropdown .dropdown-menu .menu-item .menu-item-template {
+ vertical-align: middle;
+}
+
+.multiselect-dropdown .selected-item-template {
+ margin-right: 10px;
+}
diff --git a/static/css/directives/ui/namespace-selector.css b/static/css/directives/ui/namespace-selector.css
new file mode 100644
index 000000000..21053e9c9
--- /dev/null
+++ b/static/css/directives/ui/namespace-selector.css
@@ -0,0 +1,35 @@
+.namespace-selector-dropdown .namespace {
+ padding: 6px;
+ padding-left: 10px;
+ cursor: pointer;
+ font-size: 14px;
+ color: black;
+}
+
+.namespace-selector-dropdown .namespace-item {
+ position: relative;
+}
+
+.namespace-selector-dropdown .namespace-item .fa {
+ position: absolute;
+ right: 12px;
+ top: 12px;
+ color: #aaa;
+}
+
+.namespace-selector-dropdown .avatar {
+ margin-right: 4px;
+}
+
+.namespace-selector-dropdown a.namespace {
+ color: black !important;
+}
+
+.namespace-selector-dropdown .namespace-item.disabled .avatar {
+ -webkit-filter: grayscale(1);
+ opacity: 0.5;
+}
+
+.namespace-selector-dropdown .namespace-item .tooltip-inner {
+ min-width: 200px;
+}
diff --git a/static/css/directives/ui/repo-list-grid.css b/static/css/directives/ui/repo-list-grid.css
index d9418ae95..786cc793c 100644
--- a/static/css/directives/ui/repo-list-grid.css
+++ b/static/css/directives/ui/repo-list-grid.css
@@ -90,9 +90,7 @@
.new-repo-listing .description {
font-size: 0.91em;
padding-top: 13px;
-}
-
-.new-repo-listing .description {
+ padding-left: 11px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
diff --git a/static/css/pages/repo-view.css b/static/css/pages/repo-view.css
index 73e38891c..08971ed23 100644
--- a/static/css/pages/repo-view.css
+++ b/static/css/pages/repo-view.css
@@ -50,9 +50,7 @@
}
@media (max-width: 767px) {
- .repository-view .repo-star {
- position: absolute;
- top: 16px;
- left: -16px;
+ .repository-view .cor-title-content {
+ padding-top: 8px;
}
}
diff --git a/static/css/quay.css b/static/css/quay.css
index b570ffa22..8631585e7 100644
--- a/static/css/quay.css
+++ b/static/css/quay.css
@@ -548,38 +548,6 @@ i.toggle-icon:hover {
float: right;
}
-.namespace-selector-dropdown .namespace {
- padding: 6px;
- padding-left: 10px;
- cursor: pointer;
- font-size: 14px;
- color: black;
-}
-
-.namespace-selector-dropdown .namespace-item {
- position: relative;
-}
-
-.namespace-selector-dropdown .namespace-item .fa {
- position: absolute;
- right: 12px;
- top: 12px;
- color: #aaa;
-}
-
-.namespace-selector-dropdown a.namespace {
- color: black !important;
-}
-
-.namespace-selector-dropdown .namespace-item.disabled img {
- -webkit-filter: grayscale(1);
- opacity: 0.5;
-}
-
-.namespace-selector-dropdown .namespace-item .tooltip-inner {
- min-width: 200px;
-}
-
.notification-primary {
background: #428bca;
color: white;
diff --git a/static/directives/build-logs-view.html b/static/directives/build-logs-view.html
index 074ccd5b0..f99a7eb00 100644
--- a/static/directives/build-logs-view.html
+++ b/static/directives/build-logs-view.html
@@ -30,7 +30,7 @@
-
+
diff --git a/static/directives/build-mini-status.html b/static/directives/build-mini-status.html
index bba073329..97b14d5c5 100644
--- a/static/directives/build-mini-status.html
+++ b/static/directives/build-mini-status.html
@@ -1,6 +1,7 @@
-
+
diff --git a/static/directives/cor-title-content.html b/static/directives/cor-title-content.html
index 5b2077d08..0d3e13ddd 100644
--- a/static/directives/cor-title-content.html
+++ b/static/directives/cor-title-content.html
@@ -1,3 +1,3 @@
-
+
diff --git a/static/directives/cor-title-link.html b/static/directives/cor-title-link.html
index 428671f86..afd345c75 100644
--- a/static/directives/cor-title-link.html
+++ b/static/directives/cor-title-link.html
@@ -1 +1 @@
-
+
diff --git a/static/directives/multiselect-dropdown.html b/static/directives/multiselect-dropdown.html
new file mode 100644
index 000000000..11acda579
--- /dev/null
+++ b/static/directives/multiselect-dropdown.html
@@ -0,0 +1,31 @@
+
+
+
+
+ (No {{ itemName }}s selected)
+
+
+
+
+
+
\ No newline at end of file
diff --git a/static/directives/new-header-bar.html b/static/directives/new-header-bar.html
index 4768f8222..36be8e58e 100644
--- a/static/directives/new-header-bar.html
+++ b/static/directives/new-header-bar.html
@@ -108,8 +108,8 @@
data-container="body" data-animation="am-slide-right" bs-aside>
+
-
@@ -185,7 +185,7 @@
{{ result.namespace.name }}/{{ result.name }}
-
+
diff --git a/static/directives/plan-manager.html b/static/directives/plan-manager.html
index 18d87b55a..76038668f 100644
--- a/static/directives/plan-manager.html
+++ b/static/directives/plan-manager.html
@@ -1,6 +1,6 @@
-
+
@@ -58,7 +58,7 @@
-
+
Cancel
@@ -66,14 +66,14 @@
-
+
Change
Start Free Trial
Subscribe
-
+
Cancel
diff --git a/static/directives/repo-view/repo-panel-changes.html b/static/directives/repo-view/repo-panel-changes.html
index 281fff6bb..094df41b0 100644
--- a/static/directives/repo-view/repo-panel-changes.html
+++ b/static/directives/repo-view/repo-panel-changes.html
@@ -1,23 +1,24 @@
+
+
No tags selected to view
- Please select one or more tags in the Tags tab to visualize.
+ Please select one or more tags above.
-
-
-
+
diff --git a/static/directives/repo-view/repo-panel-info.html b/static/directives/repo-view/repo-panel-info.html
index 3cef46919..5fdab5a4c 100644
--- a/static/directives/repo-view/repo-panel-info.html
+++ b/static/directives/repo-view/repo-panel-info.html
@@ -6,12 +6,12 @@
Repo Pulls
-
{{ repository.stats.pulls.today }}
+
{{ repository.stats.pulls.today | abbreviated }}
Last 24 hours
-
{{ repository.stats.pulls.thirty_day }}
+
{{ repository.stats.pulls.thirty_day | abbreviated }}
Last 30 days
@@ -21,12 +21,12 @@
Repo Pushes
-
{{ repository.stats.pushes.today }}
+
{{ repository.stats.pushes.today | abbreviated }}
Last 24 hours
-
{{ repository.stats.pushes.thirty_day }}
+
{{ repository.stats.pushes.thirty_day | abbreviated }}
Last 30 days
diff --git a/static/directives/repo-view/repo-panel-tags.html b/static/directives/repo-view/repo-panel-tags.html
index cde9744ca..a95318ba2 100644
--- a/static/directives/repo-view/repo-panel-tags.html
+++ b/static/directives/repo-view/repo-panel-tags.html
@@ -133,12 +133,12 @@
diff --git a/static/directives/robots-manager.html b/static/directives/robots-manager.html
index 2e71ac142..345d28fc3 100644
--- a/static/directives/robots-manager.html
+++ b/static/directives/robots-manager.html
@@ -65,7 +65,7 @@
-
+
@@ -78,7 +78,7 @@
Direct Permissions on
-
repository
@@ -96,7 +96,7 @@
View Credentials
-
+
Delete Robot {{ robotInfo.name }}
diff --git a/static/js/directives/filters/abbreviated.js b/static/js/directives/filters/abbreviated.js
new file mode 100644
index 000000000..bf95c2491
--- /dev/null
+++ b/static/js/directives/filters/abbreviated.js
@@ -0,0 +1,26 @@
+/**
+ * Filter which displays numbers with suffixes.
+ *
+ * Based on: https://gist.github.com/pedrorocha-net/9aa21d5f34d9cc15d18f
+ */
+angular.module('quay').filter('abbreviated', function() {
+ return function(number) {
+ if (number >= 10000000) {
+ return (number / 1000000).toFixed(0) + 'M'
+ }
+
+ if (number >= 1000000) {
+ return (number / 1000000).toFixed(1) + 'M'
+ }
+
+ if (number >= 10000) {
+ return (number / 1000).toFixed(0) + 'K'
+ }
+
+ if (number >= 1000) {
+ return (number / 1000).toFixed(1) + 'K'
+ }
+
+ return number
+ }
+});
diff --git a/static/js/directives/ng-transcope.js b/static/js/directives/ng-transcope.js
new file mode 100644
index 000000000..795256e8b
--- /dev/null
+++ b/static/js/directives/ng-transcope.js
@@ -0,0 +1,25 @@
+/**
+ * Directive to transclude a template under an ng-repeat. From: http://stackoverflow.com/a/24512435
+ */
+angular.module('quay').directive('ngTranscope', function() {
+ return {
+ link: function( $scope, $element, $attrs, controller, $transclude ) {
+ if ( !$transclude ) {
+ throw minErr( 'ngTranscope' )( 'orphan',
+ 'Illegal use of ngTransclude directive in the template! ' +
+ 'No parent directive that requires a transclusion found. ' +
+ 'Element: {0}',
+ startingTag( $element ));
+ }
+ var innerScope = $scope.$new();
+
+ $transclude( innerScope, function( clone ) {
+ $element.empty();
+ $element.append( clone );
+ $element.on( '$destroy', function() {
+ innerScope.$destroy();
+ });
+ });
+ }
+ };
+});
diff --git a/static/js/directives/repo-view/repo-panel-builds.js b/static/js/directives/repo-view/repo-panel-builds.js
index 00d13a765..5e7342be2 100644
--- a/static/js/directives/repo-view/repo-panel-builds.js
+++ b/static/js/directives/repo-view/repo-panel-builds.js
@@ -57,14 +57,16 @@ angular.module('quay').directive('repoPanelBuilds', function () {
$scope.fullBuilds = orderBy(unordered, $scope.options.predicate, $scope.options.reverse);
};
- var loadBuilds = function() {
+ var loadBuilds = function(opt_forcerefresh) {
if (!$scope.builds || !$scope.repository || !$scope.options.filter) {
return;
}
// Note: We only refresh if the filter has changed.
var filter = $scope.options.filter;
- if ($scope.buildsResource && filter == $scope.currentFilter) { return; }
+ if ($scope.buildsResource && filter == $scope.currentFilter && !opt_forcerefresh) {
+ return;
+ }
var since = null;
var limit = 10;
@@ -104,17 +106,30 @@ angular.module('quay').directive('repoPanelBuilds', function () {
}
// Replace any build records with updated records from the server.
+ var requireReload = false;
$scope.builds.map(function(build) {
+ var found = false;
for (var i = 0; i < $scope.allBuilds.length; ++i) {
var current = $scope.allBuilds[i];
if (current.id == build.id && current.phase != build.phase) {
$scope.allBuilds[i] = build;
- break
+ found = true;
+ break;
}
}
+
+ // If the build was not found, then a new build has started. Reload
+ // the builds list.
+ if (!found) {
+ requireReload = true;
+ }
});
- updateBuilds();
+ if (requireReload) {
+ loadBuilds(/* force refresh */true);
+ } else {
+ updateBuilds();
+ }
};
var loadBuildTriggers = function() {
diff --git a/static/js/directives/repo-view/repo-panel-changes.js b/static/js/directives/repo-view/repo-panel-changes.js
index 7b5debe37..585d03c51 100644
--- a/static/js/directives/repo-view/repo-panel-changes.js
+++ b/static/js/directives/repo-view/repo-panel-changes.js
@@ -96,20 +96,24 @@ angular.module('quay').directive('repoPanelChanges', function () {
'isEnabled': '=isEnabled'
},
controller: function($scope, $element, $timeout, ApiService, UtilService, ImageMetadataService) {
+ $scope.tagNames = [];
var update = function() {
- if (!$scope.repository || !$scope.selectedTags) { return; }
+ if (!$scope.repository || !$scope.isEnabled) { return; }
+ $scope.tagNames = Object.keys($scope.repository.tags);
$scope.currentImage = null;
$scope.currentTag = null;
- if (!$scope.tracker) {
+ if ($scope.tracker) {
+ refreshTree();
+ } else {
updateImages();
}
};
var updateImages = function() {
- if (!$scope.repository || !$scope.images) { return; }
+ if (!$scope.repository || !$scope.images || !$scope.isEnabled) { return; }
$scope.tracker = new RepositoryImageTracker($scope.repository, $scope.images);
@@ -120,16 +124,17 @@ angular.module('quay').directive('repoPanelChanges', function () {
$scope.$watch('selectedTags', update)
$scope.$watch('repository', update);
+ $scope.$watch('isEnabled', update);
+
$scope.$watch('images', updateImages);
- $scope.$watch('isEnabled', function(isEnabled) {
- if (isEnabled) {
- refreshTree();
- }
- });
+ $scope.updateState = function() {
+ update();
+ };
var refreshTree = function() {
- if (!$scope.repository || !$scope.images) { return; }
+ if (!$scope.repository || !$scope.images || !$scope.isEnabled) { return; }
+ if ($scope.selectedTags.length < 1) { return; }
$('#image-history-container').empty();
@@ -149,6 +154,7 @@ angular.module('quay').directive('repoPanelChanges', function () {
// Give enough time for the UI to be drawn before we resize the tree.
$timeout(function() {
$scope.tree.notifyResized();
+ $scope.setTag($scope.selectedTags[0]);
}, 100);
// Listen for changes to the selected tag and image in the tree.
diff --git a/static/js/directives/ui/multiselect-dropdown.js b/static/js/directives/ui/multiselect-dropdown.js
new file mode 100644
index 000000000..d87629f8f
--- /dev/null
+++ b/static/js/directives/ui/multiselect-dropdown.js
@@ -0,0 +1,35 @@
+/**
+ * An element which displays a dropdown for selecting multiple elements.
+ */
+angular.module('quay').directive('multiselectDropdown', function ($compile) {
+ var directiveDefinitionObject = {
+ priority: 0,
+ templateUrl: '/static/directives/multiselect-dropdown.html',
+ transclude: true,
+ replace: false,
+ restrict: 'C',
+ scope: {
+ 'items': '=items',
+ 'selectedItems': '=selectedItems',
+ 'itemName': '@itemName',
+ 'itemChecked': '&itemChecked'
+ },
+ controller: function($scope, $element) {
+ $scope.isChecked = function(checked, item) {
+ return checked.indexOf(item) >= 0;
+ };
+
+ $scope.toggleItem = function(item) {
+ var isChecked = $scope.isChecked($scope.selectedItems, item);
+ if (!isChecked) {
+ $scope.selectedItems.push(item);
+ } else {
+ var index = $scope.selectedItems.indexOf(item);
+ $scope.selectedItems.splice(index, 1);
+ }
+ $scope.itemChecked({'item': item, 'checked': !isChecked});
+ };
+ }
+ };
+ return directiveDefinitionObject;
+});
\ No newline at end of file
diff --git a/static/js/directives/ui/robots-manager.js b/static/js/directives/ui/robots-manager.js
index 8cacfa687..ca672c4be 100644
--- a/static/js/directives/ui/robots-manager.js
+++ b/static/js/directives/ui/robots-manager.js
@@ -128,6 +128,15 @@ angular.module('quay').directive('robotsManager', function () {
}, ApiService.errorDisplay('Cannot delete robot account'));
};
+
+ $scope.askDeleteRobot = function(info) {
+ bootbox.confirm('Are you sure you want to delete robot ' + info.name + '?', function(resp) {
+ if (resp) {
+ $scope.deleteRobot(info);
+ }
+ });
+ };
+
var update = function() {
if (!$scope.user && !$scope.organization) { return; }
if ($scope.loading || !$scope.isEnabled) { return; }
diff --git a/static/js/services/notification-service.js b/static/js/services/notification-service.js
index 235083baa..ade256a63 100644
--- a/static/js/services/notification-service.js
+++ b/static/js/services/notification-service.js
@@ -56,6 +56,16 @@ function($rootScope, $interval, UserService, ApiService, StringBuilderService, P
' Please upgrade your plan to avoid disruptions in service.',
'page': function(metadata) {
var organization = UserService.getOrganization(metadata['namespace']);
+
+ // TODO(jschorr): Remove once the new layout is in prod.
+ if (Config.isNewLayout()) {
+ if (organization) {
+ return '/organization/' + metadata['namespace'] + '?tab=billing';
+ } else {
+ return '/user/' + metadata['namespace'] + '?tab=billing';
+ }
+ }
+
if (organization) {
return '/organization/' + metadata['namespace'] + '/admin';
} else {
diff --git a/static/lib/dropdowns-enhancement.js b/static/lib/dropdowns-enhancement.js
new file mode 100644
index 000000000..3b885dc7d
--- /dev/null
+++ b/static/lib/dropdowns-enhancement.js
@@ -0,0 +1,267 @@
+/* ========================================================================
+ * Bootstrap Dropdowns Enhancement: dropdowns-enhancement.js v3.1.1 (Beta 1)
+ * http://behigh.github.io/bootstrap_dropdowns_enhancement/
+ * ========================================================================
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
+ * ======================================================================== */
+
+(function($) {
+ "use strict";
+
+ var toggle = '[data-toggle="dropdown"]',
+ disabled = '.disabled, :disabled',
+ backdrop = '.dropdown-backdrop',
+ menuClass = 'dropdown-menu',
+ subMenuClass = 'dropdown-submenu',
+ namespace = '.bs.dropdown.data-api',
+ eventNamespace = '.bs.dropdown',
+ openClass = 'open',
+ touchSupport = 'ontouchstart' in document.documentElement,
+ opened;
+
+
+ function Dropdown(element) {
+ $(element).on('click' + eventNamespace, this.toggle)
+ }
+
+ var proto = Dropdown.prototype;
+
+ proto.toggle = function(event) {
+ var $element = $(this);
+
+ if ($element.is(disabled)) return;
+
+ var $parent = getParent($element);
+ var isActive = $parent.hasClass(openClass);
+ var isSubMenu = $parent.hasClass(subMenuClass);
+ var menuTree = isSubMenu ? getSubMenuParents($parent) : null;
+
+ closeOpened(event, menuTree);
+
+ if (!isActive) {
+ if (!menuTree)
+ menuTree = [$parent];
+
+ if (touchSupport && !$parent.closest('.navbar-nav').length && !menuTree[0].find(backdrop).length) {
+ // if mobile we use a backdrop because click events don't delegate
+ $('
').appendTo(menuTree[0]).on('click', closeOpened)
+ }
+
+ for (var i = 0, s = menuTree.length; i < s; i++) {
+ if (!menuTree[i].hasClass(openClass)) {
+ menuTree[i].addClass(openClass);
+ positioning(menuTree[i].children('.' + menuClass), menuTree[i]);
+ }
+ }
+ opened = menuTree[0];
+ }
+
+ return false;
+ };
+
+ proto.keydown = function (e) {
+ if (!/(38|40|27)/.test(e.keyCode)) return;
+
+ var $this = $(this);
+
+ e.preventDefault();
+ e.stopPropagation();
+
+ if ($this.is('.disabled, :disabled')) return;
+
+ var $parent = getParent($this);
+ var isActive = $parent.hasClass('open');
+
+ if (!isActive || (isActive && e.keyCode == 27)) {
+ if (e.which == 27) $parent.find(toggle).trigger('focus');
+ return $this.trigger('click')
+ }
+
+ var desc = ' li:not(.divider):visible a';
+ var desc1 = 'li:not(.divider):visible > input:not(disabled) ~ label';
+ var $items = $parent.find(desc1 + ', ' + '[role="menu"]' + desc + ', [role="listbox"]' + desc);
+
+ if (!$items.length) return;
+
+ var index = $items.index($items.filter(':focus'));
+
+ if (e.keyCode == 38 && index > 0) index--; // up
+ if (e.keyCode == 40 && index < $items.length - 1) index++; // down
+ if (!~index) index = 0;
+
+ $items.eq(index).trigger('focus')
+ };
+
+ proto.change = function (e) {
+
+ var
+ $parent,
+ $menu,
+ $toggle,
+ selector,
+ text = '',
+ $items;
+
+ $menu = $(this).closest('.' + menuClass);
+
+ $toggle = $menu.parent().find('[data-label-placement]');
+
+ if (!$toggle || !$toggle.length) {
+ $toggle = $menu.parent().find(toggle);
+ }
+
+ if (!$toggle || !$toggle.length || $toggle.data('placeholder') === false)
+ return; // do nothing, no control
+
+ ($toggle.data('placeholder') == undefined && $toggle.data('placeholder', $.trim($toggle.text())));
+ text = $.data($toggle[0], 'placeholder');
+
+ $items = $menu.find('li > input:checked');
+
+ if ($items.length) {
+ text = [];
+ $items.each(function () {
+ var str = $(this).parent().find('label').eq(0),
+ label = str.find('.data-label');
+
+ if (label.length) {
+ var p = $('
');
+ p.append(label.clone());
+ str = p.html();
+ }
+ else {
+ str = str.html();
+ }
+
+
+ str && text.push($.trim(str));
+ });
+
+ text = text.length < 4 ? text.join(', ') : text.length + ' selected';
+ }
+
+ var caret = $toggle.find('.caret');
+
+ $toggle.html(text || ' ');
+ if (caret.length)
+ $toggle.append(' ') && caret.appendTo($toggle);
+
+ };
+
+ function positioning($menu, $control) {
+ if ($menu.hasClass('pull-center')) {
+ $menu.css('margin-right', $menu.outerWidth() / -2);
+ }
+
+ if ($menu.hasClass('pull-middle')) {
+ $menu.css('margin-top', ($menu.outerHeight() / -2) - ($control.outerHeight() / 2));
+ }
+ }
+
+ function closeOpened(event, menuTree) {
+ if (opened) {
+
+ if (!menuTree) {
+ menuTree = [opened];
+ }
+
+ var parent;
+
+ if (opened[0] !== menuTree[0][0]) {
+ parent = opened;
+ } else {
+ parent = menuTree[menuTree.length - 1];
+ if (parent.parent().hasClass(menuClass)) {
+ parent = parent.parent();
+ }
+ }
+
+ parent.find('.' + openClass).removeClass(openClass);
+
+ if (parent.hasClass(openClass))
+ parent.removeClass(openClass);
+
+ if (parent === opened) {
+ opened = null;
+ $(backdrop).remove();
+ }
+ }
+ }
+
+ function getSubMenuParents($submenu) {
+ var result = [$submenu];
+ var $parent;
+ while (!$parent || $parent.hasClass(subMenuClass)) {
+ $parent = ($parent || $submenu).parent();
+ if ($parent.hasClass(menuClass)) {
+ $parent = $parent.parent();
+ }
+ if ($parent.children(toggle)) {
+ result.unshift($parent);
+ }
+ }
+ return result;
+ }
+
+ function getParent($this) {
+ var selector = $this.attr('data-target');
+
+ if (!selector) {
+ selector = $this.attr('href');
+ selector = selector && /#[A-Za-z]/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, ''); //strip for ie7
+ }
+
+ var $parent = selector && $(selector);
+
+ return $parent && $parent.length ? $parent : $this.parent()
+ }
+
+ // DROPDOWN PLUGIN DEFINITION
+ // ==========================
+
+ var old = $.fn.dropdown;
+
+ $.fn.dropdown = function (option) {
+ return this.each(function () {
+ var $this = $(this);
+ var data = $this.data('bs.dropdown');
+
+ if (!data) $this.data('bs.dropdown', (data = new Dropdown(this)));
+ if (typeof option == 'string') data[option].call($this);
+ })
+ };
+
+ $.fn.dropdown.Constructor = Dropdown;
+
+ $.fn.dropdown.clearMenus = function(e) {
+ $(backdrop).remove();
+ $('.' + openClass + ' ' + toggle).each(function () {
+ var $parent = getParent($(this));
+ var relatedTarget = { relatedTarget: this };
+ if (!$parent.hasClass('open')) return;
+ $parent.trigger(e = $.Event('hide' + eventNamespace, relatedTarget));
+ if (e.isDefaultPrevented()) return;
+ $parent.removeClass('open').trigger('hidden' + eventNamespace, relatedTarget);
+ });
+ return this;
+ };
+
+
+ // DROPDOWN NO CONFLICT
+ // ====================
+
+ $.fn.dropdown.noConflict = function () {
+ $.fn.dropdown = old;
+ return this
+ };
+
+
+ $(document).off(namespace)
+ .on('click' + namespace, closeOpened)
+ .on('click' + namespace, toggle, proto.toggle)
+ .on('click' + namespace, '.dropdown-menu > li > input[type="checkbox"] ~ label, .dropdown-menu > li > input[type="checkbox"], .dropdown-menu.noclose > li', function (e) {
+ e.stopPropagation()
+ })
+ .on('change' + namespace, '.dropdown-menu > li > input[type="checkbox"], .dropdown-menu > li > input[type="radio"]', proto.change)
+ .on('keydown' + namespace, toggle + ', [role="menu"], [role="listbox"]', proto.keydown)
+}(jQuery));
\ No newline at end of file
diff --git a/static/partials/manage-application.html b/static/partials/manage-application.html
index 0fb160f7c..45fb03ad5 100644
--- a/static/partials/manage-application.html
+++ b/static/partials/manage-application.html
@@ -14,7 +14,7 @@
-
+
Warning: There is no OAuth Redirect setup for this application. Please enter it in the Settings tab.
diff --git a/static/partials/new-repo.html b/static/partials/new-repo.html
index c2a01ce34..5f3a7d57a 100644
--- a/static/partials/new-repo.html
+++ b/static/partials/new-repo.html
@@ -90,9 +90,9 @@
- In order to make this repository private
-
under your personal namespace
-
under the organization {{ repo.namespace }} , you will need to upgrade your plan to
+ In order to make this repository private under
+
your personal namespace
+
organization {{ repo.namespace }} , you will need to upgrade your plan to
{{ planRequired.title }}
@@ -102,10 +102,10 @@
Upgrade now
or did you mean to create this repository
under {{ user.organizations[0].name }} ?
-
+
-
+
diff --git a/static/partials/org-view.html b/static/partials/org-view.html
index cbaf77e57..5bca6aabb 100644
--- a/static/partials/org-view.html
+++ b/static/partials/org-view.html
@@ -8,6 +8,12 @@
{{ organization.name }}
+
+
+
+ Create New Repository
+
+
diff --git a/static/partials/repo-view.html b/static/partials/repo-view.html
index 827df6146..b2dcee57c 100644
--- a/static/partials/repo-view.html
+++ b/static/partials/repo-view.html
@@ -11,7 +11,7 @@
{{ namespace }} / {{ name }}
-
+
diff --git a/static/partials/user-view.html b/static/partials/user-view.html
index a4c8a56e5..29b4e9d9b 100644
--- a/static/partials/user-view.html
+++ b/static/partials/user-view.html
@@ -8,6 +8,12 @@
{{ viewuser.username }}
+
+
+
+ Create New Repository
+
+
diff --git a/templates/oauthorize.html b/templates/oauthorize.html
index 0997f1e1b..42aa44e08 100644
--- a/templates/oauthorize.html
+++ b/templates/oauthorize.html
@@ -13,10 +13,10 @@