Merge remote-tracking branch 'origin/master' into tagyourit

Conflicts:
	static/css/quay.css
	static/js/graphing.js
	static/partials/view-repo.html
	test/data/test.db
This commit is contained in:
jakedt 2014-04-15 15:58:30 -04:00
commit 3f42d15335
132 changed files with 4266 additions and 1924 deletions

View file

@ -2445,11 +2445,6 @@ p.editable:hover i {
margin-bottom: 10px;
}
.user-admin .form-change input {
margin-top: 12px;
margin-bottom: 12px;
}
.user-admin .convert-form h3 {
margin-bottom: 20px;
}
@ -2540,7 +2535,7 @@ p.editable:hover i {
text-align: center;
}
#image-history-container .tags .tag, .tag-specific-images-view .tag {
.tags .tag, .tag-specific-images-view .tag {
display: inline-block;
border-radius: 10px;
margin-right: 4px;
@ -2549,6 +2544,13 @@ p.editable:hover i {
text-overflow: ellipsis;
}
.tooltip-tags {
display: block;
margin-top: 10px;
border-top: 1px dotted #aaa;
padding-top: 10px;
}
#changes-tree-container {
overflow: hidden;
}
@ -2589,42 +2591,42 @@ p.editable:hover i {
stroke-width: 1.5px;
}
#repository-usage-chart {
.usage-chart {
display: inline-block;
vertical-align: middle;
width: 200px;
height: 200px;
}
#repository-usage-chart .count-text {
.usage-chart .count-text {
font-size: 22px;
}
#repository-usage-chart.limit-at path.arc-0 {
.usage-chart.limit-at path.arc-0 {
fill: #c09853;
}
#repository-usage-chart.limit-over path.arc-0 {
.usage-chart.limit-over path.arc-0 {
fill: #b94a48;
}
#repository-usage-chart.limit-near path.arc-0 {
.usage-chart.limit-near path.arc-0 {
fill: #468847;
}
#repository-usage-chart.limit-over path.arc-1 {
.usage-chart.limit-over path.arc-1 {
fill: #fcf8e3;
}
#repository-usage-chart.limit-at path.arc-1 {
.usage-chart.limit-at path.arc-1 {
fill: #f2dede;
}
#repository-usage-chart.limit-near path.arc-1 {
.usage-chart.limit-near path.arc-1 {
fill: #dff0d8;
}
.plan-manager-element .usage-caption {
.usage-caption {
display: inline-block;
color: #aaa;
font-size: 26px;
@ -3588,7 +3590,7 @@ pre.command:before {
position: relative;
height: 100px;
height: 75px;
opacity: 1;
}
@ -3721,4 +3723,51 @@ pre.command:before {
.auth-info .scope {
cursor: pointer;
margin-right: 4px;
}
.trigger-pull-credentials {
margin-top: 4px;
padding-left: 26px;
font-size: 12px;
}
.trigger-pull-credentials .context-tooltip {
color: gray;
margin-right: 4px;
}
.trigger-description .trigger-description-subtitle {
display: inline-block;
margin-right: 34px;
}
.trigger-option-section:not(:first-child) {
border-top: 1px solid #eee;
padding-top: 16px;
margin-top: 10px;
}
.trigger-option-section .entity-search-element .twitter-typeahead {
width: 370px;
}
.trigger-option-section .entity-search-element input {
width: 100%;
}
.trigger-option-section table td {
padding: 6px;
}
.user-row.super-user td {
background-color: #d9edf7;
}
.user-row .user-class {
text-transform: uppercase;
}
.form-change input {
margin-top: 12px;
margin-bottom: 12px;
}

View file

@ -30,7 +30,7 @@
</td>
<td>
<a ng-show="invoice.paid" href="/receipt?id={{ invoice.id }}" download="receipt.pdf" target="_new">
<i class="fa fa-download" title="Download Receipt" bs-tooltip="tooltip.title"></i>
<i class="fa fa-download" data-title="Download Receipt" bs-tooltip="tooltip.title"></i>
</a>
</td>
</tr>

View file

@ -2,7 +2,7 @@
<div class="id-container">
<div class="input-group">
<input type="text" class="form-control" value="{{ value }}" readonly>
<span class="input-group-addon" title="Copy to Clipboard">
<span class="input-group-addon" data-title="Copy to Clipboard">
<i class="fa fa-copy"></i>
</span>
</div>

View file

@ -1,4 +1,4 @@
<span class="delete-ui-element" ng-click="focus()">
<span class="delete-ui-button" ng-click="performDelete()"><button class="btn btn-danger">{{ buttonTitleInternal }}</button></span>
<i class="fa fa-times" bs-tooltip="tooltip.title" data-placement="left" title="{{ deleteTitle }}"></i>
<i class="fa fa-times" bs-tooltip="tooltip.title" data-placement="left" data-title="{{ deleteTitle }}"></i>
</span>

View file

@ -12,7 +12,7 @@
<div class="container" ng-show="!uploading && !building">
<div class="init-description">
Upload a Dockerfile or a zip file containing a Dockerfile <b>in the root directory</b>
Upload a <b>Dockerfile</b> or an archive (<code>.zip</code> or <code>.tar.gz</code>) containing a Dockerfile <b>in the root directory</b>
</div>
<input id="file-drop" class="file-drop" type="file" file-present="internal.hasDockerfile">
</div>

View file

@ -1,14 +1,14 @@
<span class="entity-reference-element">
<span ng-if="entity.kind == 'team'">
<i class="fa fa-group" title="Team" bs-tooltip="tooltip.title" data-container="body"></i>
<i class="fa fa-group" data-title="Team" bs-tooltip="tooltip.title" data-container="body"></i>
<span class="entity-name">
<span ng-if="!getIsAdmin(namespace)">{{entity.name}}</span>
<span ng-if="getIsAdmin(namespace)"><a href="/organization/{{ namespace }}/teams/{{ entity.name }}">{{entity.name}}</a></span>
</span>
</span>
<span ng-if="entity.kind != 'team'">
<i class="fa fa-user" ng-show="!entity.is_robot" title="User" bs-tooltip="tooltip.title" data-container="body"></i>
<i class="fa fa-wrench" ng-show="entity.is_robot" title="Robot Account" bs-tooltip="tooltip.title" data-container="body"></i>
<i class="fa fa-user" ng-show="!entity.is_robot" data-title="User" bs-tooltip="tooltip.title" data-container="body"></i>
<i class="fa fa-wrench" ng-show="entity.is_robot" data-title="Robot Account" bs-tooltip="tooltip.title" data-container="body"></i>
<span class="entity-name" ng-if="entity.is_robot">
<a href="{{ getRobotUrl(entity.name) }}" ng-if="getIsAdmin(getPrefix(entity.name))">
<span class="prefix">{{ getPrefix(entity.name) }}+</span><span>{{ getShortenedName(entity.name) }}</span>
@ -22,6 +22,6 @@
</span>
</span>
<i class="fa fa-exclamation-triangle" ng-if="entity.is_org_member === false"
title="This user is not a member of the organization" bs-tooltip="tooltip.title" data-container="body">
data-title="This user is not a member of the organization" bs-tooltip="tooltip.title" data-container="body">
</i>
</span>

View file

@ -1,5 +1,5 @@
<span class="entity-search-element" ng-class="isPersistent ? 'persistent' : ''"><input class="entity-search-control form-control">
<span class="entity-reference block-reference" ng-show="isPersistent && currentEntity" entity="currentEntity"></span>
<span class="entity-reference block-reference" ng-show="isPersistent && currentEntityInternal" entity="currentEntityInternal"></span>
<div class="dropdown">
<button class="btn btn-default dropdown-toggle" type="button" id="entityDropdownMenu" data-toggle="dropdown"
ng-click="lazyLoad()">

View file

@ -14,7 +14,7 @@
<li><a ng-href="/repository/" target="{{ appLinkTarget() }}">Repositories</a></li>
<li><a href="http://docs.quay.io/" target="_blank">Docs</a></li>
<li><a ng-href="/tutorial/" target="{{ appLinkTarget() }}">Tutorial</a></li>
<li><a ng-href="/plans/" target="{{ appLinkTarget() }}">Pricing</a></li>
<li><a ng-href="/plans/" target="{{ appLinkTarget() }}" quay-require="['BILLING']">Pricing</a></li>
<li><a ng-href="/organizations/" target="{{ appLinkTarget() }}">Organizations</a></li>
</ul>
@ -28,7 +28,7 @@
</form>
<span class="navbar-left user-tools" ng-show="!user.anonymous">
<a href="/new/"><i class="fa fa-upload user-tool" bs-tooltip="tooltip.title" data-placement="bottom" title="Create new repository"></i></a>
<a href="/new/"><i class="fa fa-upload user-tool" bs-tooltip="tooltip.title" data-placement="bottom" data-title="Create new repository"></i></a>
</span>
</li>
@ -40,7 +40,7 @@
ng-show="notificationService.notifications.length"
ng-class="notificationService.notificationClasses"
bs-tooltip=""
title="User Notifications"
data-title="User Notifications"
data-placement="left"
data-container="body">
{{ notificationService.notifications.length }}
@ -65,6 +65,7 @@
</a>
</li>
<li><a ng-href="/organizations/" target="{{ appLinkTarget() }}">Organizations</a></li>
<li ng-if="user.super_user"><a href="/superuser/"><strong>Super User Admin Panel</strong></a></li>
<li><a href="javascript:void(0)" ng-click="signout()">Sign out</a></li>
</ul>
</li>

View file

@ -13,9 +13,9 @@
</span>
<span class="right">
<i class="fa fa-bar-chart-o toggle-icon" ng-class="chartVisible ? 'active' : ''"
ng-click="toggleChart()" title="Toggle Chart" bs-tooltip="tooltip.title"></i>
ng-click="toggleChart()" data-title="Toggle Chart" bs-tooltip="tooltip.title"></i>
<a href="{{ logsPath }}" download="usage-log.json" target="_new">
<i class="fa fa-download toggle-icon" title="Download Logs" bs-tooltip="tooltip.title"></i>
<i class="fa fa-download toggle-icon" data-title="Download Logs" bs-tooltip="tooltip.title"></i>
</a>
</span>
</div>
@ -55,7 +55,7 @@
<td>
<span class="log-performer" ng-if="log.metadata.oauth_token_application">
<div>
<span class="application-reference" title="log.metadata.oauth_token_application"
<span class="application-reference" data-title="log.metadata.oauth_token_application"
client-id="log.metadata.oauth_token_application_id"></span>
</div>
<div style="text-align: center; font-size: 12px; color: #aaa; padding: 4px;">on behalf of</div>

View file

@ -18,7 +18,7 @@
</a>
<i class="fa fa-exclamation-triangle" ng-show="requireCreate && !namespaces[org.name].can_create_repo"
title="You do not have permission to create repositories for this organization"
data-title="You do not have permission to create repositories for this organization"
data-placement="right"
bs-tooltip="tooltip.title"></i>
</li>

View file

@ -20,7 +20,7 @@
<!-- Chart -->
<div>
<div id="repository-usage-chart" class="limit-{{limit}}"></div>
<div id="repository-usage-chart" class="usage-chart limit-{{limit}}"></div>
<span class="usage-caption" ng-show="chart">Repository Usage</span>
</div>
@ -38,7 +38,7 @@
<td>
{{ plan.title }}
<div class="deprecated-plan-label" ng-show="plan.deprecated">
<span class="context-tooltip" title="This plan has been discontinued. As a valued early adopter, you may continue to stay on this plan indefinitely." bs-tooltip="tooltip.title" data-placement="right">Discontinued Plan</span>
<span class="context-tooltip" data-title="This plan has been discontinued. As a valued early adopter, you may continue to stay on this plan indefinitely." bs-tooltip="tooltip.title" data-placement="right">Discontinued Plan</span>
</div>
</td>
<td>{{ plan.privateRepos }}</td>

View file

@ -3,7 +3,7 @@
<div class="container" ng-show="!loading">
<div class="alert alert-info">
Default permissions provide a means of specifying <span class="context-tooltip" title="By default, all repositories have the creating user added as an 'Admin'" bs-tooltip="tooltip.title">additional</span> permissions that should be granted automatically to a repository.
Default permissions provide a means of specifying <span class="context-tooltip" data-title="By default, all repositories have the creating user added as an 'Admin'" bs-tooltip="tooltip.title">additional</span> permissions that should be granted automatically to a repository.
</div>
<div class="side-controls">
@ -17,13 +17,13 @@
<thead>
<th>
<span class="context-tooltip"
title="The user or robot that is creating a repository. If '(Organization Default)', then any repository created in this organization will be granted the permission."
data-title="The user or robot that is creating a repository. If '(Organization Default)', then any repository created in this organization will be granted the permission."
bs-tooltip="tooltip.title" data-container="body">
Repository Creator
</span>
</th>
<th>
<span class="context-tooltip" title="The user, robot or team that is being granted the permission"
<span class="context-tooltip" data-title="The user, robot or team that is being granted the permission"
bs-tooltip="tooltip.title" data-container="body">
Applies To User/Robot/Team
</span>

View file

@ -1,2 +1,2 @@
<i class="fa fa-lock fa-lg" style="{{ repo.is_public ? 'visibility: hidden' : 'visibility: inherit' }}" title="Private Repository"></i>
<i class="fa fa-lock fa-lg" style="{{ repo.is_public ? 'visibility: hidden' : 'visibility: inherit' }}" data-title="Private Repository"></i>
<i class="fa fa-hdd-o"></i>

View file

@ -0,0 +1,100 @@
<div class="setup-trigger-directive-element">
<!-- Modal message dialog -->
<div class="modal fade" id="setupTriggerModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">Setup new build trigger</h4>
</div>
<div class="modal-body">
<!-- Trigger-specific setup -->
<div class="trigger-description-element trigger-option-section" ng-switch on="trigger.service">
<div ng-switch-when="github">
<div class="trigger-setup-github" repository="repository" trigger="trigger"
analyze="checkAnalyze(isValid)"></div>
</div>
</div>
<!-- Pull information -->
<div class="trigger-option-section" ng-show="showPullRequirements">
<div ng-show="!pullRequirements">
<span class="quay-spinner"></span> Checking pull credential requirements...
</div>
<div ng-show="pullRequirements">
<div class="alert alert-danger" ng-if="pullRequirements.status == 'error'">
{{ pullRequirements.message }}
</div>
<div class="alert alert-warning" ng-if="pullRequirements.status == 'warning'">
{{ pullRequirements.message }}
</div>
<div class="alert alert-success" ng-if="pullRequirements.status == 'analyzed' && pullRequirements.is_public === false">
The
<a href="{{ pullRequirements.dockerfile_url }}" ng-if="pullRequirements.dockerfile_url" target="_blank">Dockerfile found</a>
<span ng-if="!pullRequirements.dockerfile_url">Dockerfile found</span>
depends on repository
<a href="/repository/{{ pullRequirements.namespace }}/{{ pullRequirements.name }}" target="_blank">
{{ pullRequirements.namespace }}/{{ pullRequirements.name }}
</a> which requires
a robot account for pull access, because it is marked <strong>private</strong>.
</div>
</div>
<table style="width: 100%;" ng-show="pullRequirements">
<tr>
<td style="width: 114px">
<div class="context-tooltip" data-title="The credentials used by the builder when pulling images" bs-tooltip>
Pull Credentials:
</div>
</td>
<td>
<div ng-if="!isNamespaceAdmin(repository.namespace)" style="color: #aaa;">
In order to set pull credentials for a build trigger, you must be an Administrator of the namespace <strong>{{ repository.namespace }}</strong>
</div>
<div class="btn-group btn-group-sm" ng-if="isNamespaceAdmin(repository.namespace)">
<button type="button" class="btn btn-default"
ng-class="publicPull ? 'active btn-info' : ''" ng-click="setPublicPull(true)">Public</button>
<button type="button" class="btn btn-default"
ng-class="publicPull ? '' : 'active btn-info'" ng-click="setPublicPull(false)">
<i class="fa fa-wrench"></i>
Robot account
</button>
</div>
</td>
</tr>
<tr ng-show="!publicPull">
<td>
</td>
<td>
<div class="entity-search" namespace="repository.namespace" include-teams="false"
input-title="'Select robot account for pulling...'"
is-organization="repository.is_organization"
is-persistent="true"
current-entity="pullEntity"
filter="['robot']"></div>
<div class="alert alert-info" ng-if="pullRequirements.robots.length" style="margin-top: 20px; margin-bottom: 0px;">
Note: We've automatically selected robot account <span class="entity-reference" entity="pullRequirements.robots[0]"></span>, since it has access to the repository.
</div>
<div class="alert alert-warning" ng-if="!pullRequirements.robots.length" style="margin-top: 20px; margin-bottom: 0px;">
Note: No robot account currently has access to the repository. Please create one and/or assign access in the
<a href="/repository/{{ pullRequirements.namespace }}/{{ pullRequirements.name }}/admin" target="_blank">repository's admin panel</a>.
</div>
</td>
</tr>
</table>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary"
ng-disabled="!trigger.$ready || (!publicPull && !pullEntity) || checkingPullRequirements"
ng-click="activate">Finished</button>
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
</div>

View file

@ -6,12 +6,13 @@
placeholder="Password" ng-model="user.password">
<button class="btn btn-lg btn-primary btn-block" type="submit">Sign In</button>
<span class="social-alternate">
<span class="social-alternate" quay-require="['GITHUB_LOGIN']">
<i class="fa fa-circle"></i>
<span class="inner-text">OR</span>
</span>
<a id="github-signin-link" class="btn btn-primary btn-lg btn-block" href="javascript:void(0)" ng-click="showGithub()">
<a id="github-signin-link" class="btn btn-primary btn-lg btn-block" href="javascript:void(0)" ng-click="showGithub()"
quay-require="['GITHUB_LOGIN']">
<i class="fa fa-github fa-lg"></i> Sign In with GitHub
</a>
</form>

View file

@ -1,21 +1,28 @@
<div class="signup-form-element">
<form class="form-signup" name="signupForm" ng-submit="register()" data-trigger="manual"
data-content="{{ registerError }}" data-placement="left" ng-show="!awaitingConfirmation && !registering">
<input type="text" class="form-control" placeholder="Create a username" name="username" ng-model="newUser.username" autofocus required>
<form class="form-signup" name="signupForm" ng-submit="register()" ngshow="!awaitingConfirmation && !registering">
<input type="text" class="form-control" placeholder="Create a username" name="username" ng-model="newUser.username" autofocus required ng-pattern="/^[a-z0-9_]{4,30}$/">
<input type="email" class="form-control" placeholder="Email address" ng-model="newUser.email" required>
<input type="password" class="form-control" placeholder="Create a password" ng-model="newUser.password" required>
<input type="password" class="form-control" placeholder="Create a password" ng-model="newUser.password" required
ng-pattern="/^.{8,}$/">
<input type="password" class="form-control" placeholder="Verify your password" ng-model="newUser.repeatPassword"
match="newUser.password" required>
match="newUser.password" required
ng-pattern="/^.{8,}$/">
<div class="form-group signin-buttons">
<button class="btn btn-primary btn-block landing-signup-button" ng-disabled="signupForm.$invalid" type="submit"
analytics-on analytics-event="register">Sign Up for Free!</button>
<span class="social-alternate">
<button id="signupButton"
class="btn btn-primary btn-block landing-signup-button" ng-disabled="signupForm.$invalid" type="submit"
analytics-on analytics-event="register">
<span quay-show="Features.BILLING">Sign Up for Free!</span>
<span quay-show="!Features.BILLING">Sign Up</span>
</button>
<span class="social-alternate" quay-require="['GITHUB_LOGIN']">
<i class="fa fa-circle"></i>
<span class="inner-text">OR</span>
</span>
<a href="https://github.com/login/oauth/authorize?client_id={{ githubClientId }}&scope=user:email{{ github_state_clause }}"
class="btn btn-primary btn-block"><i class="fa fa-github fa-lg"></i> Sign In with GitHub</a>
<p class="help-block">No credit card required.</p>
class="btn btn-primary btn-block" quay-require="['GITHUB_LOGIN']">
<i class="fa fa-github fa-lg"></i> Sign In with GitHub
</a>
<p class="help-block" quay-require="['BILLING']">No credit card required.</p>
</div>
</form>
<div ng-show="registering" style="text-align: center">

View file

@ -5,7 +5,7 @@
ng-class="getImageListingClasses(image)">
<span class="image-listing-circle"></span>
<span class="image-listing-line"></span>
<span class="context-tooltip image-listing-id" bs-tooltip="" title="getFirstTextLine(image.comment)"
<span class="context-tooltip image-listing-id" bs-tooltip="" data-title="getFirstTextLine(image.comment)"
data-html="true">
{{ image.id.substr(0, 12) }}
</span>

View file

@ -1,6 +1,6 @@
<span class="trigger-description-element" ng-switch on="trigger.service">
<span ng-switch-when="github">
<i class="fa fa-github fa-lg" style="margin-right: 6px" title="GitHub" bs-tooltip="tooltip.title"></i>
<i class="fa fa-github fa-lg" style="margin-right: 6px" data-title="GitHub" bs-tooltip="tooltip.title"></i>
Push to GitHub repository <a href="https://github.com/{{ trigger.config.build_source }}" target="_new">{{ trigger.config.build_source }}</a>
<div style="margin-top: 4px; margin-left: 26px; font-size: 12px; color: gray;" ng-if="trigger.config.subdir">
<span>Dockerfile:
@ -10,7 +10,7 @@
</span>
</div>
<div style="margin-top: 4px; margin-left: 26px; font-size: 12px; color: gray;" ng-if="!trigger.config.subdir && !short">
<span>Dockerfile:
<span><span class="trigger-description-subtitle">Dockerfile:</span>
<a href="https://github.com/{{ trigger.config.build_source }}/tree/{{ trigger.config.master_branch || 'master' }}/Dockerfile" target="_blank">
//Dockerfile
</a>

View file

@ -102,7 +102,17 @@ function getMarkedDown(string) {
return Markdown.getSanitizingConverter().makeHtml(string || '');
}
quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angular-tour', 'restangular', 'angularMoment', 'angulartics', /*'angulartics.google.analytics',*/ 'angulartics.mixpanel', 'mgcrea.ngStrap', 'ngCookies', 'ngSanitize', 'angular-md5', 'pasvaz.bindonce', 'ansiToHtml', 'ngAnimate'], function($provide, cfpLoadingBarProvider) {
quayDependencies = ['ngRoute', 'chieffancypants.loadingBar', 'angular-tour', 'restangular', 'angularMoment',
'mgcrea.ngStrap', 'ngCookies', 'ngSanitize', 'angular-md5', 'pasvaz.bindonce', 'ansiToHtml',
'ngAnimate'];
if (window.__config && window.__config.MIXPANEL_KEY) {
quayDependencies.push('angulartics');
quayDependencies.push('angulartics.mixpanel');
}
quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoadingBarProvider) {
cfpLoadingBarProvider.includeSpinner = false;
/**
@ -325,6 +335,42 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
}]);
$provide.factory('UIService', [function() {
var uiService = {};
uiService.hidePopover = function(elem) {
var popover = $('#signupButton').data('bs.popover');
if (popover) {
popover.hide();
}
};
uiService.showPopover = function(elem, content) {
var popover = $(elem).data('bs.popover');
if (!popover) {
$(elem).popover({'content': '-', 'placement': 'left'});
}
setTimeout(function() {
var popover = $(elem).data('bs.popover');
popover.options.content = content;
popover.show();
}, 500);
};
uiService.showFormError = function(elem, result) {
var message = result.data['message'] || result.data['error_description'] || '';
if (message) {
uiService.showPopover(elem, message);
} else {
uiService.hidePopover(elem);
}
};
return uiService;
}]);
$provide.factory('UtilService', ['$sanitize', function($sanitize) {
var utilService = {};
@ -439,6 +485,63 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
return metadataService;
}]);
$provide.factory('Features', [function() {
if (!window.__features) {
return {};
}
var features = window.__features;
features.getFeature = function(name, opt_defaultValue) {
var value = features[name];
if (value == null) {
return opt_defaultValue;
}
return value;
};
features.hasFeature = function(name) {
return !!features.getFeature(name);
};
features.matchesFeatures = function(list) {
for (var i = 0; i < list.length; ++i) {
var value = features.getFeature(list[i]);
if (!value) {
return false;
}
}
return true;
};
return features;
}]);
$provide.factory('Config', [function() {
if (!window.__config) {
return {};
}
var config = window.__config;
config.getDomain = function() {
return config['SERVER_HOSTNAME'];
};
config.getUrl = function(opt_path) {
var path = opt_path || '';
return config['PREFERRED_URL_SCHEME'] + '://' + config['SERVER_HOSTNAME'] + path;
};
config.getValue = function(name, opt_defaultValue) {
var value = config[name];
if (value == null) {
return opt_defaultValue;
}
return value;
};
return config;
}]);
$provide.factory('ApiService', ['Restangular', function(Restangular) {
var apiService = {};
@ -622,8 +725,8 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
return cookieService;
}]);
$provide.factory('UserService', ['ApiService', 'CookieService', '$rootScope',
function(ApiService, CookieService, $rootScope) {
$provide.factory('UserService', ['ApiService', 'CookieService', '$rootScope', 'Config',
function(ApiService, CookieService, $rootScope, Config) {
var userResponse = {
verified: false,
anonymous: true,
@ -653,15 +756,17 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
userResponse = loadedUser;
if (!userResponse.anonymous) {
mixpanel.identify(userResponse.username);
mixpanel.people.set({
'$email': userResponse.email,
'$username': userResponse.username,
'verified': userResponse.verified
});
mixpanel.people.set_once({
'$created': new Date()
})
if (Config.MIXPANEL_KEY) {
mixpanel.identify(userResponse.username);
mixpanel.people.set({
'$email': userResponse.email,
'$username': userResponse.username,
'verified': userResponse.verified
});
mixpanel.people.set_once({
'$created': new Date()
})
}
if (window.olark !== undefined) {
olark('api.visitor.getDetails', function(details) {
@ -735,8 +840,8 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
return userService;
}]);
$provide.factory('NotificationService', ['$rootScope', '$interval', 'UserService', 'ApiService', 'StringBuilderService', 'PlanService', 'UserService',
function($rootScope, $interval, UserService, ApiService, StringBuilderService, PlanService, UserService) {
$provide.factory('NotificationService', ['$rootScope', '$interval', 'UserService', 'ApiService', 'StringBuilderService', 'PlanService', 'UserService', 'Config',
function($rootScope, $interval, UserService, ApiService, StringBuilderService, PlanService, UserService, Config) {
var notificationService = {
'user': null,
'notifications': [],
@ -830,28 +935,18 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
return notificationService;
}]);
$provide.factory('KeyService', ['$location', function($location) {
$provide.factory('KeyService', ['$location', 'Config', function($location, Config) {
var keyService = {}
if ($location.host() === 'quay.io') {
keyService['stripePublishableKey'] = 'pk_live_P5wLU0vGdHnZGyKnXlFG4oiu';
keyService['githubClientId'] = '5a8c08b06c48d89d4d1e';
keyService['githubRedirectUri'] = 'https://quay.io/oauth2/github/callback';
} else if($location.host() === 'staging.quay.io') {
keyService['stripePublishableKey'] = 'pk_live_P5wLU0vGdHnZGyKnXlFG4oiu';
keyService['githubClientId'] = '4886304accbc444f0471';
keyService['githubRedirectUri'] = 'https://staging.quay.io/oauth2/github/callback';
} else {
keyService['stripePublishableKey'] = 'pk_test_uEDHANKm9CHCvVa2DLcipGRh';
keyService['githubClientId'] = 'cfbc4aca88e5c1b40679';
keyService['githubRedirectUri'] = 'http://localhost:5000/oauth2/github/callback';
}
keyService['stripePublishableKey'] = Config['STRIPE_PUBLISHABLE_KEY'];
keyService['githubClientId'] = Config['GITHUB_CLIENT_ID'];
keyService['githubLoginClientId'] = Config['GITHUB_LOGIN_CLIENT_ID'];
keyService['githubRedirectUri'] = Config.getUrl('/oauth2/github/callback');
return keyService;
}]);
$provide.factory('PlanService', ['KeyService', 'UserService', 'CookieService', 'ApiService',
function(KeyService, UserService, CookieService, ApiService) {
$provide.factory('PlanService', ['KeyService', 'UserService', 'CookieService', 'ApiService', 'Features', 'Config',
function(KeyService, UserService, CookieService, ApiService, Features, Config) {
var plans = null;
var planDict = {};
var planService = {};
@ -877,7 +972,9 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
};
planService.notePlan = function(planId) {
CookieService.putSession('quay.notedplan', planId);
if (Features.BILLING) {
CookieService.putSession('quay.notedplan', planId);
}
};
planService.isOrgCompatible = function(plan) {
@ -903,7 +1000,7 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
planService.handleNotedPlan = function() {
var planId = planService.getAndResetNotedPlan();
if (!planId) { return false; }
if (!planId || !Features.BILLING) { return false; }
UserService.load(function() {
if (UserService.currentUser().anonymous) {
@ -948,6 +1045,8 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
};
planService.verifyLoaded = function(callback) {
if (!Features.BILLING) { return; }
if (plans) {
callback(plans);
return;
@ -1007,10 +1106,14 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
};
planService.getSubscription = function(orgname, success, failure) {
ApiService.getSubscription(orgname).then(success, failure);
if (!Features.BILLING) { return; }
ApiService.getSubscription(orgname).then(success, failure);
};
planService.setSubscription = function(orgname, planId, success, failure, opt_token) {
if (!Features.BILLING) { return; }
var subscriptionDetails = {
plan: planId
};
@ -1030,6 +1133,8 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
};
planService.getCardInfo = function(orgname, callback) {
if (!Features.BILLING) { return; }
ApiService.getCard(orgname).then(function(resp) {
callback(resp.card);
}, function() {
@ -1038,6 +1143,8 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
};
planService.changePlan = function($scope, orgname, planId, callbacks) {
if (!Features.BILLING) { return; }
if (callbacks['started']) {
callbacks['started']();
}
@ -1063,6 +1170,8 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
};
planService.changeCreditCard = function($scope, orgname, callbacks) {
if (!Features.BILLING) { return; }
if (callbacks['opening']) {
callbacks['opening']();
}
@ -1119,6 +1228,8 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
};
planService.showSubscribeDialog = function($scope, orgname, planId, callbacks) {
if (!Features.BILLING) { return; }
if (callbacks['opening']) {
callbacks['opening']();
}
@ -1128,7 +1239,9 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
if (submitted) { return; }
submitted = true;
mixpanel.track('plan_subscribe');
if (Config.MIXPANEL_KEY) {
mixpanel.track('plan_subscribe');
}
$scope.$apply(function() {
if (callbacks['started']) {
@ -1146,7 +1259,7 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
email: email,
amount: planDetails.price,
currency: 'usd',
name: 'Quay ' + planDetails.title + ' Subscription',
name: 'Quay.io ' + planDetails.title + ' Subscription',
description: 'Up to ' + planDetails.privateRepos + ' private repositories',
panelLabel: 'Subscribe',
token: submitToken,
@ -1189,10 +1302,8 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
});
};
}).
config(['$routeProvider', '$locationProvider', '$analyticsProvider',
function($routeProvider, $locationProvider, $analyticsProvider) {
$analyticsProvider.virtualPageviews(true);
config(['$routeProvider', '$locationProvider',
function($routeProvider, $locationProvider) {
$locationProvider.html5Mode(true);
@ -1213,6 +1324,8 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
templateUrl: '/static/partials/repo-list.html', controller: RepoListCtrl}).
when('/user/', {title: 'Account Settings', description:'Account settings for Quay.io', templateUrl: '/static/partials/user-admin.html',
reloadOnSearch: false, controller: UserAdminCtrl}).
when('/superuser/', {title: 'Superuser Admin Panel', description:'Admin panel for Quay.io', templateUrl: '/static/partials/super-user.html',
reloadOnSearch: false, controller: SuperUserAdminCtrl}).
when('/guide/', {title: 'Guide', description:'Guide to using private docker repositories on Quay.io', templateUrl: '/static/partials/guide.html',
controller: GuideCtrl}).
when('/tutorial/', {title: 'Tutorial', description:'Interactive tutorial for using Quay.io', templateUrl: '/static/partials/tutorial.html',
@ -1245,6 +1358,178 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
RestangularProvider.setBaseUrl('/api/v1/');
});
if (window.__config && window.__config.MIXPANEL_KEY) {
quayApp.config(['$analyticsProvider', function($analyticsProvider) {
$analyticsProvider.virtualPageviews(true);
}]);
}
function buildConditionalLinker($animate, name, evaluator) {
// Based off of a solution found here: http://stackoverflow.com/questions/20325480/angularjs-whats-the-best-practice-to-add-ngif-to-a-directive-programmatically
return function ($scope, $element, $attr, ctrl, $transclude) {
var block;
var childScope;
var roles;
$attr.$observe(name, function (value) {
if (evaluator($scope.$eval(value))) {
if (!childScope) {
childScope = $scope.$new();
$transclude(childScope, function (clone) {
block = {
startNode: clone[0],
endNode: clone[clone.length++] = document.createComment(' end ' + name + ': ' + $attr[name] + ' ')
};
$animate.enter(clone, $element.parent(), $element);
});
}
} else {
if (childScope) {
childScope.$destroy();
childScope = null;
}
if (block) {
$animate.leave(getBlockElements(block));
block = null;
}
}
});
}
}
quayApp.directive('quayRequire', function ($animate, Features) {
return {
transclude: 'element',
priority: 600,
terminal: true,
restrict: 'A',
link: buildConditionalLinker($animate, 'quayRequire', function(value) {
return Features.matchesFeatures(value);
})
};
});
quayApp.directive('quayShow', function($animate, Features, Config) {
return {
priority: 590,
restrict: 'A',
link: function($scope, $element, $attr, ctrl, $transclude) {
$scope.Features = Features;
$scope.Config = Config;
$scope.$watch($attr.quayShow, function(result) {
$animate[!!result ? 'removeClass' : 'addClass']($element, 'ng-hide');
});
}
};
});
quayApp.directive('quayClasses', function(Features, Config) {
return {
priority: 580,
restrict: 'A',
link: function($scope, $element, $attr, ctrl, $transclude) {
// Borrowed from ngClass.
function flattenClasses(classVal) {
if(angular.isArray(classVal)) {
return classVal.join(' ');
} else if (angular.isObject(classVal)) {
var classes = [], i = 0;
angular.forEach(classVal, function(v, k) {
if (v) {
classes.push(k);
}
});
return classes.join(' ');
}
return classVal;
}
function removeClass(classVal) {
$attr.$removeClass(flattenClasses(classVal));
}
function addClass(classVal) {
$attr.$addClass(flattenClasses(classVal));
}
$scope.$watch($attr.quayClasses, function(result) {
var scopeVals = {
'Features': Features,
'Config': Config
};
for (var expr in result) {
if (!result.hasOwnProperty(expr)) { continue; }
// Evaluate the expression with the entire features list added.
var value = $scope.$eval(expr, scopeVals);
if (value) {
addClass(result[expr]);
} else {
removeClass(result[expr]);
}
}
});
}
};
});
quayApp.directive('quayInclude', function($compile, $templateCache, $http, Features, Config) {
return {
priority: 595,
restrict: 'A',
link: function($scope, $element, $attr, ctrl) {
var getTemplate = function(templateName) {
var templateUrl = '/static/partials/' + templateName;
return $http.get(templateUrl, {cache: $templateCache});
};
var result = $scope.$eval($attr.quayInclude);
if (!result) {
return;
}
var scopeVals = {
'Features': Features,
'Config': Config
};
var templatePath = null;
for (var expr in result) {
if (!result.hasOwnProperty(expr)) { continue; }
// Evaluate the expression with the entire features list added.
var value = $scope.$eval(expr, scopeVals);
if (value) {
templatePath = result[expr];
break;
}
}
if (!templatePath) {
return;
}
var promise = getTemplate(templatePath).success(function(html) {
$element.html(html);
}).then(function (response) {
$element.replaceWith($compile($element.html())($scope));
if ($attr.onload) {
$scope.$eval($attr.onload);
}
});
}
};
});
quayApp.directive('entityReference', function () {
var directiveDefinitionObject = {
@ -1516,12 +1801,14 @@ quayApp.directive('signinForm', function () {
'signInStarted': '&signInStarted',
'signedIn': '&signedIn'
},
controller: function($scope, $location, $timeout, ApiService, KeyService, UserService, CookieService) {
controller: function($scope, $location, $timeout, ApiService, KeyService, UserService, CookieService, Features, Config) {
$scope.showGithub = function() {
if (!Features.GITHUB_LOGIN) { return; }
$scope.markStarted();
var mixpanelDistinctIdClause = '';
if (mixpanel.get_distinct_id !== undefined) {
if (Config.MIXPANEL_KEY && mixpanel.get_distinct_id !== undefined) {
$scope.mixpanelDistinctIdClause = "&state=" + encodeURIComponent(mixpanel.get_distinct_id());
}
@ -1587,34 +1874,35 @@ quayApp.directive('signupForm', function () {
scope: {
},
controller: function($scope, $location, $timeout, ApiService, KeyService, UserService) {
controller: function($scope, $location, $timeout, ApiService, KeyService, UserService, Config, UIService) {
$('.form-signup').popover();
angulartics.waitForVendorApi(mixpanel, 500, function(loadedMixpanel) {
var mixpanelId = loadedMixpanel.get_distinct_id();
$scope.github_state_clause = '&state=' + mixpanelId;
});
if (Config.MIXPANEL_KEY) {
angulartics.waitForVendorApi(mixpanel, 500, function(loadedMixpanel) {
var mixpanelId = loadedMixpanel.get_distinct_id();
$scope.github_state_clause = '&state=' + mixpanelId;
});
}
$scope.githubClientId = KeyService.githubClientId;
$scope.awaitingConfirmation = false;
$scope.registering = false;
$scope.register = function() {
$('.form-signup').popover('hide');
UIService.hidePopover('#signupButton');
$scope.registering = true;
ApiService.createNewUser($scope.newUser).then(function() {
$scope.awaitingConfirmation = true;
$scope.registering = false;
mixpanel.alias($scope.newUser.username);
$scope.awaitingConfirmation = true;
if (Config.MIXPANEL_KEY) {
mixpanel.alias($scope.newUser.username);
}
}, function(result) {
$scope.registering = false;
$scope.registerError = result.data.message;
$timeout(function() {
$('.form-signup').popover('show');
});
UIService.showFormError('#signupButton', result);
});
};
}
@ -1644,7 +1932,7 @@ quayApp.directive('plansTable', function () {
});
quayApp.directive('dockerAuthDialog', function () {
quayApp.directive('dockerAuthDialog', function (Config) {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/docker-auth-dialog.html',
@ -1665,11 +1953,10 @@ quayApp.directive('dockerAuthDialog', function () {
$scope.downloadCfg = function() {
var auth = $.base64.encode($scope.username + ":" + $scope.token);
config = {
"https://quay.io/v1/": {
"auth": auth,
"email": ""
}
config = {}
config[Config.getUrl('/v1/')] = {
"auth": auth,
"email": ""
};
var file = JSON.stringify(config, null, ' ');
@ -2653,11 +2940,13 @@ quayApp.directive('entitySearch', function () {
'isOrganization': '=isOrganization',
'isPersistent': '=isPersistent',
'currentEntity': '=currentEntity',
'clearNow': '=clearNow'
'clearNow': '=clearNow',
'filter': '=filter',
},
controller: function($scope, $element, Restangular, UserService, ApiService) {
$scope.lazyLoading = true;
$scope.isAdmin = false;
$scope.currentEntityInternal = $scope.currentEntity;
$scope.lazyLoad = function() {
if (!$scope.namespace || !$scope.lazyLoading) { return; }
@ -2736,20 +3025,27 @@ quayApp.directive('entitySearch', function () {
entity['is_org_member'] = true;
}
$scope.setEntityInternal(entity);
$scope.setEntityInternal(entity, false);
};
$scope.clearEntityInternal = function() {
$scope.currentEntityInternal = null;
$scope.currentEntity = null;
if ($scope.entitySelected) {
$scope.entitySelected(null);
}
};
$scope.setEntityInternal = function(entity) {
$(input).typeahead('val', $scope.isPersistent ? entity.name : '');
$scope.setEntityInternal = function(entity, updateTypeahead) {
if (updateTypeahead) {
$(input).typeahead('val', $scope.isPersistent ? entity.name : '');
} else {
$(input).val($scope.isPersistent ? entity.name : '');
}
if ($scope.isPersistent) {
$scope.currentEntityInternal = entity;
$scope.currentEntity = entity;
}
@ -2777,6 +3073,19 @@ quayApp.directive('entitySearch', function () {
var datums = [];
for (var i = 0; i < data.results.length; ++i) {
var entity = data.results[i];
if ($scope.filter) {
var allowed = $scope.filter;
var found = 'user';
if (entity.kind == 'user') {
found = entity.is_robot ? 'robot' : 'user';
} else if (entity.kind == 'team') {
found = 'team';
}
if (allowed.indexOf(found)) {
continue;
}
}
datums.push({
'value': entity.name,
'tokens': [entity.name],
@ -2849,7 +3158,7 @@ quayApp.directive('entitySearch', function () {
$(input).on('typeahead:selected', function(e, datum) {
$scope.$apply(function() {
$scope.setEntityInternal(datum.entity);
$scope.setEntityInternal(datum.entity, true);
});
});
@ -2861,6 +3170,16 @@ quayApp.directive('entitySearch', function () {
$scope.$watch('inputTitle', function(title) {
input.setAttribute('placeholder', title);
});
$scope.$watch('currentEntity', function(entity) {
if ($scope.currentEntityInternal != entity) {
if (entity) {
$scope.setEntityInternal(entity, false);
} else {
$scope.clearEntityInternal();
}
}
});
}
};
return directiveDefinitionObject;
@ -3072,7 +3391,7 @@ quayApp.directive('planManager', function () {
}
if (!$scope.chart) {
$scope.chart = new RepositoryUsageChart();
$scope.chart = new UsageChart();
$scope.chart.draw('repository-usage-chart');
}
@ -3398,6 +3717,145 @@ quayApp.directive('dropdownSelectMenu', function () {
});
quayApp.directive('setupTriggerDialog', function () {
var directiveDefinitionObject = {
templateUrl: '/static/directives/setup-trigger-dialog.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
'repository': '=repository',
'trigger': '=trigger',
'counter': '=counter',
'canceled': '&canceled',
'activated': '&activated'
},
controller: function($scope, $element, ApiService, UserService) {
$scope.show = function() {
$scope.pullEntity = null;
$scope.publicPull = true;
$scope.showPullRequirements = false;
$('#setupTriggerModal').modal({});
$('#setupTriggerModal').on('hidden.bs.modal', function () {
$scope.$apply(function() {
$scope.cancelSetupTrigger();
});
});
};
$scope.isNamespaceAdmin = function(namespace) {
return UserService.isNamespaceAdmin(namespace);
};
$scope.cancelSetupTrigger = function() {
$scope.canceled({'trigger': $scope.trigger});
};
$scope.hide = function() {
$('#setupTriggerModal').modal('hide');
};
$scope.setPublicPull = function(value) {
$scope.publicPull = value;
};
$scope.checkAnalyze = function(isValid) {
if (!isValid) {
$scope.publicPull = true;
$scope.pullEntity = null;
$scope.showPullRequirements = false;
$scope.checkingPullRequirements = false;
return;
}
$scope.checkingPullRequirements = true;
$scope.showPullRequirements = true;
$scope.pullRequirements = null;
var params = {
'repository': $scope.repository.namespace + '/' + $scope.repository.name,
'trigger_uuid': $scope.trigger.id
};
var data = {
'config': $scope.trigger.config
};
ApiService.analyzeBuildTrigger(data, params).then(function(resp) {
$scope.pullRequirements = resp;
if (resp['status'] == 'publicbase') {
$scope.publicPull = true;
$scope.pullEntity = null;
} else if (resp['namespace']) {
$scope.publicPull = false;
if (resp['robots'] && resp['robots'].length > 0) {
$scope.pullEntity = resp['robots'][0];
} else {
$scope.pullEntity = null;
}
}
$scope.checkingPullRequirements = false;
}, function(resp) {
$scope.pullRequirements = resp;
$scope.checkingPullRequirements = false;
});
};
$scope.activate = function() {
var params = {
'repository': $scope.repository.namespace + '/' + $scope.repository.name,
'trigger_uuid': $scope.trigger.id
};
var data = {
'config': $scope.trigger['config']
};
if ($scope.pullEntity) {
data['pull_robot'] = $scope.pullEntity['name'];
}
ApiService.activateBuildTrigger(data, params).then(function(resp) {
trigger['is_active'] = true;
trigger['pull_robot'] = resp['pull_robot'];
$scope.activated({'trigger': $scope.trigger});
}, function(resp) {
$scope.hide();
$scope.canceled({'trigger': $scope.trigger});
bootbox.dialog({
"message": resp['data']['message'] || 'The build trigger setup could not be completed',
"title": "Could not activate build trigger",
"buttons": {
"close": {
"label": "Close",
"className": "btn-primary"
}
}
});
});
};
var check = function() {
if ($scope.counter && $scope.trigger && $scope.repository) {
$scope.show();
}
};
$scope.$watch('trigger', check);
$scope.$watch('counter', check);
$scope.$watch('repository', check);
}
};
return directiveDefinitionObject;
});
quayApp.directive('triggerSetupGithub', function () {
var directiveDefinitionObject = {
priority: 0,
@ -3407,15 +3865,18 @@ quayApp.directive('triggerSetupGithub', function () {
restrict: 'C',
scope: {
'repository': '=repository',
'trigger': '=trigger'
'trigger': '=trigger',
'analyze': '&analyze'
},
controller: function($scope, $element, ApiService) {
$scope.analyzeCounter = 0;
$scope.setupReady = false;
$scope.loading = true;
$scope.handleLocationInput = function(location) {
$scope.trigger['config']['subdir'] = location || '';
$scope.isInvalidLocation = $scope.locations.indexOf(location) < 0;
$scope.analyze({'isValid': !$scope.isInvalidLocation});
};
$scope.handleLocationSelected = function(datum) {
@ -3426,6 +3887,7 @@ quayApp.directive('triggerSetupGithub', function () {
$scope.currentLocation = location;
$scope.trigger['config']['subdir'] = location || '';
$scope.isInvalidLocation = false;
$scope.analyze({'isValid': true});
};
$scope.selectRepo = function(repo, org) {
@ -3464,6 +3926,7 @@ quayApp.directive('triggerSetupGithub', function () {
$scope.locations = null;
$scope.trigger.$ready = false;
$scope.isInvalidLocation = false;
$scope.analyze({'isValid': false});
return;
}
@ -3476,12 +3939,14 @@ quayApp.directive('triggerSetupGithub', function () {
} else {
$scope.currentLocation = null;
$scope.isInvalidLocation = resp['subdir'].indexOf('') < 0;
$scope.analyze({'isValid': !$scope.isInvalidLocation});
}
}, function(resp) {
$scope.locationError = resp['message'] || 'Could not load Dockerfile locations';
$scope.locations = null;
$scope.trigger.$ready = false;
$scope.isInvalidLocation = false;
$scope.analyze({'isValid': false});
});
}
};
@ -3526,7 +3991,14 @@ quayApp.directive('triggerSetupGithub', function () {
});
};
loadSources();
var check = function() {
if ($scope.repository && $scope.trigger) {
loadSources();
}
};
$scope.$watch('repository', check);
$scope.$watch('trigger', check);
$scope.$watch('currentRepo', function(repo) {
$scope.selectRepoInternal(repo);
@ -3572,7 +4044,7 @@ quayApp.directive('dockerfileCommand', function () {
scope: {
'command': '=command'
},
controller: function($scope, $element, $sanitize) {
controller: function($scope, $element, $sanitize, Config) {
var registryHandlers = {
'quay.io': function(pieces) {
var rnamespace = pieces[pieces.length - 2];
@ -3587,6 +4059,8 @@ quayApp.directive('dockerfileCommand', function () {
}
};
registryHandlers[Config.getDomain()] = registryHandlers['quay.io'];
var kindHandlers = {
'FROM': function(title) {
var pieces = title.split('/');
@ -4259,6 +4733,17 @@ quayApp.run(['$location', '$rootScope', 'Restangular', 'UserService', 'PlanServi
});
};
$rootScope.$watch('description', function(description) {
if (!description) {
description = 'Hosted private docker repositories. Includes full user management and history. Free for public repositories.';
}
// Note: We set the content of the description tag manually here rather than using Angular binding
// because we need the <meta> tag to have a default description that is not of the form "{{ description }}",
// we read by tools that do not properly invoke the Angular code.
$('#descriptionTag').attr('content', description);
});
$rootScope.$on('$routeUpdate', function(){
if ($location.search()['tab']) {
changeTab($location.search()['tab']);
@ -4275,7 +4760,7 @@ quayApp.run(['$location', '$rootScope', 'Restangular', 'UserService', 'PlanServi
if (current.$$route.description) {
$rootScope.description = current.$$route.description;
} else {
$rootScope.description = 'Hosted private docker repositories. Includes full user management and history. Free for public repositories.';
$rootScope.description = '';
}
$rootScope.fixFooter = !!current.$$route.fixFooter;

View file

@ -48,14 +48,15 @@ function PlansCtrl($scope, $location, UserService, PlanService) {
};
}
function TutorialCtrl($scope, AngularTour, AngularTourSignals, UserService) {
function TutorialCtrl($scope, AngularTour, AngularTourSignals, UserService, Config) {
// Default to showing sudo on all commands if on linux.
var showSudo = navigator.appVersion.indexOf("Linux") != -1;
$scope.tour = {
'title': 'Quay.io Tutorial',
'initialScope': {
'showSudo': showSudo
'showSudo': showSudo,
'domainName': Config.getDomain()
},
'steps': [
{
@ -262,7 +263,7 @@ function RepoListCtrl($scope, $sanitize, Restangular, UserService, ApiService) {
loadPublicRepos();
}
function LandingCtrl($scope, UserService, ApiService) {
function LandingCtrl($scope, UserService, ApiService, Features, Config) {
$scope.namespace = null;
$scope.$watch('namespace', function(namespace) {
@ -303,10 +304,22 @@ function LandingCtrl($scope, UserService, ApiService) {
});
};
browserchrome.update();
$scope.chromify = function() {
browserchrome.update();
};
$scope.getEnterpriseLogo = function() {
if (!Config.ENTERPRISE_LOGO_URL) {
return '/static/img/quay-logo.png';
}
return Config.ENTERPRISE_LOGO_URL;
};
}
function RepoCtrl($scope, $sanitize, Restangular, ImageMetadataService, ApiService, $routeParams, $rootScope, $location, $timeout) {
function RepoCtrl($scope, $sanitize, Restangular, ImageMetadataService, ApiService, $routeParams, $rootScope, $location, $timeout, Config) {
$scope.Config = Config;
var namespace = $routeParams.namespace;
var name = $routeParams.name;
@ -945,9 +958,13 @@ function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
var data = {
'file_id': build['resource_key'],
'subdirectory': subdirectory
'subdirectory': subdirectory,
};
if (build['pull_robot']) {
data['pull_robot'] = build['pull_robot']['name'];
}
var params = {
'repository': namespace + '/' + name
};
@ -1073,6 +1090,7 @@ function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
// Note: We use extend here rather than replacing as Angular is depending on the
// root build object to remain the same object.
$.extend(true, $scope.builds[$scope.currentBuildIndex], resp);
var currentBuild = $scope.builds[$scope.currentBuildIndex];
checkPollTimer();
// Load the updated logs for the build.
@ -1089,6 +1107,18 @@ function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
processLogs(resp['logs'], resp['start']);
$scope.logStartIndex = resp['total'];
$scope.polling = false;
// If the build status is an error, open the last two log entries.
if (currentBuild['phase'] == 'error' && $scope.logEntries.length > 1) {
var openLogEntries = function(entry) {
if (entry.logs) {
entry.logs.setVisible(true);
}
};
openLogEntries($scope.logEntries[$scope.logEntries.length - 2]);
openLogEntries($scope.logEntries[$scope.logEntries.length - 1]);
}
}, function() {
$scope.polling = false;
});
@ -1131,7 +1161,7 @@ function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
fetchRepository();
}
function RepoAdminCtrl($scope, Restangular, ApiService, KeyService, $routeParams, $rootScope, $location) {
function RepoAdminCtrl($scope, Restangular, ApiService, KeyService, $routeParams, $rootScope, $location, UserService, Config) {
var namespace = $routeParams.namespace;
var name = $routeParams.name;
@ -1144,15 +1174,17 @@ function RepoAdminCtrl($scope, Restangular, ApiService, KeyService, $routeParams
$scope.githubRedirectUri = KeyService.githubRedirectUri;
$scope.githubClientId = KeyService.githubClientId;
$scope.showTriggerSetupCounter = 0;
$scope.getBadgeFormat = function(format, repo) {
if (!repo) { return; }
var imageUrl = 'https://quay.io/repository/' + namespace + '/' + name + '/status';
var imageUrl = Config.getUrl('/' + namespace + '/' + name + '/status');
if (!$scope.repo.is_public) {
imageUrl += '?token=' + $scope.repo.status_token;
}
var linkUrl = 'https://quay.io/repository/' + namespace + '/' + name;
var linkUrl = Config.getUrl('/' + namespace + '/' + name);
switch (format) {
case 'svg':
@ -1433,48 +1465,15 @@ function RepoAdminCtrl($scope, Restangular, ApiService, KeyService, $routeParams
};
$scope.setupTrigger = function(trigger) {
$scope.triggerSetupReady = false;
$scope.currentSetupTrigger = trigger;
$('#setupTriggerModal').modal({});
$('#setupTriggerModal').on('hidden.bs.modal', function () {
$scope.$apply(function() {
$scope.cancelSetupTrigger();
});
});
$scope.showTriggerSetupCounter++;
};
$scope.finishSetupTrigger = function(trigger) {
$('#setupTriggerModal').modal('hide');
$scope.currentSetupTrigger = null;
var params = {
'repository': namespace + '/' + name,
'trigger_uuid': trigger.id
};
ApiService.activateBuildTrigger(trigger['config'], params).then(function(resp) {
trigger['is_active'] = true;
}, function(resp) {
$scope.triggers.splice($scope.triggers.indexOf(trigger), 1);
bootbox.dialog({
"message": resp['data']['message'] || 'The build trigger setup could not be completed',
"title": "Could not activate build trigger",
"buttons": {
"close": {
"label": "Close",
"className": "btn-primary"
}
}
});
});
};
$scope.cancelSetupTrigger = function() {
if (!$scope.currentSetupTrigger) { return; }
$('#setupTriggerModal').modal('hide');
$scope.deleteTrigger($scope.currentSetupTrigger);
$scope.cancelSetupTrigger = function(trigger) {
if ($scope.currentSetupTrigger != trigger) { return; }
$scope.currentSetupTrigger = null;
$scope.deleteTrigger(trigger);
};
$scope.startTrigger = function(trigger) {
@ -1569,12 +1568,16 @@ function RepoAdminCtrl($scope, Restangular, ApiService, KeyService, $routeParams
}
function UserAdminCtrl($scope, $timeout, $location, ApiService, PlanService, UserService, CookieService, KeyService,
$routeParams, $http) {
$routeParams, $http, UIService, Features) {
$scope.Features = Features;
if ($routeParams['migrate']) {
$('#migrateTab').tab('show')
}
UserService.updateUserIn($scope, function(user) {
if (!Features.GITHUB_LOGIN) { return; }
$scope.cuser = jQuery.extend({}, user);
for (var i = 0; i < $scope.cuser.logins.length; i++) {
@ -1602,8 +1605,6 @@ function UserAdminCtrl($scope, $timeout, $location, ApiService, PlanService, Use
$scope.githubClientId = KeyService.githubClientId;
$scope.authorizedApps = null;
$('.form-change').popover();
$scope.logsShown = 0;
$scope.invoicesShown = 0;
@ -1652,13 +1653,15 @@ function UserAdminCtrl($scope, $timeout, $location, ApiService, PlanService, Use
};
$scope.showConvertForm = function() {
PlanService.getMatchingBusinessPlan(function(plan) {
$scope.org.plan = plan;
});
if (Features.BILLING) {
PlanService.getMatchingBusinessPlan(function(plan) {
$scope.org.plan = plan;
});
PlanService.getPlans(function(plans) {
$scope.orgPlans = plans;
});
PlanService.getPlans(function(plans) {
$scope.orgPlans = plans;
});
}
$scope.convertStep = 1;
};
@ -1673,7 +1676,7 @@ function UserAdminCtrl($scope, $timeout, $location, ApiService, PlanService, Use
var data = {
'adminUser': $scope.org.adminUser,
'adminPassword': $scope.org.adminPassword,
'plan': $scope.org.plan.stripeId
'plan': $scope.org.plan ? $scope.org.plan.stripeId : ''
};
ApiService.convertUserToOrganization(data).then(function(resp) {
@ -1691,7 +1694,8 @@ function UserAdminCtrl($scope, $timeout, $location, ApiService, PlanService, Use
};
$scope.changeEmail = function() {
$('#changeEmailForm').popover('hide');
UIService.hidePopover('#changeEmailForm');
$scope.updatingUser = true;
$scope.changeEmailSent = false;
@ -1706,16 +1710,13 @@ function UserAdminCtrl($scope, $timeout, $location, ApiService, PlanService, Use
$scope.changeEmailForm.$setPristine();
}, function(result) {
$scope.updatingUser = false;
$scope.changeEmailError = result.data.message;
$timeout(function() {
$('#changeEmailForm').popover('show');
});
UIService.showFormError('#changeEmailForm', result);
});
};
$scope.changePassword = function() {
$('#changePasswordForm').popover('hide');
UIService.hidePopover('#changePasswordForm');
$scope.updatingUser = true;
$scope.changePasswordSuccess = false;
@ -1733,11 +1734,7 @@ function UserAdminCtrl($scope, $timeout, $location, ApiService, PlanService, Use
UserService.load();
}, function(result) {
$scope.updatingUser = false;
$scope.changePasswordError = result.data.message;
$timeout(function() {
$('#changePasswordForm').popover('show');
});
UIService.showFormError('#changePasswordForm', result);
});
};
}
@ -1874,7 +1871,7 @@ function V1Ctrl($scope, $location, UserService) {
UserService.updateUserIn($scope);
}
function NewRepoCtrl($scope, $location, $http, $timeout, UserService, ApiService, PlanService, KeyService) {
function NewRepoCtrl($scope, $location, $http, $timeout, UserService, ApiService, PlanService, KeyService, Features) {
UserService.updateUserIn($scope);
$scope.githubRedirectUri = KeyService.githubRedirectUri;
@ -1996,13 +1993,19 @@ function NewRepoCtrl($scope, $location, $http, $timeout, UserService, ApiService
var checkPrivateAllowed = function() {
if (!$scope.repo || !$scope.repo.namespace) { return; }
if (!Features.BILLING) {
$scope.checkingPlan = false;
$scope.planRequired = null;
return;
}
$scope.checkingPlan = true;
var isUserNamespace = $scope.isUserNamespace;
ApiService.getPrivateAllowed(isUserNamespace ? null : $scope.repo.namespace).then(function(resp) {
$scope.checkingPlan = false;
if (resp['privateAllowed']) {
if (resp['privateAllowed']) {
$scope.planRequired = null;
return;
}
@ -2122,18 +2125,20 @@ function OrgViewCtrl($rootScope, $scope, ApiService, $routeParams) {
loadOrganization();
}
function OrgAdminCtrl($rootScope, $scope, $timeout, Restangular, $routeParams, UserService, PlanService, ApiService) {
function OrgAdminCtrl($rootScope, $scope, $timeout, Restangular, $routeParams, UserService, PlanService, ApiService, Features, UIService) {
var orgname = $routeParams.orgname;
// Load the list of plans.
PlanService.getPlans(function(plans) {
$scope.plans = plans;
$scope.plan_map = {};
for (var i = 0; i < plans.length; ++i) {
$scope.plan_map[plans[i].stripeId] = plans[i];
}
});
if (Features.BILLING) {
PlanService.getPlans(function(plans) {
$scope.plans = plans;
$scope.plan_map = {};
for (var i = 0; i < plans.length; ++i) {
$scope.plan_map[plans[i].stripeId] = plans[i];
}
});
}
$scope.orgname = orgname;
$scope.membersLoading = true;
@ -2161,10 +2166,12 @@ function OrgAdminCtrl($rootScope, $scope, $timeout, Restangular, $routeParams, U
};
$scope.$watch('organizationEmail', function(e) {
$('#changeEmailForm').popover('hide');
UIService.hidePopover('#changeEmailForm');
});
$scope.changeEmail = function() {
UIService.hidePopover('#changeEmailForm');
$scope.changingOrganization = true;
var params = {
'orgname': orgname
@ -2180,10 +2187,7 @@ function OrgAdminCtrl($rootScope, $scope, $timeout, Restangular, $routeParams, U
$scope.organization = org;
}, function(result) {
$scope.changingOrganization = false;
$scope.changeEmailError = result.data.message;
$timeout(function() {
$('#changeEmailForm').popover('show');
});
UIService.showFormError('#changeEmailForm', result);
});
};
@ -2316,30 +2320,39 @@ function OrgsCtrl($scope, UserService) {
browserchrome.update();
}
function NewOrgCtrl($scope, $routeParams, $timeout, $location, UserService, PlanService, ApiService, CookieService) {
function NewOrgCtrl($scope, $routeParams, $timeout, $location, UserService, PlanService, ApiService, CookieService, Features) {
$scope.Features = Features;
$scope.holder = {};
UserService.updateUserIn($scope);
var requested = $routeParams['plan'];
// Load the list of plans.
PlanService.getPlans(function(plans) {
$scope.plans = plans;
$scope.currentPlan = null;
if (requested) {
PlanService.getPlan(requested, function(plan) {
$scope.currentPlan = plan;
});
}
});
if (Features.BILLING) {
// Load the list of plans.
PlanService.getPlans(function(plans) {
$scope.plans = plans;
$scope.currentPlan = null;
if (requested) {
PlanService.getPlan(requested, function(plan) {
$scope.currentPlan = plan;
});
}
});
}
$scope.signedIn = function() {
PlanService.handleNotedPlan();
if (Features.BILLING) {
PlanService.handleNotedPlan();
}
};
$scope.signinStarted = function() {
PlanService.getMinimumPlan(1, true, function(plan) {
PlanService.notePlan(plan.stripeId);
});
if (Features.BILLING) {
PlanService.getMinimumPlan(1, true, function(plan) {
PlanService.notePlan(plan.stripeId);
});
}
};
$scope.setPlan = function(plan) {
@ -2371,7 +2384,7 @@ function NewOrgCtrl($scope, $routeParams, $timeout, $location, UserService, Plan
};
// If the selected plan is free, simply move to the org page.
if ($scope.currentPlan.price == 0) {
if (!Features.BILLING || $scope.currentPlan.price == 0) {
showOrg();
return;
}
@ -2564,4 +2577,135 @@ function ManageApplicationCtrl($scope, $routeParams, $rootScope, $location, $tim
// Load the organization and application info.
loadOrganization();
loadApplicationInfo();
}
function SuperUserAdminCtrl($scope, ApiService, Features, UserService) {
if (!Features.SUPER_USERS) {
return;
}
// Monitor any user changes and place the current user into the scope.
UserService.updateUserIn($scope);
$scope.loadUsers = function() {
if ($scope.users) {
return;
}
$scope.loadUsersInternal();
};
$scope.loadUsersInternal = function() {
ApiService.listAllUsers().then(function(resp) {
$scope.users = resp['users'];
}, function(resp) {
$scope.users = [];
$scope.usersError = resp['data']['message'] || resp['data']['error_description'];
});
};
$scope.showChangePassword = function(user) {
$scope.userToChange = user;
$('#changePasswordModal').modal({});
};
$scope.showDeleteUser = function(user) {
if (user.username == UserService.currentUser().username) {
bootbox.dialog({
"message": 'Cannot delete yourself!',
"title": "Cannot delete user",
"buttons": {
"close": {
"label": "Close",
"className": "btn-primary"
}
}
});
return;
}
$scope.userToDelete = user;
$('#confirmDeleteUserModal').modal({});
};
$scope.changeUserPassword = function(user) {
$('#changePasswordModal').modal('hide');
var params = {
'username': user.username
};
var data = {
'password': user.password
};
ApiService.changeInstallUser(data, params).then(function(resp) {
$scope.loadUsersInternal();
}, function(resp) {
bootbox.dialog({
"message": resp.data ? resp.data.message : 'Could not change user',
"title": "Cannot change user",
"buttons": {
"close": {
"label": "Close",
"className": "btn-primary"
}
}
});
});
};
$scope.deleteUser = function(user) {
$('#confirmDeleteUserModal').modal('hide');
var params = {
'username': user.username
};
ApiService.deleteInstallUser(null, params).then(function(resp) {
$scope.loadUsersInternal();
}, function(resp) {
bootbox.dialog({
"message": resp.data ? resp.data.message : 'Could not delete user',
"title": "Cannot delete user",
"buttons": {
"close": {
"label": "Close",
"className": "btn-primary"
}
}
});
});
};
var seatUsageLoaded = function(usage) {
$scope.usageLoading = false;
if (usage.count > usage.allowed) {
$scope.limit = 'over';
} else if (usage.count == usage.allowed) {
$scope.limit = 'at';
} else if (usage.count >= usage.allowed * 0.7) {
$scope.limit = 'near';
} else {
$scope.limit = 'none';
}
if (!$scope.chart) {
$scope.chart = new UsageChart();
$scope.chart.draw('seat-usage-chart');
}
$scope.chart.update(usage.count, usage.allowed);
};
var loadSeatUsage = function() {
$scope.usageLoading = true;
ApiService.getSeatCount().then(function(resp) {
seatUsageLoaded(resp);
});
};
loadSeatUsage();
}

View file

@ -230,7 +230,17 @@ ImageHistoryTree.prototype.draw = function(container) {
if (d.image.command && d.image.command.length) {
html += '<span class="command info-line"><i class="fa fa-terminal"></i>' + formatCommand(d.image) + '</span>';
}
html += '<span class="created info-line"><i class="fa fa-calendar"></i>' + formatTime(d.image.created) + '</span>';
html += '<span class="created info-line"><i class="fa fa-calendar"></i>' + formatTime(d.image.created) + '</span>';
var tags = d.tags || [];
html += '<span class="tooltip-tags tags">';
for (var i = 0; i < tags.length; ++i) {
var tag = tags[i];
var kind = 'default';
html += '<span class="label label-' + kind + ' tag" data-tag="' + tag + '">' + tag + '</span>';
}
html += '</span>';
return html;
})
@ -338,6 +348,23 @@ ImageHistoryTree.prototype.changeImage_ = function(imageId) {
};
/**
* Expands the given collapsed node in the tree.
*/
ImageHistoryTree.prototype.expandCollapsed_ = function(imageNode) {
var index = imageNode.parent.children.indexOf(imageNode);
if (index < 0 || imageNode.encountered.length < 2) {
return;
}
// Note: we start at 1 since the 0th encountered node is the parent.
imageNode.parent.children.splice(index, 1, imageNode.encountered[1]);
this.maxHeight_ = this.determineMaximumHeight_(this.root_);
this.update_(this.root_);
this.updateDimensions_();
};
/**
* Builds the root node for the tree.
*/
@ -640,7 +667,10 @@ ImageHistoryTree.prototype.update_ = function(source) {
.attr("dy", ".35em")
.attr("text-anchor", function(d) { return d.children || d._children ? "end" : "start"; })
.text(function(d) { return d.name; })
.on("click", function(d) { if (d.image) { that.changeImage_(d.image.id); } })
.on("click", function(d) {
if (d.image) { that.changeImage_(d.image.id); }
if (d.collapsed) { that.expandCollapsed_(d); }
})
.on('mouseover', tip.show)
.on('mouseout', tip.hide)
.on("contextmenu", function(d, e) {
@ -729,7 +759,7 @@ ImageHistoryTree.prototype.update_ = function(source) {
if (tag == currentTag) {
kind = 'success';
}
html += '<span class="label label-' + kind + ' tag" data-tag="' + tag + '"" style="max-width: ' + DEPTH_HEIGHT + 'px">' + tag + '</span>';
html += '<span class="label label-' + kind + ' tag" data-tag="' + tag + '" title="' + tag + '" style="max-width: ' + DEPTH_HEIGHT + 'px">' + tag + '</span>';
}
html += '</div>';
return html;
@ -1350,7 +1380,7 @@ FileTree.prototype.getNodesHeight = function() {
/**
* Based off of http://bl.ocks.org/mbostock/1346410
*/
function RepositoryUsageChart() {
function UsageChart() {
this.total_ = null;
this.count_ = null;
this.drawn_ = false;
@ -1360,7 +1390,7 @@ function RepositoryUsageChart() {
/**
* Updates the chart with the given count and total of number of repositories.
*/
RepositoryUsageChart.prototype.update = function(count, total) {
UsageChart.prototype.update = function(count, total) {
if (!this.g_) { return; }
this.total_ = total;
this.count_ = count;
@ -1371,7 +1401,7 @@ RepositoryUsageChart.prototype.update = function(count, total) {
/**
* Conducts the actual draw or update (if applicable).
*/
RepositoryUsageChart.prototype.drawInternal_ = function() {
UsageChart.prototype.drawInternal_ = function() {
// If the total is null, then we have not yet set the proper counts.
if (this.total_ === null) { return; }
@ -1430,7 +1460,7 @@ RepositoryUsageChart.prototype.drawInternal_ = function() {
/**
* Draws the chart in the given container.
*/
RepositoryUsageChart.prototype.draw = function(container) {
UsageChart.prototype.draw = function(container) {
var cw = 200;
var ch = 200;
var radius = Math.min(cw, ch) / 2;
@ -1707,7 +1737,7 @@ LogUsageChart.prototype.draw = function(container, logData, startDate, endDate)
.duration(500)
.call(chart);
nv.utils.windowResize(chart.update);
nv.utils.windoweResize(chart.update);
chart.multibar.dispatch.on('elementClick', function(e) { that.handleElementClicked_(e); });
chart.dispatch.on('stateChange', function(e) { that.handleStateChange_(e); });

View file

@ -135,7 +135,7 @@ angular.module("angular-tour", [])
};
var fireMixpanelEvent = function() {
if (!$scope.step || !mixpanel) { return; }
if (!$scope.step || !window['mixpanel']) { return; }
var eventName = $scope.step['mixpanelEvent'];
if (eventName) {

View file

@ -23,7 +23,7 @@
<dd am-time-ago="parseDate(image.value.created)"></dd>
<dt>Compressed Image Size</dt>
<dd><span class="context-tooltip"
title="The amount of data sent between Docker and Quay.io when pushing/pulling"
data-title="The amount of data sent between Docker and Quay.io when pushing/pulling"
bs-tooltip="tooltip.title" data-container="body">{{ image.value.size | bytes }}</span>
</dd>
@ -64,7 +64,7 @@
</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 title="{{change.file}}">
<span data-title="{{change.file}}">
<span style="color: #888;">
<span ng-repeat="folder in getFolders(change.file)"><a href="javascript:void(0)" ng-click="setFolderFilter(getFolder(change.file), $index)">{{folder}}</a>/</span></span><span>{{getFilename(change.file)}}</span>
</span>

View file

@ -0,0 +1,58 @@
<div class="landing-content">
<div class="jumbotron landing">
<div class="container">
<div class="row messages">
<div class="col-md-7">
<div ng-show="user.anonymous" style="text-align: center">
<span style="display: inline-block; background: white; padding: 10px; border-radius: 10px;">
<img ng-src="{{ getEnterpriseLogo() }}" style="max-height: 100px;">
</span>
<h1>Quay.io Enterprise Edition</h1>
</div>
<div ng-show="!user.anonymous">
<span class="namespace-selector" user="user" namespace="namespace" ng-show="user.organizations"></span>
<div class="resource-view" resource="my_repositories">
<!-- Repos -->
<div ng-show="my_repositories.value.length > 0">
<h2>Top Repositories</h2>
<div class="repo-listing" ng-repeat="repository in my_repositories.value">
<span class="repo-circle no-background" repo="repository"></span>
<a ng-href="/repository/{{ repository.namespace }}/{{ repository.name }}">{{repository.namespace}}/{{repository.name}}</a>
<div class="markdown-view description" content="repository.description" first-line-only="true"></div>
</div>
</div>
<!-- No Repos -->
<div ng-show="my_repositories.value.length == 0">
<div class="sub-message" style="margin-top: 20px">
<span ng-show="namespace != user.username">You don't have access to any repositories in this organization yet.</span>
<span ng-show="namespace == user.username">You don't have any repositories yet!</span>
<div class="options">
<a class="btn btn-primary" href="/repository/">Browse all repositories</a>
<a class="btn btn-success" href="/new/" ng-show="canCreateRepo(namespace)">Create a new repository</a>
</div>
</div>
</div>
</div>
</div>
</div> <!-- col -->
<div class="col-md-4 col-md-offset-1">
<div ng-show="user.anonymous">
<h3>Create Username</h3>
<div class="signup-form"></div>
</div>
<div ng-show="!user.anonymous" class="user-welcome">
<img class="gravatar" src="//www.gravatar.com/avatar/{{ user.gravatar }}?s=128&d=identicon" />
<div class="sub-message">Welcome <b>{{ user.username }}</b>!</div>
<a ng-show="my_repositories.value" class="btn btn-primary" href="/repository/">Browse all repositories</a>
<a class="btn btn-success" href="/new/">Create a new repository</a>
</div>
</div> <!-- col -->
</div> <!-- row -->
</div> <!-- container -->
</div> <!-- jumbotron -->
</div>

View file

@ -0,0 +1,152 @@
<div class="landing-content">
<div class="jumbotron landing">
<div class="container">
<div class="row messages">
<div class="col-md-7">
<div ng-show="user.anonymous">
<h1>Secure hosting for <b>private</b> Docker<a class="disclaimer-link" href="/disclaimer" target="_self">*</a> repositories</h1>
<h3>Use the Docker images <b>your team</b> needs with the safety of <b>private</b> repositories</h3>
<div class="sellcall"><a href="/plans/">Private repository plans starting at $12/mo</a></div>
</div>
<div ng-show="!user.anonymous">
<span class="namespace-selector" user="user" namespace="namespace" ng-show="user.organizations"></span>
<div class="resource-view" resource="my_repositories">
<!-- Repos -->
<div ng-show="my_repositories.value.length > 0">
<h2>Top Repositories</h2>
<div class="repo-listing" ng-repeat="repository in my_repositories.value">
<span class="repo-circle no-background" repo="repository"></span>
<a ng-href="/repository/{{ repository.namespace }}/{{ repository.name }}">{{repository.namespace}}/{{repository.name}}</a>
<div class="markdown-view description" content="repository.description" first-line-only="true"></div>
</div>
</div>
<!-- No Repos -->
<div ng-show="my_repositories.value.length == 0">
<div class="sub-message" style="margin-top: 20px">
<span ng-show="namespace != user.username">You don't have access to any repositories in this organization yet.</span>
<span ng-show="namespace == user.username">You don't have any repositories yet!</span>
<div class="options">
<a class="btn btn-primary" href="/repository/">Browse all repositories</a>
<a class="btn btn-success" href="/new/" ng-show="canCreateRepo(namespace)">Create a new repository</a>
</div>
</div>
</div>
</div>
</div>
</div> <!-- col -->
<div class="col-md-4 col-md-offset-1">
<div ng-show="user.anonymous">
<div class="signup-form"></div>
</div>
<div ng-show="!user.anonymous" class="user-welcome">
<img class="gravatar" src="//www.gravatar.com/avatar/{{ user.gravatar }}?s=128&d=identicon" />
<div class="sub-message">Welcome <b>{{ user.username }}</b>!</div>
<a ng-show="my_repositories.value" class="btn btn-primary" href="/repository/">Browse all repositories</a>
<a class="btn btn-success" href="/new/">Create a new repository</a>
</div>
</div> <!-- col -->
</div> <!-- row -->
<div class="row" ng-show="user.anonymous">
<div class="col-md-4 shoutout">
<i class="fa fa-lock"></i>
<b>Secure</b>
<span class="shoutout-expand">
Your data is transferred using <strong>SSL at all times</strong> and <strong>encrypted</strong> when at rest. More information available in our <a href="/security/">security guide</a>
</span>
</div>
<div class="col-md-4 shoutout">
<i class="fa fa-group"></i>
<b>Shareable</b>
<span class="shoutout-expand">
Have to share a repository? No problem! Share with anyone you choose
</span>
</div>
<div class="col-md-4 shoutout">
<i class="fa fa-cloud"></i>
<b>Cloud Hosted</b>
<span class="shoutout-expand">
Accessible from anywhere, anytime
</span>
</div>
</div> <!-- row -->
</div> <!-- container -->
</div> <!-- jumbotron -->
<div class="product-tour container" ng-show="user.anonymous">
<div class="tour-header row">
<div class="tour-shoutout-header"><i class="fa fa-chevron-circle-down"></i></div>
<div class="tour-shoutout">Take a tour of Quay</div>
</div>
<div class="tour-section row">
<div class="col-md-7"><img src="/static/img/user-home.png" title="User Home - Quay.io" data-screenshot-url="https://quay.io/" class="img-responsive"></div>
<div class="col-md-5">
<div class="tour-section-title">Customized for you</div>
<div class="tour-section-description">
Your personal home screen shows those repositories most important to you, ordered by recent activity.
</div>
<div class="tour-section-description">Keep up to date on the status of those repositories you deem important.</div>
</div>
</div>
<div class="tour-section row">
<div class="col-md-7 col-md-push-5"><img src="/static/img/repo-view.png" title="Repository View - Quay.io" data-screenshot-url="https://quay.io/repository/devtable/complex" class="img-responsive"></div>
<div class="col-md-5 col-md-pull-7">
<div class="tour-section-title">Useful views of respositories</div>
<div class="tour-section-description">
Each repository is presented with the maximum amount of useful information, including its image history, <b>markdown</b>-based description, and tags.
</div>
</div>
</div>
<div class="tour-section row">
<div class="col-md-7"><img src="/static/img/build-history.png" title="View Image - Quay.io"
data-screenshot-url="https://quay.io/repository/devtable/building/build"
class="img-responsive"></div>
<div class="col-md-5">
<div class="tour-section-title">Dockerfile Build in the cloud</div>
<div class="tour-section-description">
Like to use <b>Dockerfiles</b> to build your images? Simply upload your Dockerfile (and any additional files it needs) and we'll build your Dockerfile into an image and push it to your repository.
</div>
<div class="tour-section-description">
If you store your Dockerfile in <i class="fa fa-github fa-lg" style="margin: 6px;"></i><b>GitHub</b>, add a <b>Build Trigger</b> to your repository and we'll start a Dockerfile build for every change you make.
</div>
</div>
</div>
<div class="tour-section row">
<div class="col-md-7 col-md-push-5"><img src="/static/img/repo-admin.png" title="Repository Admin - Quay.io" data-screenshot-url="https://quay.io/repository/devtable/complex/admin" class="img-responsive"></div>
<div class="col-md-5 col-md-pull-7">
<div class="tour-section-title">Share at your control</div>
<div class="tour-section-description">
Share any repository with as many (or as few) users as you choose.
</div>
<div class="tour-section-description">Need a repository only for your team? Easily <b>share</b> with your team members.</div>
<div class="tour-section-description">Need finer grain control? Mark a user as <b>read-only</b> or <b>read/write</b>.</div>
<div class="tour-section-description">Have a build script or a deploy process that needs access? Generate an <b>access token</b> to grant revocable access for pushing or pulling.</div>
<div class="tour-section-description">Want to share with the world? Make your repository <b>fully public</b>.</div>
</div>
</div>
<div class="tour-section row">
<div class="col-md-7"><img src="/static/img/repo-changes.png" title="View Image - Quay.io" data-screenshot-url="https://quay.io/repository/devtable/image/..." class="img-responsive"></div>
<div class="col-md-5">
<div class="tour-section-title">Docker diff whenever you need it</div>
<div class="tour-section-description">
We wanted to know what was changing in each image of our repositories just as much as you do. So we added diffs. Now you can see exactly which files were <b>added</b>, <b>changed</b>, or <b>removed</b> for each image. We've also provided two awesome ways to view your changes, either in a filterable list, or in a drill down tree view.
</div>
</div>
</div>
<div style="border-top: 1px solid #eee; padding-top: 20px;">
<a href="https://mixpanel.com/f/partner"><img src="//cdn.mxpnl.com/site_media/images/partner/badge_light.png" alt="Mobile Analytics" /></a>
</div>
</div>
</div>

View file

@ -1,150 +1,3 @@
<div class="jumbotron landing">
<div class="container">
<div class="row messages">
<div class="col-md-7">
<div ng-show="user.anonymous">
<h1>Secure hosting for <b>private</b> Docker<a class="disclaimer-link" href="/disclaimer" target="_self">*</a> repositories</h1>
<h3>Use the Docker images <b>your team</b> needs with the safety of <b>private</b> repositories</h3>
<div class="sellcall"><a href="/plans/">Private repository plans starting at $12/mo</a></div>
</div>
<div ng-show="!user.anonymous">
<span class="namespace-selector" user="user" namespace="namespace" ng-show="user.organizations"></span>
<div class="resource-view" resource="my_repositories">
<!-- Repos -->
<div ng-show="my_repositories.value.length > 0">
<h2>Top Repositories</h2>
<div class="repo-listing" ng-repeat="repository in my_repositories.value">
<span class="repo-circle no-background" repo="repository"></span>
<a ng-href="/repository/{{ repository.namespace }}/{{ repository.name }}">{{repository.namespace}}/{{repository.name}}</a>
<div class="markdown-view description" content="repository.description" first-line-only="true"></div>
</div>
</div>
<!-- No Repos -->
<div ng-show="my_repositories.value.length == 0">
<div class="sub-message" style="margin-top: 20px">
<span ng-show="namespace != user.username">You don't have access to any repositories in this organization yet.</span>
<span ng-show="namespace == user.username">You don't have any repositories yet!</span>
<div class="options">
<a class="btn btn-primary" href="/repository/">Browse all repositories</a>
<a class="btn btn-success" href="/new/" ng-show="canCreateRepo(namespace)">Create a new repository</a>
</div>
</div>
</div>
</div>
</div>
</div> <!-- col -->
<div class="col-md-4 col-md-offset-1">
<div ng-show="user.anonymous">
<div class="signup-form"></div>
</div>
<div ng-show="!user.anonymous" class="user-welcome">
<img class="gravatar" src="//www.gravatar.com/avatar/{{ user.gravatar }}?s=128&d=identicon" />
<div class="sub-message">Welcome <b>{{ user.username }}</b>!</div>
<a ng-show="my_repositories.value" class="btn btn-primary" href="/repository/">Browse all repositories</a>
<a class="btn btn-success" href="/new/">Create a new repository</a>
</div>
</div> <!-- col -->
</div> <!-- row -->
<div class="row" ng-show="user.anonymous">
<div class="col-md-4 shoutout">
<i class="fa fa-lock"></i>
<b>Secure</b>
<span class="shoutout-expand">
Your data is transferred using <strong>SSL at all times</strong> and <strong>encrypted</strong> when at rest. More information available in our <a href="/security/">security guide</a>
</span>
</div>
<div class="col-md-4 shoutout">
<i class="fa fa-group"></i>
<b>Shareable</b>
<span class="shoutout-expand">
Have to share a repository? No problem! Share with anyone you choose
</span>
</div>
<div class="col-md-4 shoutout">
<i class="fa fa-cloud"></i>
<b>Cloud Hosted</b>
<span class="shoutout-expand">
Accessible from anywhere, anytime
</span>
</div>
</div> <!-- row -->
</div> <!-- container -->
</div> <!-- jumbotron -->
<div class="product-tour container" ng-show="user.anonymous">
<div class="tour-header row">
<div class="tour-shoutout-header"><i class="fa fa-chevron-circle-down"></i></div>
<div class="tour-shoutout">Take a tour of Quay</div>
</div>
<div class="tour-section row">
<div class="col-md-7"><img src="/static/img/user-home.png" title="User Home - Quay.io" data-screenshot-url="https://quay.io/" class="img-responsive"></div>
<div class="col-md-5">
<div class="tour-section-title">Customized for you</div>
<div class="tour-section-description">
Your personal home screen shows those repositories most important to you, ordered by recent activity.
</div>
<div class="tour-section-description">Keep up to date on the status of those repositories you deem important.</div>
</div>
</div>
<div class="tour-section row">
<div class="col-md-7 col-md-push-5"><img src="/static/img/repo-view.png" title="Repository View - Quay.io" data-screenshot-url="https://quay.io/repository/devtable/complex" class="img-responsive"></div>
<div class="col-md-5 col-md-pull-7">
<div class="tour-section-title">Useful views of respositories</div>
<div class="tour-section-description">
Each repository is presented with the maximum amount of useful information, including its image history, <b>markdown</b>-based description, and tags.
</div>
</div>
</div>
<div class="tour-section row">
<div class="col-md-7"><img src="/static/img/build-history.png" title="View Image - Quay.io"
data-screenshot-url="https://quay.io/repository/devtable/building/build"
class="img-responsive"></div>
<div class="col-md-5">
<div class="tour-section-title">Dockerfile Build in the cloud</div>
<div class="tour-section-description">
Like to use <b>Dockerfiles</b> to build your images? Simply upload your Dockerfile (and any additional files it needs) and we'll build your Dockerfile into an image and push it to your repository.
</div>
<div class="tour-section-description">
If you store your Dockerfile in <i class="fa fa-github fa-lg" style="margin: 6px;"></i><b>GitHub</b>, add a <b>Build Trigger</b> to your repository and we'll start a Dockerfile build for every change you make.
</div>
</div>
</div>
<div class="tour-section row">
<div class="col-md-7 col-md-push-5"><img src="/static/img/repo-admin.png" title="Repository Admin - Quay.io" data-screenshot-url="https://quay.io/repository/devtable/complex/admin" class="img-responsive"></div>
<div class="col-md-5 col-md-pull-7">
<div class="tour-section-title">Share at your control</div>
<div class="tour-section-description">
Share any repository with as many (or as few) users as you choose.
</div>
<div class="tour-section-description">Need a repository only for your team? Easily <b>share</b> with your team members.</div>
<div class="tour-section-description">Need finer grain control? Mark a user as <b>read-only</b> or <b>read/write</b>.</div>
<div class="tour-section-description">Have a build script or a deploy process that needs access? Generate an <b>access token</b> to grant revocable access for pushing or pulling.</div>
<div class="tour-section-description">Want to share with the world? Make your repository <b>fully public</b>.</div>
</div>
</div>
<div class="tour-section row">
<div class="col-md-7"><img src="/static/img/repo-changes.png" title="View Image - Quay.io" data-screenshot-url="https://quay.io/repository/devtable/image/..." class="img-responsive"></div>
<div class="col-md-5">
<div class="tour-section-title">Docker diff whenever you need it</div>
<div class="tour-section-description">
We wanted to know what was changing in each image of our repositories just as much as you do. So we added diffs. Now you can see exactly which files were <b>added</b>, <b>changed</b>, or <b>removed</b> for each image. We've also provided two awesome ways to view your changes, either in a filterable list, or in a drill down tree view.
</div>
</div>
</div>
<div style="border-top: 1px solid #eee; padding-top: 20px;">
<a href="https://mixpanel.com/f/partner"><img src="//cdn.mxpnl.com/site_media/images/partner/badge_light.png" alt="Mobile Analytics" /></a>
</div>
</div>
<div quay-include="{'Features.BILLING': 'landing-normal.html', '!Features.BILLING': 'landing-login.html'}" onload="chromify()">
<span class="quay-spinner"></span>
</div>

View file

@ -106,7 +106,7 @@
</td>
</tr>
<tr>
<td>Client Secret: <i class="fa fa-lock fa-lg" title="Keep this secret!" bs-tooltip style="margin-left: 10px"></i></td>
<td>Client Secret: <i class="fa fa-lock fa-lg" data-title="Keep this secret!" bs-tooltip style="margin-left: 10px"></i></td>
<td>
{{ application.client_secret }}
</td>

View file

@ -66,13 +66,14 @@
</div>
<!-- Plans Table -->
<div class="form-group nested plan-group">
<div class="form-group nested plan-group" quay-require="['BILLING']">
<strong>Choose your organization's plan</strong>
<div class="plans-table" plans="plans" current-plan="currentPlan"></div>
<div class="plans-table" plans="plans" current-plan="holder.currentPlan"></div>
</div>
<div class="button-bar">
<button class="btn btn-large btn-success" type="submit" ng-disabled="newOrgForm.$invalid || !currentPlan"
<button class="btn btn-large btn-success" type="submit"
ng-disabled="newOrgForm.$invalid || (Features.BILLING && !holder.currentPlan)"
analytics-on analytics-event="create_organization">
Create Organization
</button>

View file

@ -51,7 +51,7 @@
<div class="section">
<div class="repo-option">
<input type="radio" id="publicrepo" name="publicorprivate" ng-model="repo.is_public" value="1">
<i class="fa fa-unlock fa-large" title="Public Repository"></i>
<i class="fa fa-unlock fa-large" data-title="Public Repository"></i>
<div class="option-description">
<label for="publicrepo"><strong>Public</strong></label>
@ -60,7 +60,7 @@
</div>
<div class="repo-option">
<input type="radio" id="privaterepo" name="publicorprivate" ng-model="repo.is_public" value="0">
<i class="fa fa-lock fa-large" title="Private Repository"></i>
<i class="fa fa-lock fa-large" data-title="Private Repository"></i>
<div class="option-description">
<label for="privaterepo"><strong>Private</strong></label>
@ -75,7 +75,7 @@
<span ng-if="isUserNamespace">under your personal namespace</span>
<span ng-if="!isUserNamespace">under the organization <b>{{ repo.namespace }}</b></span>, you will need to upgrade your plan to
<b style="border-bottom: 1px dotted black;" data-html="true"
title="{{ '<b>' + planRequired.title + '</b><br>' + planRequired.privateRepos + ' private repositories' }}" bs-tooltip>
data-title="{{ '<b>' + planRequired.title + '</b><br>' + planRequired.privateRepos + ' private repositories' }}" bs-tooltip>
{{ planRequired.title }}
</b>.
This will cost $<span>{{ planRequired.price / 100 }}</span>/month.
@ -118,11 +118,11 @@
<label for="initDockerfile">Initialize from a <b>Dockerfile</b></label>
</div>
<!-- Zip file -->
<!-- Zip/TarGz file -->
<div class="repo-option">
<input type="radio" id="initZipfile" name="initialize" ng-model="repo.initialize" value="zipfile">
<i class="fa fa-archive fa-lg" style="padding: 6px; padding-left: 10px; padding-right: 8px;"></i>
<label for="initZipfile">Initialize from a ZIP file (containing a Dockerfile and other supporting files)</label>
<label for="initZipfile">Initialize from a <b>Dockerfile</b> inside a <code>.zip</code> or <code>.tar.gz</code> archive</label>
</div>
<!-- Github -->
@ -140,7 +140,7 @@
<div class="col-md-1"></div>
<div class="col-md-8">
<div class="section">
<div class="section-title">Upload <span ng-if="repo.initialize == 'dockerfile'">Dockerfile</span><span ng-if="repo.initialize == 'zipfile'">ZIP file</span></div>
<div class="section-title">Upload <span ng-if="repo.initialize == 'dockerfile'">Dockerfile</span><span ng-if="repo.initialize == 'zipfile'">Archive</span></div>
<div style="padding-top: 20px;">
<div class="initialize-repo">
<div class="dockerfile-build-form" repository="createdForBuild" upload-failed="handleBuildFailed(message)"

View file

@ -6,15 +6,23 @@
<!-- Side tabs -->
<div class="col-md-2">
<ul class="nav nav-pills nav-stacked">
<li class="active"><a href="javascript:void(0)" data-toggle="tab" data-target="#plan">Plan and Usage</a></li>
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#settings">Organization Settings</a></li>
<li class="active" quay-require="['BILLING']">
<a href="javascript:void(0)" data-toggle="tab" data-target="#plan">Plan and Usage</a>
</li>
<li quay-classes="{'!Features.BILLING': 'active'}">
<a href="javascript:void(0)" data-toggle="tab" data-target="#settings">Organization Settings</a>
</li>
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#logs" ng-click="loadLogs()">Usage Logs</a></li>
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#members" ng-click="loadMembers()">Members</a></li>
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#robots">Robot Accounts</a></li>
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#prototypes">Default Permissions</a></li>
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#applications" ng-click="loadApplications()">Applications</a></li>
<li ng-show="hasPaidPlan"><a href="javascript:void(0)" data-toggle="tab" data-target="#billingoptions">Billing</a></li>
<li ng-show="hasPaidPlan"><a href="javascript:void(0)" data-toggle="tab" data-target="#billing" ng-click="loadInvoices()">Billing History</a></li>
<li ng-show="hasPaidPlan" quay-require="['BILLING']">
<a href="javascript:void(0)" data-toggle="tab" data-target="#billingoptions">Billing</a>
</li>
<li ng-show="hasPaidPlan" quay-require="['BILLING']">
<a href="javascript:void(0)" data-toggle="tab" data-target="#billing" ng-click="loadInvoices()">Billing History</a>
</li>
</ul>
</div>
@ -22,12 +30,12 @@
<div class="col-md-10">
<div class="tab-content">
<!-- Plans tab -->
<div id="plan" class="tab-pane active">
<div id="plan" class="tab-pane active" quay-require="['BILLING']">
<div class="plan-manager" organization="orgname" plan-changed="planChanged(plan)"></div>
</div>
<!-- Organization settings tab -->
<div id="settings" class="tab-pane">
<div id="settings" class="tab-pane" quay-classes="{'!Features.BILLING': 'active'}">
<div class="quay-spinner" ng-show="changingOrganization"></div>
<div class="panel" ng-show="!changingOrganization">
@ -67,12 +75,12 @@
</div>
<!-- Billing Options tab -->
<div id="billingoptions" class="tab-pane">
<div id="billingoptions" class="tab-pane" quay-require="['BILLING']">
<div class="billing-options" organization="organization"></div>
</div>
<!-- Billing History tab -->
<div id="billing" class="tab-pane">
<div id="billing" class="tab-pane" quay-require="['BILLING']">
<div class="billing-invoices" organization="organization" visible="invoicesShown"></div>
</div>
@ -106,7 +114,7 @@
</span>
</td>
<td>
<a href="/organization/{{ organization.name }}/logs/{{ memberInfo.name }}" title="Member Usage Logs" bs-tooltip="tooltip.title">
<a href="/organization/{{ organization.name }}/logs/{{ memberInfo.name }}" data-title="Member Usage Logs" bs-tooltip="tooltip.title">
<i class="fa fa-book"></i>
</a>
</td>

View file

@ -15,7 +15,7 @@
<div class="row hidden-xs">
<div class="col-md-4 col-md-offset-8 col-sm-5 col-sm-offset-7 header-col" ng-show="organization.is_admin">
Team Permissions
<i class="info-icon fa fa-info-circle" data-placement="bottom" data-original-title="" title=""
<i class="info-icon fa fa-info-circle" data-placement="bottom" data-original-title="" data-title=""
data-content="Global permissions for the team and its members<br><br><dl><dt>Member</dt><dd>Permissions are assigned on a per repository basis</dd><dt>Creator</dt><dd>A team can create its own repositories</dd><dt>Admin</dt><dd>A team has full control of the organization</dd></dl>"></i>
</div>
</div>

View file

@ -4,13 +4,13 @@
</div>
<div class="button-bar-right">
<a href="/organizations/new/" title="Starts the process to create a new organization" bs-tooltip="tooltip.title">
<a href="/organizations/new/" data-title="Starts the process to create a new organization" bs-tooltip="tooltip.title">
<button class="btn btn-success">
<i class="fa fa-plus"></i>
Create New Organization
</button>
</a>
<a href="/user/?migrate" ng-show="!user.anonymous" title="Starts the process to convert this account into an organization" bs-tooltip="tooltip.title">
<a href="/user/?migrate" ng-show="!user.anonymous" data-title="Starts the process to convert this account into an organization" bs-tooltip="tooltip.title">
<button class="btn btn-primary">
<i class="fa fa-caret-square-o-right"></i>
Convert account
@ -43,7 +43,7 @@
<div class="tour-section row">
<div class="col-md-7"><img src="/static/img/org-repo-list.png" title="Repositories - Quay.io" data-screenshot-url="https://quay.io/repository/" class="img-responsive"></div>
<div class="col-md-7"><img src="/static/img/org-repo-list.png" data-title="Repositories - Quay.io" data-screenshot-url="https://quay.io/repository/" class="img-responsive"></div>
<div class="col-md-5">
<div class="tour-section-title">A central collection of repositories</div>
<div class="tour-section-description">
@ -57,7 +57,7 @@
</div>
<div class="tour-section row">
<div class="col-md-7 col-md-push-5"><img src="/static/img/org-admin.png" title="buynlarge Admin - Quay.io" data-screenshot-url="https://quay.io/organization/buynlarge/admin" class="img-responsive"></div>
<div class="col-md-7 col-md-push-5"><img src="/static/img/org-admin.png" data-title="buynlarge Admin - Quay.io" data-screenshot-url="https://quay.io/organization/buynlarge/admin" class="img-responsive"></div>
<div class="col-md-5 col-md-pull-7">
<div class="tour-section-title">Organization settings at a glance</div>
<div class="tour-section-description">
@ -73,7 +73,7 @@
</div>
<div class="tour-section row">
<div class="col-md-7"><img src="/static/img/org-logs.png" title="buynlarge Admin - Quay.io" data-screenshot-url="https://quay.io/organization/buynlarge" class="img-responsive"></div>
<div class="col-md-7"><img src="/static/img/org-logs.png" data-title="buynlarge Admin - Quay.io" data-screenshot-url="https://quay.io/organization/buynlarge" class="img-responsive"></div>
<div class="col-md-5">
<div class="tour-section-title">Logging for comprehensive analysis</div>
<div class="tour-section-description">
@ -94,7 +94,7 @@
</div>
<div class="tour-section row">
<div class="col-md-7 col-md-push-5"><img src="/static/img/org-teams.png" title="buynlarge - Quay.io" data-screenshot-url="https://quay.io/organization/buynlarge" class="img-responsive"></div>
<div class="col-md-7 col-md-push-5"><img src="/static/img/org-teams.png" data-title="buynlarge - Quay.io" data-screenshot-url="https://quay.io/organization/buynlarge" class="img-responsive"></div>
<div class="col-md-5 col-md-pull-7">
<div class="tour-section-title">Teams simplify access controls</div>
<div class="tour-section-description">
@ -115,7 +115,7 @@
</div>
<div class="tour-section row">
<div class="col-md-7"><img src="/static/img/org-repo-admin.png" title="buynlarge/orgrepo - Quay.io" data-screenshot-url="https://quay.io/repository/buynlarge/orgrepo" class="img-responsive"></div>
<div class="col-md-7"><img src="/static/img/org-repo-admin.png" data-title="buynlarge/orgrepo - Quay.io" data-screenshot-url="https://quay.io/repository/buynlarge/orgrepo" class="img-responsive"></div>
<div class="col-md-5">
<div class="tour-section-title">Fine-grained control of sharing</div>
<div class="tour-section-description">
@ -133,13 +133,13 @@
</div>
<div class="button-bar-right button-bar-bottom">
<a href="/organizations/new/" title="Starts the process to create a new organization" bs-tooltip="tooltip.title">
<a href="/organizations/new/" data-title="Starts the process to create a new organization" bs-tooltip="tooltip.title">
<button class="btn btn-success">
<i class="fa fa-plus"></i>
Create New Organization
</button>
</a>
<a href="/user/?migrate" ng-show="!user.anonymous" title="Starts the process to convert this account into an organization" bs-tooltip="tooltip.title">
<a href="/user/?migrate" ng-show="!user.anonymous" data-title="Starts the process to convert this account into an organization" bs-tooltip="tooltip.title">
<button class="btn btn-primary">
<i class="fa fa-caret-square-o-right"></i>
Convert account

View file

@ -7,7 +7,7 @@
<div class="feature">
<span class="context-tooltip" bs-tooltip="tooltip.title" data-container="body" data-placement="right"
title="All plans have unlimited public repositories">
data-title="All plans have unlimited public repositories">
<span class="hidden-sm-inline">Public Repositories</span>
<span class="visible-sm-inline">Public Repos</span>
</span>
@ -15,49 +15,49 @@
</div>
<div class="feature">
<span class="context-tooltip" bs-tooltip="tooltip.title" data-container="body" data-placement="right"
title="SSL encryption is enabled end-to-end for all operations">
data-title="SSL encryption is enabled end-to-end for all operations">
SSL Encryption
</span>
<i class="fa fa-lock visible-lg"></i>
</div>
<div class="feature">
<span class="context-tooltip" bs-tooltip="tooltip.title" data-container="body" data-placement="right"
title="Allows users or organizations to grant permissions in multiple repositories to the same non-login-capable account">
data-title="Allows users or organizations to grant permissions in multiple repositories to the same non-login-capable account">
Robot accounts
</span>
<i class="fa fa-wrench visible-lg"></i>
</div>
<div class="feature">
<span class="context-tooltip" bs-tooltip="tooltip.title" data-container="body" data-placement="right"
title="Repository images can be built directly from Dockerfiles">
data-title="Repository images can be built directly from Dockerfiles">
Dockerfile Build
</span>
<i class="fa fa-upload visible-lg"></i>
</div>
<div class="feature">
<span class="context-tooltip" bs-tooltip="tooltip.title" data-container="body" data-placement="right"
title="Grant subsets of users in an organization their own permissions, either on a global basis or a per-repository basis">
data-title="Grant subsets of users in an organization their own permissions, either on a global basis or a per-repository basis">
Teams
</span>
<i class="fa fa-group visible-lg"></i>
</div>
<div class="feature">
<span class="context-tooltip" bs-tooltip="tooltip.title" data-container="body" data-placement="right"
title="Every action take within an organization is logged in detail, with the ability to visualize logs and download them">
data-title="Every action take within an organization is logged in detail, with the ability to visualize logs and download them">
Logging
</span>
<i class="fa fa-bar-chart-o visible-lg"></i>
</div>
<div class="feature">
<span class="context-tooltip" bs-tooltip="tooltip.title" data-container="body" data-placement="right"
title="Administrators can view and download the full invoice history for their organization">
data-title="Administrators can view and download the full invoice history for their organization">
Invoice History
</span>
<i class="fa fa-calendar visible-lg"></i>
</div>
<div class="feature">
<span class="context-tooltip" bs-tooltip="tooltip.title" data-container="body" data-placement="right"
title="All plans have a 14-day free trial">
data-title="All plans have a 14-day free trial">
<span class="hidden-sm-inline">14-Day Free Trial</span>
<span class="visible-sm-inline">14-Day Trial</span>
</span>

View file

@ -50,7 +50,7 @@
<!-- Status Image -->
<a ng-href="/repository/{{ repo.namespace }}/{{ repo.name }}" ng-if="repo && repo.name">
<img ng-src="/repository/{{ repo.namespace }}/{{ repo.name }}/status?token={{ repo.status_token }}" title="Docker Repository on Quay.io">
<img ng-src="/repository/{{ repo.namespace }}/{{ repo.name }}/status?token={{ repo.status_token }}" data-title="Docker Repository on Quay.io">
</a>
<!-- Embed formats -->
@ -270,10 +270,16 @@
Setting up trigger
</div>
<div ng-show="trigger.is_active" class="trigger-description" trigger="trigger"></div>
<div class="trigger-pull-credentials" ng-if="trigger.is_active && trigger.pull_robot">
<span class="context-tooltip" data-title="The credentials used by the builder when pulling images" bs-tooltip>
Pull Credentials:
</span>
<span class="entity-reference" entity="trigger.pull_robot"></span>
</div>
</td>
<td style="white-space: nowrap;">
<div class="dropdown" style="display: inline-block" ng-visible="trigger.is_active">
<button class="btn btn-default dropdown-toggle" data-toggle="dropdown" title="Build History" bs-tooltip="tooltip.title" data-container="body"
<button class="btn btn-default dropdown-toggle" data-toggle="dropdown" data-title="Build History" bs-tooltip="tooltip.title" data-container="body"
ng-click="loadTriggerBuildHistory(trigger)">
<i class="fa fa-tasks"></i>
<b class="caret"></b>
@ -291,7 +297,7 @@
</div>
<div class="dropdown" style="display: inline-block">
<button class="btn btn-default dropdown-toggle" data-toggle="dropdown" title="Trigger Settings" bs-tooltip="tooltip.title" data-container="body">
<button class="btn btn-default dropdown-toggle" data-toggle="dropdown" data-title="Trigger Settings" bs-tooltip="tooltip.title" data-container="body">
<i class="fa fa-cog"></i>
<b class="caret"></b>
</button>
@ -371,36 +377,17 @@
</div>
</div>
<!-- Auth dialog -->
<div class="docker-auth-dialog" username="'$token'" token="shownToken.code"
shown="!!shownToken" counter="shownTokenCounter">
<i class="fa fa-key"></i> {{ shownToken.friendlyName }}
</div>
<!-- Modal message dialog -->
<div class="modal fade" id="setupTriggerModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">Setup new build trigger</h4>
</div>
<div class="modal-body">
<div class="trigger-description-element" ng-switch on="currentSetupTrigger.service">
<div ng-switch-when="github">
<div class="trigger-setup-github" repository="repo" trigger="currentSetupTrigger"></div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" ng-disabled="!currentSetupTrigger.$ready" ng-click="finishSetupTrigger(currentSetupTrigger)">Finished</button>
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
<!-- Setup trigger dialog-->
<div class="setup-trigger-dialog" repository="repo"
trigger="currentSetupTrigger"
canceled="cancelSetupTrigger(trigger)"
counter="showTriggerSetupCounter"></div>
<!-- Modal message dialog -->
<div class="modal fade" id="cannotchangeModal">

View file

@ -55,7 +55,7 @@
<i class="fa fa-archive"></i>
<a href="/repository/{{ repo.namespace }}/{{ repo.name }}/build/{{ currentBuild.id }}/buildpack"
style="display: inline-block; margin-left: 6px" bs-tooltip="tooltip.title"
title="View the uploaded build package for this build">Build Package</a>
data-title="View the uploaded build package for this build">Build Package</a>
</span>
</div>
<span class="phase-icon" ng-class="build.phase"></span>

View file

@ -4,7 +4,7 @@
<div class="button-bar-right">
<a href="/new/">
<button class="btn btn-success">
<i class="fa fa-upload user-tool" title="Create new repository"></i>
<i class="fa fa-upload user-tool" data-title="Create new repository"></i>
Create Repository
</button>
</a>
@ -60,11 +60,11 @@
<div class="description markdown-view" content="repository.description" first-line-only="true"></div>
</div>
<div class="page-controls">
<button class="btn btn-default" title="Previous Page" bs-tooltip="title" ng-show="page > 1"
<button class="btn btn-default" data-title="Previous Page" bs-tooltip="title" ng-show="page > 1"
ng-click="movePublicPage(-1)">
<i class="fa fa-chevron-left"></i>
</button>
<button class="btn btn-default" title="Next Page" bs-tooltip="title" ng-show="page < publicPageCount"
<button class="btn btn-default" data-title="Next Page" bs-tooltip="title" ng-show="page < publicPageCount"
ng-click="movePublicPage(1)">
<i class="fa fa-chevron-right"></i>
</button>

View file

@ -1,7 +1,7 @@
<div class="container signin-container">
<div class="row">
<div class="col-sm-6 col-sm-offset-3">
<div class="user-setup"></div>
<div class="user-setup" redirect-url="'/'"></div>
</div>
</div>
</div>

View file

@ -0,0 +1,149 @@
<div class="container" quay-show="Features.SUPER_USERS">
<div class="alert alert-info">
This panel provides administrator access to <strong>super users of this installation of Quay.io</strong>. Super users can be managed in the configuration for this installation.
</div>
<div class="row">
<!-- Side tabs -->
<div class="col-md-2">
<ul class="nav nav-pills nav-stacked">
<li class="active">
<a href="javascript:void(0)" data-toggle="tab" data-target="#license">License and Usage</a>
</li>
<li>
<a href="javascript:void(0)" data-toggle="tab" data-target="#users" ng-click="loadUsers()">Users</a>
</li>
</ul>
</div>
<!-- Content -->
<div class="col-md-10">
<div class="tab-content">
<!-- License tab -->
<div id="license" class="tab-pane active">
<div class="quay-spinner 3x" ng-show="usageLoading"></div>
<!-- Chart -->
<div>
<div id="seat-usage-chart" class="usage-chart limit-{{limit}}"></div>
<span class="usage-caption" ng-show="chart">Seat Usage</span>
</div>
</div>
<!-- Users tab -->
<div id="users" class="tab-pane">
<div class="quay-spinner" ng-show="!users"></div>
<div class="alert alert-error" ng-show="usersError">
{{ usersError }}
</div>
<div ng-show="users">
<div class="side-controls">
<div class="result-count">
Showing {{(users | filter:search | limitTo:100).length}} of
{{(users | filter:search).length}} matching users
</div>
<div class="filter-input">
<input id="log-filter" class="form-control" placeholder="Filter Users" type="text" ng-model="search.$">
</div>
</div>
<table class="table">
<thead>
<th>Username</th>
<th>E-mail address</th>
<th></th>
<th></th>
</thead>
<tr ng-repeat="current_user in (users | filter:search | orderBy:'username' | limitTo:100)"
class="user-row"
ng-class="current_user.super_user ? 'super-user' : ''">
<td>
<i class="fa fa-user" style="margin-right: 6px"></i>
{{ current_user.username }}
</td>
<td>
<a href="mailto:{{ current_user.email }}">{{ current_user.email }}</a>
</td>
<td class="user-class">
<span ng-if="current_user.super_user">Super user</span>
</td>
<td>
<div class="dropdown" ng-if="user.username != current_user.username && !user.super_user">
<button class="btn btn-default dropdown-toggle" data-toggle="dropdown">
<i class="fa fa-ellipsis-h"></i>
</button>
<ul class="dropdown-menu">
<li>
<a href="javascript:void(0)" ng-click="showChangePassword(current_user)">
<i class="fa fa-key"></i> Change Password
</a>
<a href="javascript:void(0)" ng-click="showDeleteUser(current_user)">
<i class="fa fa-times"></i> Delete User
</a>
</li>
</ul>
</div>
</td>
</tr>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- Modal message dialog -->
<div class="modal fade" id="confirmDeleteUserModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">Delete User?</h4>
</div>
<div class="modal-body">
<div class="alert alert-danger">
This operation <strong>cannot be undone</strong> and will <strong>delete any repositories owned by the user</strong>.
</div>
Are you <strong>sure</strong> you want to delete user <strong>{{ userToDelete.username }}</strong>?
</div>
<div class="modal-footer">
<button type="button" class="btn btn-danger" ng-click="deleteUser(userToDelete)">Delete User</button>
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
<!-- Modal message dialog -->
<div class="modal fade" id="changePasswordModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">Change User Password</h4>
</div>
<div class="modal-body">
<div class="alert alert-warning">
The user will no longer be able to access Quay.io with their current password
</div>
<form class="form-change" id="changePasswordForm" name="changePasswordForm" data-trigger="manual">
<input type="password" class="form-control" placeholder="User's new password" ng-model="userToChange.password" required ng-pattern="/^.{8,}$/">
<input type="password" class="form-control" placeholder="Verify the new password" ng-model="userToChange.repeatPassword"
match="userToChange.password" required ng-pattern="/^.{8,}$/">
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" ng-click="changeUserPassword(userToChange)"
ng-disabled="changePasswordForm.$invalid">Change User Password</button>
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
</div>

View file

@ -20,15 +20,24 @@
<!-- Side tabs -->
<div class="col-md-2">
<ul class="nav nav-pills nav-stacked">
<li class="active"><a href="javascript:void(0)" data-toggle="tab" data-target="#plan">Plan and Usage</a></li>
<li ng-show="hasPaidPlan"><a href="javascript:void(0)" data-toggle="tab" data-target="#billingoptions">Billing Options</a></li>
<li ng-show="hasPaidBusinessPlan"><a href="javascript:void(0)" data-toggle="tab" data-target="#billing" ng-click="loadInvoices()">Billing History</a></li>
<!-- Billing Related -->
<li class="active" quay-require="['BILLING']"><a href="javascript:void(0)" data-toggle="tab" data-target="#plan">Plan and Usage</a></li>
<li ng-show="hasPaidPlan" quay-require="['BILLING']">
<a href="javascript:void(0)" data-toggle="tab" data-target="#billingoptions">Billing Options</a>
</li>
<li ng-show="hasPaidBusinessPlan" quay-require="['BILLING']">
<a href="javascript:void(0)" data-toggle="tab" data-target="#billing" ng-click="loadInvoices()">Billing History</a>
</li>
<!-- Non-billing -->
<li quay-classes="{'!Features.BILLING': 'active'}"><a href="javascript:void(0)" data-toggle="tab" data-target="#email">Account E-mail</a></li>
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#robots">Robot Accounts</a></li>
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#email">Account E-mail</a></li>
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#password">Change Password</a></li>
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#github">GitHub Login</a></li>
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#github" quay-require="['GITHUB_LOGIN']">GitHub Login</a></li>
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#authorized" ng-click="loadAuthedApps()">Authorized Applications</a></li>
<li ng-show="hasPaidBusinessPlan"><a href="javascript:void(0)" data-toggle="tab" data-target="#logs" ng-click="loadLogs()">Usage Logs</a></li>
<li quay-show="Features.USER_LOG_ACCESS || hasPaidBusinessPlan">
<a href="javascript:void(0)" data-toggle="tab" data-target="#logs" ng-click="loadLogs()">Usage Logs</a>
</li>
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#migrate" id="migrateTab">Convert to Organization</a></li>
</ul>
</div>
@ -60,10 +69,10 @@
<td>
<img src="//www.gravatar.com/avatar/{{ authInfo.gravatar }}?s=16&d=identicon">
<a href="{{ authInfo.application.url }}" ng-if="authInfo.application.url" target="_blank"
title="{{ authInfo.application.description || authInfo.application.name }}" bs-tooltip>
data-title="{{ authInfo.application.description || authInfo.application.name }}" bs-tooltip>
{{ authInfo.application.name }}
</a>
<span ng-if="!authInfo.application.url" title="{{ authInfo.application.description || authInfo.application.name }}" bs-tooltip>
<span ng-if="!authInfo.application.url" data-title="{{ authInfo.application.description || authInfo.application.name }}" bs-tooltip>
{{ authInfo.application.name }}
</span>
<span class="by">{{ authInfo.application.organization.name }}</span>
@ -71,7 +80,7 @@
<td>
<span class="label label-default scope"
ng-class="{'repo:admin': 'label-primary', 'repo:write': 'label-success', 'repo:create': 'label-success'}[scopeInfo.scope]"
ng-repeat="scopeInfo in authInfo.scopes" title="{{ scopeInfo.description }}" bs-tooltip>
ng-repeat="scopeInfo in authInfo.scopes" data-title="{{ scopeInfo.description }}" bs-tooltip>
{{ scopeInfo.scope }}
</span>
</td>
@ -91,12 +100,12 @@
</div>
<!-- Plans tab -->
<div id="plan" class="tab-pane active">
<div id="plan" class="tab-pane active" quay-require="['BILLING']">
<div class="plan-manager" user="user.username" ready-for-plan="readyForPlan()" plan-changed="planChanged(plan)"></div>
</div>
<!-- E-mail address tab -->
<div id="email" class="tab-pane">
<div id="email" class="tab-pane" quay-classes="{'!Features.BILLING': 'active'}">
<div class="row">
<div class="alert alert-success" ng-show="changeEmailSent">An e-mail has been sent to {{ sentEmail }} to verify the change.</div>
@ -115,8 +124,8 @@
<div class="panel-title">Change e-mail address</div>
<div class="panel-body">
<form class="form-change col-md-6" id="changeEmailForm" name="changeEmailForm" ng-submit="changeEmail()" data-trigger="manual"
data-content="{{ changeEmailError }}" data-placement="right" ng-show="!awaitingConfirmation && !registering">
<form class="form-change col-md-6" id="changeEmailForm" name="changeEmailForm" ng-submit="changeEmail()"
ng-show="!awaitingConfirmation && !registering">
<input type="email" class="form-control" placeholder="Your new e-mail address" ng-model="cuser.email" required>
<button class="btn btn-primary" ng-disabled="changeEmailForm.$invalid || cuser.email == user.email" type="submit">Change E-mail Address</button>
</form>
@ -137,11 +146,12 @@
<span class="help-block" ng-show="changePasswordSuccess">Password changed successfully</span>
<div ng-show="!updatingUser" class="panel-body">
<form class="form-change col-md-6" id="changePasswordForm" name="changePasswordForm" ng-submit="changePassword()" data-trigger="manual"
data-content="{{ changePasswordError }}" data-placement="right" ng-show="!awaitingConfirmation && !registering">
<input type="password" class="form-control" placeholder="Your new password" ng-model="cuser.password" required>
<form class="form-change col-md-6" id="changePasswordForm" name="changePasswordForm" ng-submit="changePassword()"
ng-show="!awaitingConfirmation && !registering">
<input type="password" class="form-control" placeholder="Your new password" ng-model="cuser.password" required
ng-pattern="/^.{8,}$/">
<input type="password" class="form-control" placeholder="Verify your new password" ng-model="cuser.repeatPassword"
match="cuser.password" required>
match="cuser.password" required ng-pattern="/^.{8,}$/">
<button class="btn btn-danger" ng-disabled="changePasswordForm.$invalid" type="submit"
analytics-on analytics-event="change_pass">Change Password</button>
</form>
@ -151,7 +161,7 @@
</div>
<!-- Github tab -->
<div id="github" class="tab-pane">
<div id="github" class="tab-pane" quay-require="['GITHUB_LOGIN']">
<div class="loading" ng-show="!cuser">
<div class="quay-spinner 3x"></div>
</div>
@ -160,7 +170,7 @@
<div class="panel-title">GitHub Login:</div>
<div class="panel-body">
<div ng-show="githubLogin" class="lead col-md-8">
<i class="fa fa-github fa-lg" style="margin-right: 6px;" title="GitHub" bs-tooltip="tooltip.title"></i>
<i class="fa fa-github fa-lg" style="margin-right: 6px;" data-title="GitHub" bs-tooltip="tooltip.title"></i>
<b><a href="https://github.com/{{githubLogin}}" target="_blank">{{githubLogin}}</a></b>
</div>
<div ng-show="!githubLogin" class="col-md-8">
@ -177,12 +187,12 @@
</div>
<!-- Billing options tab -->
<div id="billingoptions" class="tab-pane">
<div id="billingoptions" class="tab-pane" quay-require="['BILLING']">
<div class="billing-options" user="user"></div>
</div>
<!-- Billing History tab -->
<div id="billing" class="tab-pane">
<div id="billing" class="tab-pane" quay-require="['BILLING']">
<div class="billing-invoices" user="user" visible="invoicesShown"></div>
</div>
@ -234,13 +244,14 @@
</div>
<!-- Plans Table -->
<div class="form-group plan-group">
<div class="form-group plan-group" quay-require="['BILLING']">
<label>Organization Plan</label>
<div class="plans-table" plans="orgPlans" current-plan="org.plan"></div>
</div>
<div class="button-bar">
<button class="btn btn-large btn-danger" type="submit" ng-disabled="convertForm.$invalid || !org.plan"
<button class="btn btn-large btn-danger" type="submit"
ng-disabled="convertForm.$invalid || (Features.BILLING && !org.plan)"
analytics-on analytics-event="convert_to_organization">
Convert To Organization
</button>

View file

@ -17,7 +17,7 @@
<!-- Builds -->
<div class="dropdown" data-placement="top" style="display: inline-block"
bs-tooltip=""
title="{{ runningBuilds.length ? 'Dockerfile Builds Running: ' + (runningBuilds.length) : 'Dockerfile Build' }}"
data-title="{{ runningBuilds.length ? 'Dockerfile Builds Running: ' + (runningBuilds.length) : 'Dockerfile Build' }}"
ng-show="repo.can_write || buildHistory.length">
<button class="btn btn-default dropdown-toggle" data-toggle="dropdown">
<i class="fa fa-tasks fa-lg"></i>
@ -51,15 +51,15 @@
<!-- Admin -->
<a id="admin-cog" href="{{ '/repository/' + repo.namespace + '/' + repo.name + '/admin' }}"
ng-show="repo.can_admin">
<button class="btn btn-default" title="Repository Settings" bs-tooltip="tooltip" data-placement="top">
<button class="btn btn-default" data-title="Repository Settings" bs-tooltip="tooltip" data-placement="top">
<i class="fa fa-cog fa-lg"></i></button></a>
<!-- Pull Command -->
<span class="pull-command visible-md-inline">
<div class="pull-container" title="Pull repository" bs-tooltip="tooltip.title">
<div class="pull-container" data-title="Pull repository" bs-tooltip="tooltip.title">
<div class="input-group">
<input id="pull-text" type="text" class="form-control" value="{{ 'docker pull quay.io/' + repo.namespace + '/' + repo.name }}" readonly>
<span id="copyClipboard" class="input-group-addon" title="Copy to Clipboard" data-clipboard-target="pull-text">
<input id="pull-text" type="text" class="form-control" value="{{ 'docker pull ' + Config.getDomain() + '/' + repo.namespace + '/' + repo.name }}" readonly>
<span id="copyClipboard" class="input-group-addon" data-title="Copy to Clipboard" data-clipboard-target="pull-text">
<i class="fa fa-copy"></i>
</span>
</div>
@ -87,13 +87,13 @@
<div class="panel-heading">How to push a new image to this repository:</div>
<div class="panel-body">
First login to Quay.io (if you have not done so already):
<pre class="command">sudo docker login quay.io</pre>
<pre class="command">sudo docker login {{ Config.getDomain() }}</pre>
Tag an image to this repository:
<pre class="command">sudo docker tag <i>0u123imageidgoeshere</i> quay.io/{{repo.namespace}}/{{repo.name}}</pre>
<pre class="command">sudo docker tag <i>0u123imageidgoeshere</i> {{ Config.getDomain() }}/{{repo.namespace}}/{{repo.name}}</pre>
Push the image to this repository:
<pre class="command">sudo docker push quay.io/{{repo.namespace}}/{{repo.name}}</pre>
<pre class="command">sudo docker push {{ Config.getDomain() }}/{{repo.namespace}}/{{repo.name}}</pre>
</div>
</div>
</div>
@ -149,10 +149,10 @@
</ul>
</div>
<span class="right-tag-controls">
<i class="fa fa-tag" title="Tags" bs-tooltip="title">
<i class="fa fa-tag" data-title="Tags" bs-tooltip="title">
<span class="tag-count">{{getTagCount(repo)}}</span>
</i>
<i class="fa fa-archive" title="Images" bs-tooltip="title">
<i class="fa fa-archive" data-title="Images" bs-tooltip="title">
<span class="tag-count">{{imageHistory.value.length}}</span>
</i>
</span>
@ -166,7 +166,7 @@
<dd am-time-ago="parseDate(currentTag.image.created)"></dd>
<dt>Total Compressed Size</dt>
<dd><span class="context-tooltip"
title="The amount of data sent between Docker and Quay.io when pushing/pulling"
data-title="The amount of data sent between Docker and Quay.io when pushing/pulling"
bs-tooltip="tooltip.title" data-container="body">{{ getTotalSize(currentTag) | bytes }}</span>
</dd>
</dl>
@ -175,7 +175,7 @@
<div class="tag-image-size" ng-repeat="image in getImagesForTagBySize(currentTag) | limitTo: 10">
<span class="size-limiter">
<span class="size-bar" style="{{ 'width:' + (image.size / getTotalSize(currentTag)) * 100 + '%' }}"
bs-tooltip="" title="{{ image.size | bytes }}"></span>
bs-tooltip="" data-title="{{ image.size | bytes }}"></span>
</span>
<span class="size-title"><a href="javascript:void(0)" ng-click="setImage(image.id, true)">{{ image.id.substr(0, 12) }}</a></span>
</div>
@ -197,14 +197,14 @@
</div>
<div class="image-section">
<i class="fa fa-code section-icon" bs-tooltip="tooltip.title" title="Full Image ID"></i>
<i class="fa fa-code section-icon" bs-tooltip="tooltip.title" data-title="Full Image ID"></i>
<span class="section-info">
<a href="{{'/repository/' + repo.namespace + '/' + repo.name + '/image/' + currentImage.id}}">{{ currentImage.id }}</a>
</span>
</div>
<div class="image-section">
<i class="fa fa-tag section-icon" bs-tooltip="tooltip.title" title="Current Tags"></i>
<i class="fa fa-tag section-icon" bs-tooltip="tooltip.title" data-title="Current Tags"></i>
<span class="section-info section-info-with-dropdown">
<a class="label tag label-default" ng-repeat="tag in currentImage.tags"
href="/repository/{{ repo.namespace }}/{{ repo.name }}?tag={{ tag }}">
@ -214,7 +214,7 @@
<div class="dropdown" data-placement="top" ng-if="repo.can_write || currentImage.tags">
<a href="javascript:void(0)" class="dropdown-button" data-toggle="dropdown" bs-tooltip="tooltip.title" title="Manage Tags"
<a href="javascript:void(0)" class="dropdown-button" data-toggle="dropdown" bs-tooltip="tooltip.title" data-title="Manage Tags"
data-container="body">
<b class="caret"></b>
</a>
@ -236,17 +236,17 @@
</div>
<div class="image-section" ng-if="currentImage.command && currentImage.command.length">
<i class="fa fa-terminal section-icon" bs-tooltip="tooltip.title" title="Image Command"></i>
<i class="fa fa-terminal section-icon" bs-tooltip="tooltip.title" data-title="Image Command"></i>
<span class="section-info">
<span class="formatted-command trimmed"
data-html="true"
bs-tooltip="" title="{{ getTooltipCommand(currentImage) }}"
bs-tooltip="" data-title="{{ getTooltipCommand(currentImage) }}"
data-placement="top">{{ getFormattedCommand(currentImage) }}</span>
</span>
</div>
<div class="image-section">
<i class="fa fa-calendar section-icon" bs-tooltip="tooltip.title" title="Created"></i>
<i class="fa fa-calendar section-icon" bs-tooltip="tooltip.title" data-title="Created"></i>
<span class="section-info">
<dd am-time-ago="parseDate(currentImage.created)"></dd>
</span>
@ -254,7 +254,7 @@
<div class="image-section">
<i class="fa fa-cloud-upload section-icon" bs-tooltip="tooltip.title"
title="The amount of data sent between Docker and Quay.io when pushing/pulling"></i>
data-title="The amount of data sent between Docker and Quay.io when pushing/pulling"></i>
<span class="section-info">{{ currentImage.size | bytes }}</span>
</div>
@ -263,19 +263,19 @@
<div class="changes-container small-changes-container section-info"
ng-show="currentImageChanges.changed.length || currentImageChanges.added.length || currentImageChanges.removed.length">
<div class="changes-count-container image-section">
<i class="fa fa-code-fork section-icon" bs-tooltip="tooltip.title" title="File Changes"></i>
<i class="fa fa-code-fork section-icon" bs-tooltip="tooltip.title" data-title="File Changes"></i>
<div style="float: right; display: inline-block">
<span class="change-count added" ng-show="currentImageChanges.added.length > 0" title="Files Added"
<span class="change-count added" ng-show="currentImageChanges.added.length > 0" data-title="Files Added"
bs-tooltip="tooltip.title" data-placement="top" data-container="body">
<i class="fa fa-plus-square"></i>
<b>{{currentImageChanges.added.length}}</b>
</span>
<span class="change-count removed" ng-show="currentImageChanges.removed.length > 0" title="Files Removed"
<span class="change-count removed" ng-show="currentImageChanges.removed.length > 0" data-title="Files Removed"
bs-tooltip="tooltip.title" data-placement="top" data-container="body">
<i class="fa fa-minus-square"></i>
<b>{{currentImageChanges.removed.length}}</b>
</span>
<span class="change-count changed" ng-show="currentImageChanges.changed.length > 0" title="Files Changed"
<span class="change-count changed" ng-show="currentImageChanges.changed.length > 0" data-title="Files Changed"
bs-tooltip="tooltip.title" data-placement="top" data-container="body">
<i class="fa fa-pencil-square"></i>
<b>{{currentImageChanges.changed.length}}</b>

View file

@ -15,8 +15,8 @@
<p>Once a container has terminated in Docker, the next step is to <i>commit</i> the container to an image, and then <i>tag</i> that image with a relevant name so it can be saved to a repository.</p>
<p>Docker lets us do this in one step with the <i>commit</i> command. To do so, we run the <code>docker commit</code> with the container ID from the previous step and tag it to be a repository under <code>quay.io</code>.
<p>Docker lets us do this in one step with the <i>commit</i> command. To do so, we run the <code>docker commit</code> with the container ID from the previous step and tag it to be a repository under <code>{{ tour.tourScope.domainName }}</code>.
<pre class="command">
<code ng-show="tour.tourScope.showSudo">sudo </code>docker commit <var class="var1">{{ tour.tourScope.containerId || 'containerId' }}</var> quay.io/{{ tour.tourScope.username }}/<var class="var2">{{ tour.tourScope.repoName || 'myfirstrepo' }}</var>
<code ng-show="tour.tourScope.showSudo">sudo </code>docker commit <var class="var1">{{ tour.tourScope.containerId || 'containerId' }}</var> {{ tour.tourScope.domainName }}/{{ tour.tourScope.username }}/<var class="var2">{{ tour.tourScope.repoName || 'myfirstrepo' }}</var>
</pre>

View file

@ -10,7 +10,7 @@
<p>The first step when using Quay.io is to login via the <code>docker login</code> command.</p>
<p>Enter your Quay.io username and your password when prompted.</p>
<pre class="command"><code ng-show="tour.tourScope.showSudo">sudo </code>docker login quay.io
<pre class="command"><code ng-show="tour.tourScope.showSudo">sudo </code>docker login {{ tour.tourScope.domainName }}
Username: {{ tour.tourScope.username }}
Password: (password here)
Email: {{ tour.tourScope.email }}</pre>

View file

@ -1,8 +1,8 @@
<p>Now that we've tagged our image with a repository name, we can <code>push</code> the repository to Quay.io:</p>
<pre class="command">
<code ng-show="tour.tourScope.showSudo">sudo </code>docker push quay.io/{{ tour.tourScope.username }}/<var class="var2">{{ tour.tourScope.repoName || 'myfirstrepo' }}</var>
The push refers to a repository [quay.io/{{ tour.tourScope.username }}/<var class="var2">{{ tour.tourScope.repoName || 'myfirstrepo' }}</var>] (len: 1)
<code ng-show="tour.tourScope.showSudo">sudo </code>docker push {{ tour.tourScope.domainName }}/{{ tour.tourScope.username }}/<var class="var2">{{ tour.tourScope.repoName || 'myfirstrepo' }}</var>
The push refers to a repository [{{ tour.tourScope.domainName }}/{{ tour.tourScope.username }}/<var class="var2">{{ tour.tourScope.repoName || 'myfirstrepo' }}</var>] (len: 1)
Sending image list
Pushing repository quay.io/{{ tour.tourScope.username }}/<var class="var2">{{ tour.tourScope.repoName || 'myfirstrepo' }}</var> (1 tags)
Pushing repository {{ tour.tourScope.domainName }}/{{ tour.tourScope.username }}/<var class="var2">{{ tour.tourScope.repoName || 'myfirstrepo' }}</var> (1 tags)
</pre>