Merge remote-tracking branch 'origin/orgs'

Conflicts:
	static/partials/repo-admin.html
This commit is contained in:
yackob03 2013-11-08 16:44:49 -05:00
commit d7cae4fbca
72 changed files with 18683 additions and 1028 deletions

View file

@ -2,6 +2,70 @@
font-family: 'Droid Sans', sans-serif;
}
.button-hidden {
visibility: hidden;
}
.organization-header-element {
padding: 20px;
margin-bottom: 20px;
border-bottom: 1px solid #eee;
font-size: 20px;
}
.organization-header-element .organization-name {
display: inline-block;
margin-left: 10px;
}
.organization-header-element .divider {
color: #aaa;
margin-left: 10px;
margin-right: 10px;
}
.organization-header-element .organization-name {
display: inline-block;
font-size: 20px;
margin-left: 10px;
}
.organization-header-element .team-name {
text-transform: none;
}
.organization-header-element .header-buttons {
float: right;
}
.namespace-selector-dropdown .namespace {
padding: 6px;
padding-left: 10px;
cursor: pointer;
font-size: 14px;
}
.namespace-selector-dropdown .namespace-item {
position: relative;
}
.namespace-selector-dropdown .namespace-item .fa {
position: absolute;
right: 12px;
top: 12px;
color: #aaa;
}
.namespace-selector-dropdown .namespace-item.disabled img {
-webkit-filter: grayscale(1);
opacity: 0.5;
}
.namespace-selector-dropdown .namespace-item .tooltip-inner {
min-width: 200px;
}
.user-notification {
background: red;
}
@ -179,10 +243,6 @@
color: #444 !important;
}
.new-repo .new-header span {
font-size: 22px;
}
.new-repo .new-header .popover {
font-size: 14px;
}
@ -277,24 +337,46 @@
color: #428bca;
}
.plans .all-plans .business-feature {
color: #46ac39;
}
.plans-list {
text-align: center;
margin-bottom: 25px;
}
.plans-list .plan-container {
padding: 5px;
}
.plans-list .plan {
width: 245px;
vertical-align: top;
display: inline-block;
padding: 10px;
margin-right: 10px;
border: 1px solid #eee;
border-top: 4px solid #94C9F7;
margin-top: 10px;
font-size: 1.4em;
margin-top: 5px;
}
.plans-list .plan.small {
border: 1px solid #ddd;
border-top: 4px solid #428bca;
margin-top: 0px;
font-size: 1.6em;
}
.plans-list .plan.business-plan {
border: 1px solid #eee;
border-top: 4px solid #94F794;
}
.plans-list .plan.bus-small {
border: 1px solid #ddd;
border-top: 4px solid #47A447;
margin-top: 0px;
font-size: 1.6em;
}
.plans-list .plan:last-child {
@ -317,7 +399,7 @@
}
.plan-price:after {
content: "/ month";
content: "/ mo";
position: absolute;
bottom: 0px;
right: 20px;
@ -333,6 +415,10 @@
color: #428bca;
}
.plans-list .plan.business-plan .count b {
color: #46ac39;
}
.plans-list .plan .description {
font-size: 1em;
font-size: 16px;
@ -354,14 +440,6 @@
margin-right: 5px;
}
.plans-list .plan.small {
border: 1px solid #ddd;
border-top: 4px solid #428bca;
margin-top: 0px;
font-size: 1.6em;
}
.plans .plan-faq dd{
margin-bottom: 20px;
}
@ -575,14 +653,20 @@ form input.ng-valid.ng-dirty {
}
.user-mini-listing {
.entity-mini-listing {
margin: 2px;
}
.user-mini-listing i {
.entity-mini-listing i {
margin-right: 8px;
}
.entity-mini-listing .warning {
margin-top: 6px;
font-size: 10px;
padding: 4px;
}
.editable {
position: relative;
}
@ -611,9 +695,8 @@ p.editable {
display: inline-block;
}
p.editable .content:empty:after {
p.editable .empty {
display: inline-block;
content: "(Click to add)";
color: #aaa;
}
@ -867,6 +950,25 @@ p.editable:hover i {
margin-bottom: 40px;
}
.repo-list .button-bar-right {
float: right;
}
.button-bar-bottom {
margin-bottom: 60px;
}
.repo-list .section-header {
padding: 10px;
border-bottom: 1px solid #eee;
margin-bottom: 10px;
}
.repo-list .button-bar-right button {
margin-right: 10px;
}
.repo-listing {
display: block;
margin-bottom: 20px;
@ -899,6 +1001,10 @@ p.editable:hover i {
padding-left: 44px;
}
.repo-admin .entity-search input {
width: 300px;
}
.repo-admin .token-dialog-body .well {
margin-bottom: 0px;
}
@ -916,20 +1022,35 @@ p.editable:hover i {
width: 620px;
}
.repo-admin .user i {
margin-right: 6px;
.repo-admin .user i.fa-user {
margin-left: 2px;
margin-right: 7px;
}
.repo-admin .user {
.repo-admin .team i.fa-group {
margin-right: 4px;
}
.repo-admin .entity {
font-size: 1.2em;
min-width: 300px;
}
.repo-admin .entity .popover {
font-size: 14px;
}
.repo-admin .entity i.fa-exclamation-triangle {
color: #c09853;
float: right;
margin-right: 10px;
margin-top: 4px;
}
.repo-admin .token a {
cursor: pointer;
}
.repo .build-info {
padding: 10px;
margin: 0px;
@ -1100,7 +1221,7 @@ p.editable:hover i {
}
.delete-ui:focus .delete-ui-button {
width: 54px;
width: 60px;
}
.repo-admin .repo-delete {
@ -1160,12 +1281,13 @@ p.editable:hover i {
border: inherit;
}
.user-admin .panel-plan {
.user-admin #migrate .panel {
max-width: 600px;
text-align: center;
}
.user-admin .panel-plan .button-hidden {
visibility: hidden;
.user-admin .panel-plan {
text-align: center;
}
.user-admin .plan-description {
@ -1183,6 +1305,41 @@ p.editable:hover i {
margin-bottom: 12px;
}
.user-admin .convert-form h3 {
margin-bottom: 20px;
}
.user-admin #convertForm {
max-width: 500px;
}
.user-admin #convertForm .form-group {
margin-bottom: 20px;
}
.user-admin #convertForm input {
margin-bottom: 10px;
margin-left: 20px;
}
.user-admin #convertForm .existing-data {
font-size: 16px;
font-weight: bold;
}
.user-admin #convertForm .description {
margin-top: 10px;
display: block;
color: #888;
font-size: 12px;
margin-left: 20px;
}
.user-admin #convertForm .existing-data {
display: block;
padding-left: 20px;
margin-top: 10px;
}
#image-history-container {
overflow: hidden;
@ -1280,6 +1437,49 @@ p.editable:hover i {
stroke-width: 1.5px;
}
#repository-usage-chart {
display: inline-block;
vertical-align: middle;
width: 200px;
height: 200px;
}
#repository-usage-chart .count-text {
font-size: 22px;
}
#repository-usage-chart.limit-at path.arc-0 {
fill: #c09853;
}
#repository-usage-chart.limit-over path.arc-0 {
fill: #b94a48;
}
#repository-usage-chart.limit-near path.arc-0 {
fill: #468847;
}
#repository-usage-chart.limit-over path.arc-1 {
fill: #fcf8e3;
}
#repository-usage-chart.limit-at path.arc-1 {
fill: #f2dede;
}
#repository-usage-chart.limit-near path.arc-1 {
fill: #dff0d8;
}
.plan-manager-element .usage-caption {
display: inline-block;
color: #aaa;
font-size: 26px;
margin-left: 10px;
}
/* Overrides for the markdown editor. */
.wmd-panel .btn-toolbar {
@ -1309,6 +1509,271 @@ p.editable:hover i {
min-height: 50px;
}
.team-view .panel {
display: inline-block;
width: 620px;
}
.team-view .entity {
font-size: 1.2em;
min-width: 510px;
}
.team-view .entity i {
margin-right: 6px;
}
.team-view .entity-search {
margin-top: 10px;
display: inline-block;
}
.team-view .delete-ui {
display: inline-block;
width: 78px;
}
.team-view .delete-ui i {
margin-top: 8px;
float: right;
}
.org-view .team-listing {
padding: 4px;
}
.org-view .header-col {
color: #444;
margin-bottom: 10px;
}
.org-view .header-col dd {
margin-bottom: 20px;
}
.org-view .header-col .info-icon {
float: none;
margin-left: 10px;
}
.org-view .team-listing .control-col button.btn-danger {
margin-left: 10px;
}
.org-view .team-listing i {
margin-right: 10px;
}
.org-view .highlight .team-title {
animation: highlighttemp 1s 2;
animation-timing-function: ease-in-out;
animation-direction: alternate;
-moz-animation: highlighttemp 1s 2;
-moz-animation-timing-function: ease-in-out;
-moz-animation-direction: alternate;
-webkit-animation: highlighttemp 1s 2;
-webkit-animation-timing-function: ease-in-out;
-webkit-animation-direction: alternate;
}
@-moz-keyframes highlighttemp {
0% { background-color: white; }
100% { background-color: rgba(92, 184, 92, 0.36); }
}
@-webkit-keyframes highlighttemp {
0% { background-color: white; }
100% { background-color: rgba(92, 184, 92, 0.36); }
}
@keyframes highlighttemp {
0% { background-color: white; }
100% { background-color: rgba(92, 184, 92, 0.36); }
}
.org-view .team-title {
font-size: 20px;
text-transform: none;
padding: 4px;
}
.org-view .team-listing .team-description {
margin-top: 6px;
margin-left: 41px;
font-size: 16px;
}
.org-view #create-team-box {
border: none;
font-size: 14px;
padding: 6px;
}
.org-admin .team-link {
display: inline-block;
text-transform: none;
margin-right: 20px;
}
.org-admin #members table td {
font-size: 16px;
}
.org-admin #members table i {
margin-right: 4px;
}
.org-admin #members .side-controls {
float: right;
}
.org-admin #members .result-count {
display: inline-block;
margin-right: 10px;
}
.org-admin #members .filter-input {
display: inline-block;
}
.org-list h2 {
margin-bottom: 20px;
}
.org-list .button-bar-right {
text-align: right;
}
.org-list .organization-listing {
font-size: 18px;
padding: 10px;
}
.org-list .organization-listing img {
margin-left: 10px;
margin-right: 16px;
}
.create-org .steps-container {
text-align: center;
}
.create-org .steps {
background: #222;
display: inline-block;
margin-top: 16px;
margin-left: 0px;
border-radius: 4px;
padding: 0px;
list-style: none;
height: 46px;
width: 675px;
text-align: left;
}
.create-org .steps .step {
width: 225px;
float: left;
padding: 10px;
border-right: 1px solid #222;
margin: 0px;
background: rgba(255, 255, 255, 0.2);
color: #aaa;
border-left: 4px solid transparent;
}
.create-org .steps .step i {
font-size: 26px;
margin-right: 6px;
vertical-align: middle;
}
.create-org .steps .step.active {
color: white;
border-left: 4px solid steelblue;
background: transparent;
}
.create-org .steps .step:last-child {
border-right: 0px;
}
.create-org .steps .step b {
display: block;
}
.create-org .button-bar {
margin-bottom: 40px;
}
.create-org .form-group {
margin-bottom: 32px;
}
.create-org .plan-group {
padding-left: 10px;
}
.create-org .plan-group strong {
margin-bottom: 10px;
}
.create-org .step-container .description {
margin-top: 10px;
display: block;
color: #888;
font-size: 12px;
margin-left: 10px;
}
.create-org .form-group input {
margin-top: 10px;
margin-left: 10px;
}
.create-org h3 {
margin-bottom: 20px;
}
.plan-manager-element .plans-list-table thead td {
color: #aaa;
font-weight: bold;
}
.plan-manager-element .plans-list-table td {
padding: 10px;
font-size: 16px;
vertical-align: middle;
}
.plan-manager-element .plans-list-table td.controls {
text-align: right;
}
.plan-manager-element .plans-list-table .plan-price {
font-size: 16px;
margin-bottom: 0px;
}
.plans-table-element table {
margin: 20px;
border: 1px solid #eee;
}
.plans-table-element td {
vertical-align: middle !important;
}
.plans-table-element .plan-price {
font-size: 16px;
}
/* Overrides for typeahead to work with bootstrap 3. */
.twitter-typeahead .tt-query,
@ -1489,4 +1954,8 @@ p.editable:hover i {
top: -9px;
font-weight: bold;
font-size: .4em;
}
.page-description {
margin-bottom: 40px;
}

View file

@ -0,0 +1 @@
<input class="entity-search-control form-control">

View file

@ -0,0 +1,31 @@
<div class="markdown-input-container">
<p ng-class="'lead ' + (canWrite ? 'editable' : 'noteditable')" ng-click="editContent()">
<span class="markdown-view" content="content"></span>
<span class="empty" ng-show="!content && canWrite">(Click to set {{ fieldTitle }})</span>
<i class="fa fa-edit"></i>
</p>
<!-- Modal editor -->
<div class="modal fade">
<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">Edit {{ fieldTitle }}</h4>
</div>
<div class="modal-body">
<div class="wmd-panel">
<div id="wmd-button-bar-description-{{id}}"></div>
<textarea class="wmd-input" id="wmd-input-description-{{id}}" placeholder="Enter {{ fieldTitle }}">{{ content }}</textarea>
</div>
<div id="wmd-preview-description-{{id}}" class="wmd-panel wmd-preview"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" ng-click="saveContent()">Save changes</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
</div>

View file

@ -0,0 +1 @@
<span class="markdown-view-content" ng-bind-html-unsafe="getMarkedDown(content, firstLineOnly)"></span>

View file

@ -0,0 +1,34 @@
<span class="namespace-selector-dropdown">
<span ng-show="user.organizations.length == 0">
<img src="//www.gravatar.com/avatar/{{ user.gravatar }}?s=24&d=identicon" />
<span class="namespace-name">{{user.username}}</span>
</span>
<div class="btn-group" ng-show="user.organizations.length > 0">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
<img src="//www.gravatar.com/avatar/{{ namespaceObj.gravatar }}?s=16&d=identicon" />
{{namespace}} <span class="caret"></span>
</button>
<ul class="dropdown-menu" role="menu">
<li class="namespace-item" ng-repeat="org in user.organizations"
ng-class="(requireCreate && !namespaces[org.name].can_create_repo) ? 'disabled' : ''">
<a class="namespace" href="javascript:void(0)" ng-click="setNamespace(org)">
<img src="//www.gravatar.com/avatar/{{ org.gravatar }}?s=24&d=identicon" />
<span class="namespace-name">{{ org.name }}</span>
</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-placement="right"
bs-tooltip="tooltip.title"></i>
</li>
<li class="divider"></li>
<li>
<a class="namespace" href="javascript:void(0)" ng-click="setNamespace(user)">
<img src="//www.gravatar.com/avatar/{{ user.gravatar }}?s=24&d=identicon" />
<span class="namespace-name">{{ user.username }}</span>
</a>
</li>
</ul>
</div>
</span>

View file

@ -0,0 +1,18 @@
<div class="organization-header-element">
<img src="//www.gravatar.com/avatar/{{ organization.gravatar }}?s=24&amp;d=identicon">
<span class="organization-name" ng-show="teamName">
<a href="/organization/{{ organization.name }}">{{ organization.name }}</a>
</span>
<span class="organization-name" ng-show="!teamName">
{{ organization.name }}
</span>
<span ng-show="teamName">
<span class="divider">/</span>
<i class="fa fa-group"></i>
<span class="team-name">
{{ teamName }}
</span>
</span>
<span ng-transclude></span>
</div>

View file

@ -0,0 +1,62 @@
<div class="plan-manager-element">
<!-- Loading/Changing -->
<i class="fa fa-spinner fa-spin fa-3x" ng-show="planLoading"></i>
<!-- Alerts -->
<div class="alert alert-danger" ng-show="limit == 'over' && !planLoading">
You are using more private repositories than your plan allows. Please
upgrade your subscription to avoid disruptions in your <span ng-show="organization">organization's</span> service.
</div>
<div class="alert alert-warning" ng-show="limit == 'at' && !planLoading">
You are at your current plan's number of allowed private repositories. Please upgrade your subscription to avoid future disruptions in your <span ng-show="organization">organization's</span> service.
</div>
<div class="alert alert-success" ng-show="limit == 'near' && !planLoading">
You are nearing the number of allowed private repositories. It might be time to think about
upgrading your subscription to avoid future disruptions in your <span ng-show="organization">organization's</span> service.
</div>
<!-- Chart -->
<div>
<div id="repository-usage-chart" class="limit-{{limit}}"></div>
<span class="usage-caption" ng-show="chart">Repository Usage</span>
</div>
<!-- Plans Table -->
<table class="table table-hover plans-list-table" ng-show="!planLoading">
<thead>
<td>Plan</td>
<td>Private Repositories</td>
<td style="min-width: 64px">Price</td>
<td></td>
</thead>
<tr ng-repeat="plan in plans" ng-class="(subscribedPlan.stripeId === plan.stripeId) ? getActiveSubClass() : ''">
<td>{{ plan.title }}</td>
<td>{{ plan.privateRepos }}</td>
<td><div class="plan-price">${{ plan.price / 100 }}</div></td>
<td class="controls">
<div ng-switch='plan.stripeId'>
<div ng-switch-when='bus-free'>
<button class="btn button-hidden">Hidden!</button>
</div>
<div ng-switch-default>
<button class="btn" ng-show="subscribedPlan.stripeId !== plan.stripeId"
ng-class="subscribedPlan.price == 0 ? 'btn-primary' : 'btn-default'"
ng-click="changeSubscription(plan.stripeId)">
<i class="fa fa-spinner fa-spin" ng-show="planChanging"></i>
<span ng-show="!planChanging && subscribedPlan.price != 0">Change</span>
<span ng-show="!planChanging && subscribedPlan.price == 0">Subscribe</span>
</button>
<button class="btn btn-danger" ng-show="subscription.plan === plan.stripeId && plan.price > 0"
ng-click="cancelSubscription()">
<i class="fa fa-spinner fa-spin" ng-show="planChanging"></i>
<span ng-show="!planChanging">Cancel</span>
</button>
</div>
</div>
</td>
</tr>
</table>
</div>

View file

@ -0,0 +1,23 @@
<div class="plans-table-element">
<table class="table table-hover plans-table-table" ng-show="plans">
<thead>
<th>Plan</th>
<th>Private Repositories</th>
<th style="min-width: 85px">Price</th>
<th></th>
</thead>
<tr ng-repeat="plan in plans" ng-class="currentPlan == plan ? 'active' : ''">
<td>{{ plan.title }}</td>
<td>{{ plan.privateRepos }}</td>
<td><div class="plan-price">${{ plan.price / 100 }}</div></td>
<td class="controls">
<a class="btn" href="javascript:void(0)"
ng-class="currentPlan == plan ? 'btn-primary' : 'btn-default'"
ng-click="setPlan(plan)">
{{ currentPlan == plan ? 'Selected' : 'Choose' }}
</a>
</td>
</tr>
</table>
</div>

View file

@ -0,0 +1,5 @@
<div class="btn-group btn-group-sm">
<button ng-repeat="role in roles"
type="button" class="btn" ng-click="setRole(role.id)"
ng-class="(currentRole == role.id) ? ('active btn-' + role.kind) : 'btn-default'">{{ role.title }}</button>
</div>

View file

@ -0,0 +1,25 @@
<div class="signin-form-element">
<form class="form-signin" ng-submit="signin();">
<input type="text" class="form-control input-lg" name="username"
placeholder="Username" ng-model="user.username" autofocus>
<input type="password" class="form-control input-lg" name="password"
placeholder="Password" ng-model="user.password">
<button class="btn btn-lg btn-primary btn-block" type="submit">Sign In</button>
<span class="social-alternate">
<i class="fa fa-circle"></i>
<span class="inner-text">OR</span>
</span>
<a id="github-signin-link"
href="https://github.com/login/oauth/authorize?client_id={{ githubClientId }}&scope=user:email{{ mixpanelDistinctIdClause }}"
class="btn btn-primary btn-lg btn-block">
<i class="fa fa-github fa-lg"></i> Sign In with GitHub
</a>
</form>
<div class="alert alert-danger" ng-show="invalidCredentials">Invalid username or password.</div>
<div class="alert alert-danger" ng-show="needsEmailVerification">
You must verify your email address before you can sign in.
</div>
</div>

BIN
static/img/org-admin.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

BIN
static/img/org-teams.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 80 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 99 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 197 KiB

After

Width:  |  Height:  |  Size: 194 KiB

Before After
Before After

View file

@ -1,15 +1,65 @@
function getFirstTextLine(commentString) {
if (!commentString) { return ''; }
var lines = commentString.split('\n');
var MARKDOWN_CHARS = {
'#': true,
'-': true,
'>': true,
'`': true
};
for (var i = 0; i < lines.length; ++i) {
// Skip code lines.
if (lines[i].indexOf(' ') == 0) {
continue;
}
// Skip empty lines.
if ($.trim(lines[i]).length == 0) {
continue;
}
// Skip control lines.
if (MARKDOWN_CHARS[$.trim(lines[i])[0]]) {
continue;
}
return getMarkedDown(lines[i]);
}
return '';
}
function getRestUrl(args) {
var url = '';
for (var i = 0; i < arguments.length; ++i) {
if (i > 0) {
url += '/';
}
url += encodeURI(arguments[i])
}
return url;
}
function getMarkedDown(string) {
return Markdown.getSanitizingConverter().makeHtml(string || '');
}
// Start the application code itself.
quayApp = angular.module('quay', ['restangular', 'angularMoment', 'angulartics', 'angulartics.mixpanel', '$strap.directives'], function($provide) {
$provide.factory('UserService', ['Restangular', function(Restangular) {
quayApp = angular.module('quay', ['restangular', 'angularMoment', 'angulartics', 'angulartics.mixpanel', '$strap.directives', 'ngCookies'], function($provide) {
$provide.factory('UserService', ['Restangular', 'PlanService', function(Restangular, PlanService) {
var userResponse = {
verified: false,
anonymous: true,
username: null,
email: null,
askForPassword: false,
organizations: []
}
var userService = {}
var currentSubscription = null;
userService.load = function() {
var userFetch = Restangular.one('user/');
@ -30,6 +80,18 @@ quayApp = angular.module('quay', ['restangular', 'angularMoment', 'angulartics',
});
};
userService.resetCurrentSubscription = function() {
currentSubscription = null;
};
userService.getCurrentSubscription = function(callback, failure) {
if (currentSubscription) { callback(currentSubscription); }
PlanService.getSubscription(null, function(sub) {
currentSubscription = sub;
callback(sub);
}, failure);
};
userService.currentUser = function() {
return userResponse;
}
@ -55,93 +117,134 @@ quayApp = angular.module('quay', ['restangular', 'angularMoment', 'angulartics',
}]);
$provide.factory('PlanService', ['Restangular', 'KeyService', function(Restangular, KeyService) {
var plans = [
{
title: 'Open Source',
price: 0,
privateRepos: 0,
stripeId: 'free',
audience: 'Share with the world',
},
{
title: 'Micro',
price: 700,
privateRepos: 5,
stripeId: 'micro',
audience: 'For smaller teams',
},
{
title: 'Basic',
price: 1200,
privateRepos: 10,
stripeId: 'small',
audience: 'For your basic team',
},
{
title: 'Medium',
price: 2200,
privateRepos: 20,
stripeId: 'medium',
audience: 'For medium-sized teams',
},
];
var plans = null;
var planDict = {};
var i;
for(i = 0; i < plans.length; i++) {
planDict[plans[i].stripeId] = plans[i];
}
var planService = {}
planService.planList = function() {
return plans;
};
planService.getPlan = function(planId) {
return planDict[planId];
};
planService.getMinimumPlan = function(privateCount) {
for (var i = 0; i < plans.length; i++) {
var plan = plans[i];
if (plan.privateRepos >= privateCount) {
return plan;
}
planService.verifyLoaded = function(callback) {
if (plans) {
callback(plans);
return;
}
return null;
var getPlans = Restangular.one('plans');
getPlans.get().then(function(data) {
var i = 0;
for(i = 0; i < data.user.length; i++) {
planDict[data.user[i].stripeId] = data.user[i];
}
for(i = 0; i < data.business.length; i++) {
planDict[data.business[i].stripeId] = data.business[i];
}
plans = data;
callback(plans);
}, function() { callback([]); });
};
planService.showSubscribeDialog = function($scope, planId, started, success, failed) {
var submitToken = function(token) {
$scope.$apply(function() {
started();
});
planService.getMatchingBusinessPlan = function(callback) {
planService.getPlans(function() {
planService.getSubscription(null, function(sub) {
var plan = planDict[sub.plan];
if (!plan) {
planService.getMinimumPlan(0, true, callback);
return;
}
var count = Math.max(sub.usedPrivateRepos, plan.privateRepos);
planService.getMinimumPlan(count, true, callback);
}, function() {
planService.getMinimumPlan(0, true, callback);
});
});
};
planService.getPlans = function(callback) {
planService.verifyLoaded(callback);
};
planService.getPlan = function(planId, callback) {
planService.verifyLoaded(function() {
if (planDict[planId]) {
callback(planDict[planId]);
}
});
};
planService.getMinimumPlan = function(privateCount, isBusiness, callback) {
planService.verifyLoaded(function() {
var planSource = plans.user;
if (isBusiness) {
planSource = plans.business;
}
for (var i = 0; i < planSource.length; i++) {
var plan = planSource[i];
if (plan.privateRepos >= privateCount) {
callback(plan);
return;
}
}
callback(null);
});
};
planService.getSubscription = function(organization, success, failure) {
var url = planService.getSubscriptionUrl(organization);
var getSubscription = Restangular.one(url);
getSubscription.get().then(success, failure);
};
planService.getSubscriptionUrl = function(orgname) {
return orgname ? getRestUrl('organization', orgname, 'plan') : 'user/plan';
};
planService.setSubscription = function(orgname, planId, success, failure, opt_token) {
var subscriptionDetails = {
plan: planId
};
if (opt_token) {
subscriptionDetails['token'] = opt_token.id;
}
var url = planService.getSubscriptionUrl(orgname);
var createSubscriptionRequest = Restangular.one(url);
createSubscriptionRequest.customPUT(subscriptionDetails).then(success, failure);
};
planService.changePlan = function($scope, orgname, planId, hasExistingSubscription, started, success, failure) {
if (!hasExistingSubscription) {
planService.showSubscribeDialog($scope, orgname, planId, started, success, failure);
return;
}
started();
planService.setSubscription(orgname, planId, success, failure);
};
planService.showSubscribeDialog = function($scope, orgname, planId, started, success, failure) {
var submitToken = function(token) {
mixpanel.track('plan_subscribe');
var subscriptionDetails = {
token: token.id,
plan: planId,
};
var createSubscriptionRequest = Restangular.one('user/plan');
$scope.$apply(function() {
createSubscriptionRequest.customPUT(subscriptionDetails).then(success, failed);
started();
planService.setSubscription(orgname, planId, success, failure);
});
};
var planDetails = planService.getPlan(planId)
StripeCheckout.open({
key: KeyService.stripePublishableKey,
address: false, // TODO change to true
amount: planDetails.price,
currency: 'usd',
name: 'Quay ' + planDetails.title + ' Subscription',
description: 'Up to ' + planDetails.privateRepos + ' private repositories',
panelLabel: 'Subscribe',
token: submitToken
planService.getPlan(planId, function(planDetails) {
StripeCheckout.open({
key: KeyService.stripePublishableKey,
address: false,
amount: planDetails.price,
currency: 'usd',
name: 'Quay ' + planDetails.title + ' Subscription',
description: 'Up to ' + planDetails.privateRepos + ' private repositories',
panelLabel: 'Subscribe',
token: submitToken,
image: 'static/img/quay-icon-stripe.png'
});
});
};
@ -194,12 +297,19 @@ quayApp = angular.module('quay', ['restangular', 'angularMoment', 'angulartics',
when('/repository/:namespace/:name/image/:image', {templateUrl: '/static/partials/image-view.html', controller: ImageViewCtrl}).
when('/repository/:namespace/:name/admin', {templateUrl: '/static/partials/repo-admin.html', controller:RepoAdminCtrl}).
when('/repository/', {title: 'Repositories', templateUrl: '/static/partials/repo-list.html', controller: RepoListCtrl}).
when('/user/', {title: 'User Admin', templateUrl: '/static/partials/user-admin.html', controller: UserAdminCtrl}).
when('/guide/', {title: 'User Guide', templateUrl: '/static/partials/guide.html', controller: GuideCtrl}).
when('/user/', {title: 'Account Settings', templateUrl: '/static/partials/user-admin.html', controller: UserAdminCtrl}).
when('/guide/', {title: 'Guide', templateUrl: '/static/partials/guide.html', controller: GuideCtrl}).
when('/plans/', {title: 'Plans and Pricing', templateUrl: '/static/partials/plans.html', controller: PlansCtrl}).
when('/signin/', {title: 'Signin', templateUrl: '/static/partials/signin.html', controller: SigninCtrl}).
when('/signin/', {title: 'Sign In', templateUrl: '/static/partials/signin.html', controller: SigninCtrl}).
when('/new/', {title: 'Create new repository', templateUrl: '/static/partials/new-repo.html', controller: NewRepoCtrl}).
when('/organizations/', {title: 'Organizations', templateUrl: '/static/partials/organizations.html', controller: OrgsCtrl}).
when('/organizations/new/', {title: 'New Organization', templateUrl: '/static/partials/new-organization.html', controller: NewOrgCtrl}).
when('/organization/:orgname', {templateUrl: '/static/partials/org-view.html', controller: OrgViewCtrl}).
when('/organization/:orgname/admin', {templateUrl: '/static/partials/org-admin.html', controller: OrgAdminCtrl}).
when('/organization/:orgname/teams/:teamname', {templateUrl: '/static/partials/team-view.html', controller: TeamViewCtrl}).
when('/v1/', {title: 'Activation information', templateUrl: '/static/partials/v1-page.html', controller: V1Ctrl}).
when('/', {title: 'Hosted Private Docker Registry', templateUrl: '/static/partials/landing.html', controller: LandingCtrl}).
@ -209,6 +319,31 @@ quayApp = angular.module('quay', ['restangular', 'angularMoment', 'angulartics',
RestangularProvider.setBaseUrl('/api/');
});
quayApp.directive('markdownView', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/markdown-view.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
'content': '=content',
'firstLineOnly': '=firstLineOnly'
},
controller: function($scope, $element) {
$scope.getMarkedDown = function(content, firstLineOnly) {
if (firstLineOnly) {
content = getFirstTextLine(content);
}
return getMarkedDown(content);
};
}
};
return directiveDefinitionObject;
});
quayApp.directive('repoCircle', function () {
var directiveDefinitionObject = {
priority: 0,
@ -226,6 +361,415 @@ quayApp.directive('repoCircle', function () {
});
quayApp.directive('signinForm', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/signin-form.html',
replace: false,
transclude: true,
restrict: 'C',
scope: {
'redirectUrl': '=redirectUrl'
},
controller: function($scope, $location, $timeout, Restangular, KeyService, UserService) {
$scope.githubClientId = KeyService.githubClientId;
var appendMixpanelId = function() {
if (mixpanel.get_distinct_id !== undefined) {
$scope.mixpanelDistinctIdClause = "&state=" + mixpanel.get_distinct_id();
} else {
// Mixpanel not yet loaded, try again later
$timeout(appendMixpanelId, 200);
}
};
appendMixpanelId();
$scope.signin = function() {
var signinPost = Restangular.one('signin');
signinPost.customPOST($scope.user).then(function() {
$scope.needsEmailVerification = false;
$scope.invalidCredentials = false;
// Redirect to the specified page or the landing page
UserService.load();
$location.path($scope.redirectUrl ? $scope.redirectUrl : '/');
}, function(result) {
$scope.needsEmailVerification = result.data.needsEmailVerification;
$scope.invalidCredentials = result.data.invalidCredentials;
});
};
}
};
return directiveDefinitionObject;
});
quayApp.directive('plansTable', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/plans-table.html',
replace: false,
transclude: true,
restrict: 'C',
scope: {
'plans': '=plans',
'currentPlan': '=currentPlan'
},
controller: function($scope, $element) {
$scope.setPlan = function(plan) {
$scope.currentPlan = plan;
};
}
};
return directiveDefinitionObject;
});
quayApp.directive('organizationHeader', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/organization-header.html',
replace: false,
transclude: true,
restrict: 'C',
scope: {
'organization': '=organization',
'teamName': '=teamName'
},
controller: function($scope, $element) {
}
};
return directiveDefinitionObject;
});
quayApp.directive('markdownInput', function () {
var counter = 0;
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/markdown-input.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
'content': '=content',
'canWrite': '=canWrite',
'contentChanged': '=contentChanged',
'fieldTitle': '=fieldTitle'
},
controller: function($scope, $element) {
var elm = $element[0];
$scope.id = (counter++);
$scope.editContent = function() {
if (!$scope.canWrite) { return; }
if (!$scope.markdownDescriptionEditor) {
var converter = Markdown.getSanitizingConverter();
var editor = new Markdown.Editor(converter, '-description-' + $scope.id);
editor.run();
$scope.markdownDescriptionEditor = editor;
}
$('#wmd-input-description-' + $scope.id)[0].value = $scope.content;
$(elm).find('.modal').modal({});
};
$scope.saveContent = function() {
$scope.content = $('#wmd-input-description-' + $scope.id)[0].value;
$(elm).find('.modal').modal('hide');
if ($scope.contentChanged) {
$scope.contentChanged($scope.content);
}
};
}
};
return directiveDefinitionObject;
});
quayApp.directive('entitySearch', function () {
var number = 0;
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/entity-search.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
'organization': '=organization',
'inputTitle': '=inputTitle',
'entitySelected': '=entitySelected'
},
controller: function($scope, $element) {
if (!$scope.entitySelected) { return; }
number++;
var input = $element[0].firstChild;
$scope.organization = $scope.organization || '';
$(input).typeahead({
name: 'entities' + number,
remote: {
url: '/api/entities/%QUERY',
replace: function (url, uriEncodedQuery) {
url = url.replace('%QUERY', uriEncodedQuery);
if ($scope.organization) {
url += '?organization=' + encodeURIComponent($scope.organization);
}
return url;
},
filter: function(data) {
var datums = [];
for (var i = 0; i < data.results.length; ++i) {
var entity = data.results[i];
datums.push({
'value': entity.name,
'tokens': [entity.name],
'entity': entity
});
}
return datums;
}
},
template: function (datum) {
template = '<div class="entity-mini-listing">';
if (datum.entity.kind == 'user') {
template += '<i class="fa fa-user fa-lg"></i>';
} else if (datum.entity.kind == 'team') {
template += '<i class="fa fa-group fa-lg"></i>';
}
template += '<span class="name">' + datum.value + '</span>';
if (datum.entity.is_org_member !== undefined && !datum.entity.is_org_member) {
template += '<div class="alert-warning warning">This user is outside your organization</div>';
}
template += '</div>';
return template;
},
});
$(input).on('typeahead:selected', function(e, datum) {
$(input).typeahead('setQuery', '');
$scope.entitySelected(datum.entity);
});
$scope.$watch('inputTitle', function(title) {
input.setAttribute('placeholder', title);
});
}
};
return directiveDefinitionObject;
});
quayApp.directive('roleGroup', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/role-group.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
'roles': '=roles',
'currentRole': '=currentRole',
'roleChanged': '&roleChanged'
},
controller: function($scope, $element) {
$scope.setRole = function(role) {
if ($scope.currentRole == role) { return; }
if ($scope.roleChanged) {
$scope.roleChanged({'role': role});
} else {
$scope.currentRole = role;
}
};
}
};
return directiveDefinitionObject;
});
quayApp.directive('planManager', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/plan-manager.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
'user': '=user',
'organization': '=organization',
'readyForPlan': '&readyForPlan'
},
controller: function($scope, $element, PlanService, Restangular) {
var hasSubscription = false;
$scope.getActiveSubClass = function() {
return 'active';
};
$scope.changeSubscription = function(planId) {
if ($scope.planChanging) { return; }
PlanService.changePlan($scope, $scope.organization, planId, hasSubscription, function() {
// Started.
$scope.planChanging = true;
}, function(sub) {
// Success.
subscribedToPlan(sub);
}, function() {
// Failure.
$scope.planChanging = false;
});
};
$scope.cancelSubscription = function() {
$scope.changeSubscription(getFreePlan());
};
var subscribedToPlan = function(sub) {
$scope.subscription = sub;
if (sub.plan != getFreePlan()) {
hasSubscription = true;
}
PlanService.getPlan(sub.plan, function(subscribedPlan) {
$scope.subscribedPlan = subscribedPlan;
$scope.planUsagePercent = sub.usedPrivateRepos * 100 / $scope.subscribedPlan.privateRepos;
if (sub.usedPrivateRepos > $scope.subscribedPlan.privateRepos) {
$scope.limit = 'over';
} else if (sub.usedPrivateRepos == $scope.subscribedPlan.privateRepos) {
$scope.limit = 'at';
} else if (sub.usedPrivateRepos >= $scope.subscribedPlan.privateRepos * 0.7) {
$scope.limit = 'near';
} else {
$scope.limit = 'none';
}
if (!$scope.chart) {
$scope.chart = new RepositoryUsageChart();
$scope.chart.draw('repository-usage-chart');
}
$scope.chart.update(sub.usedPrivateRepos || 0, $scope.subscribedPlan.privateRepos || 0);
$scope.planChanging = false;
$scope.planLoading = false;
});
};
var getFreePlan = function() {
for (var i = 0; i < $scope.plans.length; ++i) {
if ($scope.plans[i].price == 0) {
return $scope.plans[i].stripeId;
}
}
return 'free';
};
var update = function() {
$scope.planLoading = true;
if (!$scope.plans) { return; }
PlanService.getSubscription($scope.organization, subscribedToPlan, function() {
// User/Organization has no subscription.
subscribedToPlan({ 'plan': getFreePlan() });
});
};
var loadPlans = function() {
if ($scope.plans || $scope.loadingPlans) { return; }
if (!$scope.user && !$scope.organization) { return; }
$scope.loadingPlans = true;
PlanService.getPlans(function(plans) {
$scope.plans = plans[$scope.organization ? 'business' : 'user'];
update();
if ($scope.readyForPlan) {
var planRequested = $scope.readyForPlan();
if (planRequested && planRequested != getFreePlan()) {
$scope.changeSubscription(planRequested);
}
}
});
};
// Start the initial download.
$scope.planLoading = true;
loadPlans();
$scope.$watch('organization', loadPlans);
$scope.$watch('user', loadPlans);
}
};
return directiveDefinitionObject;
});
quayApp.directive('namespaceSelector', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/namespace-selector.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
'user': '=user',
'namespace': '=namespace',
'requireCreate': '=requireCreate'
},
controller: function($scope, $element, $routeParams, $cookieStore) {
$scope.namespaces = {};
$scope.initialize = function(user) {
var namespaces = {};
namespaces[user.username] = user;
if (user.organizations) {
for (var i = 0; i < user.organizations.length; ++i) {
namespaces[user.organizations[i].name] = user.organizations[i];
}
}
var initialNamespace = $routeParams['namespace'] || $cookieStore.get('quay.currentnamespace') || $scope.user.username;
$scope.namespaces = namespaces;
$scope.setNamespace($scope.namespaces[initialNamespace]);
};
$scope.setNamespace = function(namespaceObj) {
if (!namespaceObj) {
namespaceObj = $scope.namespaces[$scope.user.username];
}
if ($scope.requireCreate && !namespaceObj.can_create_repo) {
namespaceObj = $scope.namespaces[$scope.user.username];
}
var newNamespace = namespaceObj.name || namespaceObj.username;
$scope.namespaceObj = namespaceObj;
$scope.namespace = newNamespace;
$cookieStore.put('quay.currentnamespace', newNamespace);
};
$scope.$watch('user', function(user) {
$scope.user = user;
$scope.initialize(user);
});
}
};
return directiveDefinitionObject;
});
quayApp.directive('buildStatus', function () {
var directiveDefinitionObject = {
priority: 0,
@ -292,6 +836,15 @@ quayApp.directive('buildStatus', function () {
return directiveDefinitionObject;
});
// Note: ngBlur is not yet in Angular stable, so we add it manaully here.
quayApp.directive('ngBlur', function() {
return function( scope, elem, attrs ) {
elem.bind('blur', function() {
scope.$apply(attrs.ngBlur);
});
};
});
quayApp.run(['$location', '$rootScope', function($location, $rootScope) {
$rootScope.$on('$routeChangeSuccess', function (event, current, previous) {
if (current.$$route.title) {

File diff suppressed because it is too large Load diff

View file

@ -137,6 +137,10 @@ ImageHistoryTree.prototype.draw = function(container) {
.direction('e')
.html(function(d) {
var html = '';
if (d.virtual) {
return d.name;
}
if (d.collapsed) {
for (var i = 1; i < d.encountered.length; ++i) {
html += '<span>' + d.encountered[i].image.id.substr(0, 12) + '</span>';
@ -272,6 +276,7 @@ ImageHistoryTree.prototype.buildRoot_ = function() {
// For each node, attach it to its immediate parent. If there is no immediate parent,
// then the node is the root.
var roots = [];
for (var i = 0; i < this.images_.length; ++i) {
var image = this.images_[i];
var imageNode = imageByDBID[image.dbid];
@ -283,10 +288,22 @@ ImageHistoryTree.prototype.buildRoot_ = function() {
imageNode.parent = parent;
parent.children.push(imageNode);
} else {
formatted = imageNode;
roots.push(imageNode);
}
}
// If there are multiple root nodes, then there is at least one branch without shared
// ancestry and we use the virtual node. Otherwise, we use the root node found.
var root = {
'name': '',
'children': roots,
'virtual': true
};
if (roots.length == 1) {
root = roots[0];
}
// Determine the maximum number of nodes at a particular level. This is used to size
// the width of the tree properly.
var maxChildCount = 0;
@ -300,14 +317,14 @@ ImageHistoryTree.prototype.buildRoot_ = function() {
// section. We only do this if the max width is > 1 (since for a single width tree, no long
// chain will hide a branch).
if (maxChildCount > 1) {
this.collapseNodes_(formatted);
this.collapseNodes_(root);
}
// Determine the maximum height of the tree.
var maxHeight = this.determineMaximumHeight_(formatted);
var maxHeight = this.determineMaximumHeight_(root);
// Finally, set the root node and return.
this.root_ = formatted;
this.root_ = root;
return {
'maxWidth': maxChildCount + 1,
@ -566,7 +583,6 @@ ImageHistoryTree.prototype.update_ = function(source) {
// Translate the foreign object so the tags are under the ID.
fo.attr("transform", function(d, i) {
bbox = this.getBBox()
return "translate(" + [-130, 0 ] + ")";
});
@ -594,6 +610,9 @@ ImageHistoryTree.prototype.update_ = function(source) {
if (d.collapsed) {
return 'collapsed';
}
if (d.virtual) {
return 'virtual';
}
if (!currentImage) {
return '';
}
@ -1130,4 +1149,116 @@ ImageFileChangeTree.prototype.toggle_ = function(d) {
d.children = d._children;
d._children = null;
}
};
////////////////////////////////////////////////////////////////////////////////
/**
* Based off of http://bl.ocks.org/mbostock/1346410
*/
function RepositoryUsageChart() {
this.total_ = null;
this.count_ = null;
this.drawn_ = false;
}
/**
* Updates the chart with the given count and total of number of repositories.
*/
RepositoryUsageChart.prototype.update = function(count, total) {
if (!this.g_) { return; }
this.total_ = total;
this.count_ = count;
this.drawInternal_();
};
/**
* Conducts the actual draw or update (if applicable).
*/
RepositoryUsageChart.prototype.drawInternal_ = function() {
// If the total is null, then we have not yet set the proper counts.
if (this.total_ === null) { return; }
var duration = 750;
var arc = this.arc_;
var pie = this.pie_;
var arcTween = this.arcTween_;
var color = d3.scale.category20();
var count = this.count_;
var total = this.total_;
var data = [Math.max(count, 1), Math.max(0, total - count)];
var arcTween = function(a) {
var i = d3.interpolate(this._current, a);
this._current = i(0);
return function(t) {
return arc(i(t));
};
};
if (!this.drawn_) {
var text = this.g_.append("svg:text")
.attr("dy", 10)
.attr("dx", 0)
.attr('dominant-baseline', 'auto')
.attr('text-anchor', 'middle')
.attr('class', 'count-text')
.text(this.count_ + ' / ' + this.total_);
var path = this.g_.datum(data).selectAll("path")
.data(pie)
.enter().append("path")
.attr("fill", function(d, i) { return color(i); })
.attr("class", function(d, i) { return 'arc-' + i; })
.attr("d", arc)
.each(function(d) { this._current = d; }); // store the initial angles
this.path_ = path;
this.text_ = text;
} else {
pie.value(function(d, i) { return data[i]; }); // change the value function
this.path_ = this.path_.data(pie); // compute the new angles
this.path_.transition().duration(duration).attrTween("d", arcTween); // redraw the arcs
// Update the text.
this.text_.text(this.count_ + ' / ' + this.total_);
}
this.drawn_ = true;
};
/**
* Draws the chart in the given container.
*/
RepositoryUsageChart.prototype.draw = function(container) {
var cw = 200;
var ch = 200;
var radius = Math.min(cw, ch) / 2;
var pie = d3.layout.pie().sort(null);
var arc = d3.svg.arc()
.innerRadius(radius - 50)
.outerRadius(radius - 25);
var svg = d3.select("#" + container).append("svg:svg")
.attr("width", cw)
.attr("height", ch);
var g = svg.append("g")
.attr("transform", "translate(" + cw / 2 + "," + ch / 2 + ")");
this.svg_ = svg;
this.g_ = g;
this.pie_ = pie;
this.arc_ = arc;
this.width_ = cw;
this.drawInternal_();
};

7
static/lib/angular-cookies.min.js vendored Normal file
View file

@ -0,0 +1,7 @@
/*
AngularJS v1.2.0-ed8640b
(c) 2010-2012 Google, Inc. http://angularjs.org
License: MIT
*/
(function(p,f,n){'use strict';f.module("ngCookies",["ng"]).factory("$cookies",["$rootScope","$browser",function(d,b){var c={},g={},h,k=!1,l=f.copy,m=f.isUndefined;b.addPollFn(function(){var a=b.cookies();h!=a&&(h=a,l(a,g),l(a,c),k&&d.$apply())})();k=!0;d.$watch(function(){var a,e,d;for(a in g)m(c[a])&&b.cookies(a,n);for(a in c)(e=c[a],f.isString(e))?e!==g[a]&&(b.cookies(a,e),d=!0):f.isDefined(g[a])?c[a]=g[a]:delete c[a];if(d)for(a in e=b.cookies(),c)c[a]!==e[a]&&(m(e[a])?delete c[a]:c[a]=e[a])});
return c}]).factory("$cookieStore",["$cookies",function(d){return{get:function(b){return(b=d[b])?f.fromJson(b):b},put:function(b,c){d[b]=f.toJson(c)},remove:function(b){delete d[b]}}}])})(window,window.angular);

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,3 @@
<form name="newteamform" ng-submit="createTeam(); hide()" novalidate>
<input id="create-team-box" type="text form-control" placeholder="Team Name" ng-blur="hide()" ng-pattern="/^[a-zA-Z][a-zA-Z0-9]+$/" ng-model="newTeamName" ng-trim="false" ng-minlength="2" required>
</form>

View file

@ -1,7 +1,7 @@
<div class="container ready-indicator" data-status="{{ status }}">
<div class="alert alert-warning">Warning: Quay requires docker version 0.6.2 or higher to work</div>
<h2>User guide</h2>
<h2>User Guide</h2>
<div class="user-guide container">
<h3>Pulling a repository from Quay</h3>

View file

@ -15,8 +15,9 @@
<div class="collapse navbar-collapse navbar-ex1-collapse">
<ul class="nav navbar-nav">
<li><a ng-href="/repository/" target="{{ appLinkTarget() }}">Repositories</a></li>
<li><a ng-href="/guide/" target="{{ appLinkTarget() }}">User Guide</a></li>
<li><a ng-href="/plans/" target="{{ appLinkTarget() }}">Plans &amp; Pricing</a></li>
<li><a ng-href="/guide/" target="{{ appLinkTarget() }}">Guide</a></li>
<li><a ng-href="/plans/" target="{{ appLinkTarget() }}">Pricing</a></li>
<li><a ng-href="/organizations/" target="{{ appLinkTarget() }}">Organizations</a></li>
</ul>
@ -28,7 +29,7 @@
</form>
<span class="navbar-left user-tools" ng-show="!user.anonymous">
<a href="/new/"><i class="fa fa-upload user-tool" title="Create new repository"></i></a>
<a href="/new/"><i class="fa fa-upload user-tool" bs-tooltip="tooltip.title" data-placement="bottom" title="Create new repository"></i></a>
</span>
<li class="dropdown" ng-switch-when="false">
@ -45,6 +46,7 @@
<span class="badge user-notification" ng-show="user.askForPassword">1</span>
</a>
</li>
<li><a ng-href="/organizations/" target="{{ appLinkTarget() }}">Organizations</a></li>
<li><a href="javascript:void(0)" ng-click="signout()">Sign out</a></li>
</ul>
</li>

View file

@ -20,7 +20,9 @@
</div>
<!-- Comment -->
<blockquote ng-show="image.comment" ng-bind-html-unsafe="getMarkedDown(image.comment)"></blockquote>
<blockquote ng-show="image.comment">
<span class="markdown-view" content="image.comment"></span>
</blockquote>
<!-- Information -->
<dl class="dl-normal">

View file

@ -12,22 +12,22 @@
<div ng-show="loadingmyrepos">
<i class="fa fa-spinner fa-spin fa-3x"></i>
</div>
<span class="namespace-selector" user="user" namespace="namespace" ng-show="!loadingmyrepos && user.organizations"></span>
<div ng-show="!loadingmyrepos && myrepos.length > 0">
<h2>Your Top Repositories</h2>
<h2>Top Repositories</h2>
<div class="repo-listing" ng-repeat="repository in myrepos">
<span class="repo-circle no-background" repo="repository"></span>
<a ng-href="/repository/{{ repository.namespace }}/{{ repository.name }}">{{repository.namespace}}/{{repository.name}}</a>
<div class="description" ng-bind-html-unsafe="getCommentFirstLine(repository.description)"></div>
<div class="markdown-view description" content="repository.description" first-line-only="true"></div>
</div>
</div>
<div ng-show="!loadingmyrepos && myrepos.length == 0">
<div class="sub-message">
You don't have any <b>private</b> repositories yet!
<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">
<div class="option"><a href="/guide">Learn how to create a repository</a></div>
<div class="or"><span>or</span></div>
<div class="option"><a href="/repository">Browse the public repositories</a></div>
<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>

View file

@ -0,0 +1,102 @@
<div class="loading" ng-show="loading || creating">
<i class="fa fa-spinner fa-spin fa-3x"></i>
</div>
<div class="container create-org" ng-show="!loading && !creating">
<div class="row header-row">
<div class="col-md-8 col-md-offset-1">
<h2>Create Organization</h2>
<div class="steps-container" ng-show="false">
<ul class="steps">
<li class="step" ng-class="!user || user.anonymous ? 'active' : ''">
<i class="fa fa-sign-in"></i>
<span class="title">Login with an account</span>
</li>
<li class="step" ng-class="!user.anonymous && !created ? 'active' : ''">
<i class="fa fa-gear"></i>
<span class="title">Setup your organization</span>
</li>
<li class="step" ng-class="!user.anonymous && created ? 'active' : ''">
<i class="fa fa-group"></i>
<span class="title">Create teams</span>
</li>
</ul>
</div>
</div>
</div>
<!-- Step 1 -->
<div class="row" ng-show="!user || user.anonymous">
<div class="col-md-10 col-md-offset-1 page-description">
In order to create a new organization, <b>you must first be signed in</b> as the
user that <b>will become an admin</b> for the organization. Please sign-in if
you already have an account, or <a href="/">sign up</a> on the landing
page to create a new account.
</div>
<div class="col-sm-6 col-sm-offset-3">
<div class="step-container" >
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">Sign In</h4>
</div>
<div class="panel-body">
<div class="signin-form" redirect-url="'/organizations/new'"></div>
</div>
</div>
</div>
</div>
</div>
<!-- Step 2 -->
<div class="row" ng-show="user && !user.anonymous && !created">
<div class="col-md-1"></div>
<div class="col-md-8">
<div class="step-container">
<h3>Setup the new organization</h3>
<form method="post" name="newOrgForm" id="newOrgForm" ng-submit="createNewOrg()">
<div class="form-group">
<label for="orgName">Organization Name</label>
<input id="orgName" name="orgName" type="text" class="form-control" placeholder="Organization Name"
ng-model="org.name" required autofocus data-trigger="manual" data-content="{{ createError }}"
data-placement="right">
<span class="description">This will also be the namespace for your repositories</span>
</div>
<div class="form-group">
<label for="orgName">Organization Email</label>
<input id="orgEmail" name="orgEmail" type="email" class="form-control" placeholder="Organization Email"
ng-model="org.email" required>
<span class="description">This address must be different from your account's email</span>
</div>
<!-- Plans Table -->
<div class="form-group plan-group">
<strong>Choose your organization's plan</strong>
<div class="plans-table" plans="plans" current-plan="currentPlan"></div>
</div>
<div class="button-bar">
<button class="btn btn-large btn-success" type="submit" ng-disabled="newOrgForm.$invalid || !currentPlan">
Create Organization
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Step 3 -->
<div class="row" ng-show="user && !user.anonymous && created">
<div class="col-md-1"></div>
<div class="col-md-8">
<div class="step-container">
<h3>Organization Created</h3>
<h4><a href="/organization/{{ org.name }}">Manage Teams Now</a></h4>
</div>
</div>
</div>
</div>

View file

@ -28,17 +28,19 @@
<div class="col-md-8">
<div class="section">
<div class="new-header">
<span class="repo-circle no-background" repo="repo"></span>
<span style="color: #444;"> {{user.username}}</span> <span style="color: #ccc">/</span> <span class="name-container"><input id="repoName" name="repoName" type="text" class="form-control" placeholder="Repository Name" ng-model="repo.name" required autofocus data-trigger="manual" data-content="{{ createError }}" data-placement="right"></span>
<span style="color: #444;">
<span class="namespace-selector" user="user" namespace="repo.namespace" require-create="true"></span>
<span style="color: #ccc">/</span>
<span class="name-container">
<input id="repoName" name="repoName" type="text" class="form-control" placeholder="Repository Name" ng-model="repo.name" required autofocus data-trigger="manual" data-content="{{ createError }}" data-placement="right">
</span>
</div>
</div>
<div class="section">
<strong>Description:</strong><br>
<p class="description lead editable" ng-click="editDescription()">
<span class="content" ng-bind-html-unsafe="getMarkedDown(repo.description)"></span>
<i class="fa fa-edit"></i>
</p>
<div class="description markdown-input" content="repo.description" can-write="true"
field-title="'repository description'"></div>
</div>
</div>
</div>
@ -68,13 +70,19 @@
</div>
<!-- Payment -->
<div class="required-plan" ng-show="repo.is_public == '0' && planRequired">
<div class="required-plan" ng-show="repo.is_public == '0' && planRequired && isUserNamespace">
<div class="alert alert-warning">
In order to make this repository private, youll need to upgrade your plan from <b>{{ subscribedPlan.title }}</b> to <b>{{ planRequired.title }}</b>. This will cost $<span>{{ planRequired.price / 100 }}</span>/month.
</div>
<a class="btn btn-primary" ng-click="upgradePlan()" ng-show="!planChanging">Upgrade now</a>
<i class="fa fa-spinner fa-spin fa-3x" ng-show="planChanging"></i>
</div>
<div class="required-plan" ng-show="repo.is_public == '0' && planRequired && !isUserNamespace">
<div class="alert alert-warning">
This organization has reached its private repository limit. Please contact your administrator.
</div>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,67 @@
<div class="loading" ng-show="loading">
<i class="fa fa-spinner fa-spin fa-3x"></i>
</div>
<div class="loading" ng-show="!loading && !organization">
No matching organization found
</div>
<div class="org-admin container" ng-show="!loading && organization">
<div class="organization-header" organization="organization"></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="#plan">Plan and Usage</a></li>
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#members" ng-click="loadMembers()">Members</a></li>
</ul>
</div>
<!-- Content -->
<div class="col-md-10">
<div class="tab-content">
<!-- Plans tab -->
<div id="plan" class="tab-pane active">
<div class="plan-manager" organization="orgname"></div>
</div>
<!-- Members tab -->
<div id="members" class="tab-pane">
<i class="fa fa-spinner fa-spin fa-3x" ng-show="membersLoading"></i>
<div ng-show="!membersLoading">
<div class="side-controls">
<div class="result-count">
Showing {{(membersFound | filter:search | limitTo:50).length}} of {{(membersFound | filter:search).length}} matching members
</div>
<div class="filter-input">
<input id="member-filter" class="form-control" placeholder="Filter Members" type="text" ng-model="search.$">
</div>
</div>
<table class="table table-striped">
<thead>
<th>User</th>
<th>Teams</th>
</thead>
<tr ng-repeat="memberInfo in (membersFound | filter:search | limitTo:50)">
<td>
<i class="fa fa-user"></i>
{{ memberInfo.username }}
</td>
<td>
<span class="team-link" ng-repeat="team in memberInfo.teams">
<i class="fa fa-group"></i>
<a href="/organization/{{ organization.name }}/teams/{{ team }}">{{ team }}</a>
</span>
</td>
</tr>
</table>
</div>
</div>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,86 @@
<div class="loading" ng-show="loading">
<i class="fa fa-spinner fa-spin fa-3x"></i>
</div>
<div class="loading" ng-show="!loading && !organization">
No matching organization found
</div>
<div class="org-view container" ng-show="!loading && organization">
<div class="organization-header" organization="organization">
<div class="header-buttons" ng-show="organization.is_admin">
<button class="btn btn-success" data-trigger="click" bs-popover="'static/partials/create-team-dialog.html'" data-placement="bottom" ng-click="createTeamShown()"><i class="fa fa-group"></i> Create Team</button>
<a class="btn btn-default" href="/organization/{{ organization.name }}/admin"><i class="fa fa-gear"></i> Settings</a>
</div>
</div>
<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=""
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>
<div class="team-listing" ng-repeat="(name, team) in organization.teams">
<div id="team-{{name}}" class="row">
<div class="col-sm-7 col-md-8">
<div class="team-title">
<i class="fa fa-group"></i>
<span ng-show="team.can_view">
<a href="/organization/{{ organization.name }}/teams/{{ team.name }}">{{ team.name }}</a>
</span>
<span ng-show="!team.can_view">
{{ team.name }}
</span>
</div>
<div class="team-description markdown-view" content="team.description" first-line-only="true"></div>
</div>
<div class="col-sm-5 col-md-4 control-col" ng-show="organization.is_admin">
<span class="role-group" current-role="team.role" role-changed="setRole(role, team.name)" roles="teamRoles"></span>
<button class="btn btn-sm btn-danger" ng-click="askDeleteTeam(team.name)">Delete</button>
</div>
</div>
</div>
</div>
<!-- Modal message dialog -->
<div class="modal fade" id="cannotChangeTeamModal">
<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">Cannot change team</h4>
</div>
<div class="modal-body">
<span ng-show="!roleError">You do not have permission to change properties on teams.</span>
<span ng-show="roleError">{{ roleError }}</span>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
<!-- Modal message dialog -->
<div class="modal fade" id="confirmdeleteModal">
<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 Team?</h4>
</div>
<div class="modal-body">
Are you sure you would like to delete this team? This <b>cannot be undone</b>.
</div>
<div class="modal-footer">
<button type="button" class="btn btn-danger" ng-click="deleteTeam()">Delete Team</button>
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->

View file

@ -0,0 +1,129 @@
<div class="container org-list">
<div class="loading" ng-show="loading">
<i class="fa fa-spinner fa-spin fa-3x"></i>
</div>
<div class="button-bar-right">
<a href="/organizations/new/" 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">
<button class="btn btn-primary">
<i class="fa fa-caret-square-o-right"></i>
Convert account
</button>
</a>
</div>
<!-- Organizations -->
<div ng-show="user.organizations.length > 0">
<h2>Organizations</h2>
<div class="organization-listing" ng-repeat="organization in user.organizations">
<img class="gravatar" src="//www.gravatar.com/avatar/{{ organization.gravatar }}?s=32&amp;d=identicon">
<a class="org-title" href="/organization/{{ organization.name }}">{{ organization.name }}</a>
</div>
</div>
<!-- Organization Help/Tour -->
<div class="product-tour" ng-show="!user.organizations || user.organizations.length == 0">
<div class="tour-section row">
<div class="col-md-12">
<div class="tour-section-title">Organizations</div>
<div class="tour-section-description">
Organizations in Quay provide unique features for businesses and other
groups, including team-based sharing and fine-grained permission controls.
</div>
</div>
</div>
<div class="tour-section row">
<div class="col-md-7"><img src="/static/img/org-repo-list.png" title="Repositories - Quay" 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">
Your organization is the focal point for all activity that occurs within
your public or private repositories. Your repositories are centrally visible
and managed within the namespace of your organization. You may share
your repositories with as many users and teams as you like, without
any additional cost.
</div>
</div>
</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" 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">
Your organization allows you to view your private repository count
and manage billing settings in a centralized place.
</div>
<div class="tour-section-description">
You can also see all of the users who have access to your organization
and the teams of which they are members. This allows you to audit the
access that has been granted in your organization.
</div>
</div>
</div>
<div class="tour-section row">
<div class="col-md-7"><img src="/static/img/org-teams.png" title="buynlarge - Quay" data-screenshot-url="https://quay.io/organization/buynlarge" class="img-responsive"></div>
<div class="col-md-5">
<div class="tour-section-title">Teams simplify access controls</div>
<div class="tour-section-description">
Teams allow your organization to delegate access to your namespace and
repositories in a controlled fashion. Each team has permissions that
apply across the entire org, and can also be given specific levels of
access to specific repositories. A user is switching roles? No problem,
change their team membership and their access will be adjusted accordingly.
</div>
<div class="tour-section-description">
Owners of your organization, and members of other teams with
administrator privileges, have full permissions to all repositories
in the organization, as well as permissions to view and adjust the
account settings for the organization. Add users to these teams with
caution.
</div>
</div>
</div>
<div class="tour-section row">
<div class="col-md-7 col-md-push-5"><img src="/static/img/org-repo-admin.png" title="buynlarge/orgrepo - Quay" data-screenshot-url="https://quay.io/repository/buynlarge/orgrepo" class="img-responsive"></div>
<div class="col-md-5 col-md-pull-7">
<div class="tour-section-title">Fine-grained control of sharing</div>
<div class="tour-section-description">
Repositories that you create within your organization can be assigned
fine-grained permissions just like any other repository. You can also
add teams that exist in your organization, or individual users from
inside our outside your organization.
</div>
<div class="tour-section-description">
In order to protect your intellectual property, we warn you before
you share your repositories with anyone who is not currently a member
of a team in your organization.
</div>
</div>
</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">
<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">
<button class="btn btn-primary">
<i class="fa fa-caret-square-o-right"></i>
Convert account
</button>
</a>
</div>
</div>
</div>

View file

@ -7,15 +7,39 @@
All plans include <span class="feature">unlimited public repositories</span> and <span class="feature">unlimited sharing</span>. All paid plans have a <span class="feature">14-day free trial</span>.
</div>
<div class="plans-list">
<div class="plan" ng-repeat="plan in plans" ng-class="plan.stripeId">
<div class="plan-title">{{ plan.title }}</div>
<div class="plan-price">${{ plan.price/100 }}</div>
<div class="count"><b>{{ plan.privateRepos }}</b> private repositories</div>
<div class="description">{{ plan.audience }}</div>
<div class="smaller">SSL secured connections</div>
<div class="row plans-list">
<div class="col-xs-0 col-lg-1"></div>
<div class="col-lg-2 col-xs-4 plan-container" ng-repeat="plan in plans.user">
<div class="plan" ng-class="plan.stripeId">
<div class="plan-title">{{ plan.title }}</div>
<div class="plan-price">${{ plan.price/100 }}</div>
<div class="count"><b>{{ plan.privateRepos }}</b> private repositories</div>
<div class="description">{{ plan.audience }}</div>
<div class="smaller">SSL secured connections</div>
<button class="btn btn-primary btn-block" ng-click="buyNow(plan.stripeId)">Sign Up Now</button>
</div>
</div>
</div>
<button class="btn btn-primary btn-block" ng-click="buyNow(plan.stripeId)">Sign Up Now</button>
<div class="callout">
Business Plan Pricing
</div>
<div class="all-plans">
All business plans include all of the personal plan features, plus: <span class="business-feature">organizations</span> and <span class="business-feature">teams</span> with <span class="business-feature">delegated access</span> to the organization. All business plans have a <span class="business-feature">14-day free trial</span>.
</div>
<div class="row plans-list">
<div class="col-xs-0 col-lg-1"></div>
<div class="col-lg-2 col-xs-4 plan-container" ng-repeat="plan in plans.business">
<div class="plan business-plan" ng-class="plan.stripeId">
<div class="plan-title">{{ plan.title }}</div>
<div class="plan-price">${{ plan.price/100 }}</div>
<div class="count"><b>{{ plan.privateRepos }}</b> private repositories</div>
<div class="description">{{ plan.audience }}</div>
<div class="smaller">SSL secured connections</div>
<button class="btn btn-success btn-block" ng-click="createOrg(plan.stripeId)">Sign Up Now</button>
</div>
</div>
</div>

View file

@ -20,44 +20,61 @@
<!-- User Access Permissions -->
<div class="panel panel-default">
<div class="panel-heading">User Access Permissions
<div class="panel-heading">User <span ng-show="repo.is_organization">and Team</span> Access Permissions
<i class="info-icon fa fa-info-circle" data-placement="left" data-content="Allow any number of users to read, write or administer this repository"></i>
<i class="info-icon fa fa-info-circle" data-placement="left" data-content="Allow any number of users or teams to read, write or administer this repository"></i>
</div>
<div class="panel-body">
<table class="permissions">
<thead>
<tr>
<td>User</td>
<td>User<span ng-show="repo.is_organization">/Team</span></td>
<td>Permissions</td>
<td></td>
<td style="width: 95px;"></td>
</tr>
</thead>
<tr ng-repeat="(username, permission) in permissions">
<td class="user">
<!-- Team Permissions -->
<tr ng-repeat="(name, permission) in permissions['team']">
<td class="team entity">
<i class="fa fa-group"></i>
<span><a href="/organization/{{ repo.namespace }}/teams/{{ name }}">{{name}}</a></span>
</td>
<td class="user-permissions">
<span class="role-group" current-role="permission.role" role-changed="setRole(role, name, 'team')" roles="roles"></span>
</td>
<td>
<span class="delete-ui" tabindex="0">
<span class="delete-ui-button" ng-click="deleteRole(name, 'team')"><button class="btn btn-danger">Delete</button></span>
<i class="fa fa-times" bs-tooltip="tooltip.title" data-placement="right" title="Delete Permission"></i>
</span>
</td>
</tr>
<!-- User Permissions -->
<tr ng-repeat="(name, permission) in permissions['user']">
<td class="{{ 'user entity ' + (permission.is_org_member? '' : 'outside') }}">
<i class="fa fa-user"></i>
<span>{{username}}</span>
<span>{{name}}</span>
<i class="fa fa-exclamation-triangle" ng-show="permission.is_org_member === false" data-trigger="hover" bs-popover="{'content': 'This user is not a member of the organization'}"></i>
</td>
<td class="user-permissions">
<div class="btn-group btn-group-sm">
<button type="button" class="btn btn-default" ng-click="setRole(username, 'read')" ng-class="{read: 'active', write: '', admin: ''}[permission.role]">Read only</button>
<button type="button" class="btn btn-default" ng-click="setRole(username, 'write')" ng-class="{read: '', write: 'active', admin: ''}[permission.role]">Write</button>
<button type="button" class="btn btn-default" ng-click="setRole(username, 'admin')" ng-class="{read: '', write: '', admin: 'active'}[permission.role]">Admin</button>
<span class="role-group" current-role="permission.role" role-changed="setRole(role, name, 'user')" roles="roles"></span>
</div>
</td>
<td>
<span class="delete-ui" tabindex="0" title="Delete Permission">
<span class="delete-ui-button" ng-click="deleteRole(username)"><button class="btn btn-danger">Delete</button></span>
<i class="fa fa-times"></i>
<span class="delete-ui-button" ng-click="deleteRole(name, 'user')"><button class="btn btn-danger">Delete</button></span>
<i class="fa fa-times" bs-tooltip="tooltip.title" data-placement="right" title="Delete Permission"></i>
</span>
</td>
</tr>
<tr>
<td colspan="2">
<input id="userSearch" class="form-control" placeholder="Add new user...">
<span class="entity-search" organization="repo.namespace" input-title="'Add a ' + (repo.is_organization ? 'team or ' : '') + 'user...'" entity-selected="addNewPermission"></span>
</td>
</tr>
</table>
@ -93,9 +110,9 @@
</div>
</td>
<td>
<span class="delete-ui" tabindex="0" title="Delete Token">
<span class="delete-ui" tabindex="0">
<span class="delete-ui-button" ng-click="deleteToken(token.code)"><button class="btn btn-danger" type="button">Delete</button></span>
<i class="fa fa-times"></i>
<i class="fa fa-times" bs-tooltip="tooltip.title" data-placement="right" title="Delete Token"></i>
</span>
</td>
</tr>
@ -246,7 +263,7 @@
<!-- Modal message dialog -->
<div class="modal fade" id="onlyadminModal">
<div class="modal fade" id="channgechangepermModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
@ -254,7 +271,8 @@
<h4 class="modal-title">Cannot change permissions</h4>
</div>
<div class="modal-body">
The selected permissions could not be changed because the user is the only <b>admin</b> on the repo.
<span ng-show="!changePermError">You do not have permission to change the permissions on the repository.</span>
<span ng-show="changePermError">{{ changePermError }}</span>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
@ -283,4 +301,24 @@
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
<!-- Modal message dialog -->
<div class="modal fade" id="confirmaddoutsideModal">
<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">Add User?</h4>
</div>
<div class="modal-body">
The selected user is outside of your organization. Are you sure you want to grant the user access to this repository?
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" ng-click="grantRole()">Yes, I'm sure</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

@ -4,25 +4,41 @@
<div class="container ready-indicator" ng-show="!loading" data-status="{{ loading ? '' : 'ready' }}">
<div class="repo-list" ng-show="!user.anonymous">
<a href="/new/">
<button class="btn btn-success" style="float: right">
<i class="fa fa-upload user-tool" title="Create new repository"></i>
Create Repository
</button>
</a>
<div ng-class="user.organizations.length ? 'section-header' : ''">
<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>
Create Repository
</button>
</a>
<a href="/organization/{{ namespace }}" ng-show="namespace != user.username">
<button class="btn btn-default">
<i class="fa fa-group user-tool"></i>
View Organization
</button>
</a>
</div>
<span class="namespace-selector" user="user" namespace="namespace" ng-show="user.organizations"></span>
</div>
<h3>Your Repositories</h3>
<div ng-show="private_repositories.length > 0">
<div class="repo-listing" ng-repeat="repository in private_repositories">
<h3 ng-show="namespace == user.username">Your Repositories</h3>
<h3 ng-show="namespace != user.username">Repositories</h3>
<div ng-show="user_repositories.length > 0">
<div class="repo-listing" ng-repeat="repository in user_repositories">
<span class="repo-circle no-background" repo="repository"></span>
<a ng-href="/repository/{{repository.namespace}}/{{ repository.name }}">{{repository.namespace}}/{{repository.name}}</a>
<div class="description" ng-bind-html-unsafe="getCommentFirstLine(repository.description)"></div>
<div class="description markdown-view" content="repository.description" first-line-only="true"></div>
</div>
</div>
<div ng-show="private_repositories.length == 0" style="padding:20px;">
<div ng-show="user_repositories.length == 0" style="padding:20px;">
<div class="alert alert-info">
<h4>You don't have any repositories yet!</h4>
<h4 ng-show="namespace == user.username">You don't have any repositories yet!</h4>
<h4 ng-show="namespace != user.username">This organization doesn't have any repositories, or you have not been provided access.</h4>
<a href="/guide"><b>Click here</b> to learn how to create a repository</a>
</div>
@ -34,7 +50,7 @@
<div class="repo-listing" ng-repeat="repository in public_repositories">
<span class="repo-circle no-background" repo="repository"></span>
<a ng-href="/repository/{{repository.namespace}}/{{ repository.name }}">{{repository.namespace}}/{{repository.name}}</a>
<div class="description" ng-bind-html-unsafe="getCommentFirstLine(repository.description)"></div>
<div class="description markdown-view" content="repository.description" first-line-only="true"></div>
</div>
</div>
</div>

View file

@ -12,22 +12,7 @@
</div>
<div id="collapseSignin" class="panel-collapse collapse in">
<div class="panel-body">
<form class="form-signin" ng-submit="signin();">
<input type="text" class="form-control input-lg" name="username" placeholder="Username" ng-model="user.username" autofocus>
<input type="password" class="form-control input-lg" name="password" placeholder="Password" ng-model="user.password">
<button class="btn btn-lg btn-primary btn-block" type="submit">Sign In</button>
<span class="social-alternate">
<i class="fa fa-circle"></i>
<span class="inner-text">OR</span>
</span>
<a id='github-signin-link' href="https://github.com/login/oauth/authorize?client_id={{ githubClientId }}&scope=user:email{{ mixpanelDistinctIdClause }}" class="btn btn-primary btn-lg btn-block"><i class="fa fa-github fa-lg"></i> Sign In with GitHub</a>
</form>
<div class="alert alert-danger" ng-show="invalidCredentials">Invalid username or password.</div>
<div class="alert alert-danger" ng-show="needsEmailVerification">You must verify your email address before you can sign in.</div>
<div class="signin-form"></div>
</div>
</div>
</div>
@ -56,17 +41,3 @@
</div>
</div>
</div>
<!-- <script type="text/javascript">
function appendMixpanelId() {
if (mixpanel.get_distinct_id !== undefined) {
var signinLink = document.getElementById("github-signin-link");
signinLink.href += ("&state=" + mixpanel.get_distinct_id());
} else {
// Mixpanel not yet loaded, try again later
window.setTimeout(appendMixpanelId, 200);
}
};
appendMixpanelId();
</script> -->

View file

@ -0,0 +1,79 @@
<div class="loading" ng-show="loading">
<i class="fa fa-spinner fa-spin fa-3x"></i>
</div>
<div class="loading" ng-show="!loading && !organization">
No matching team found
</div>
<div class="team-view container" ng-show="!loading && organization">
<div class="organization-header" organization="organization" team-name="teamname"></div>
<div class="description markdown-input" content="team.description" can-write="organization.is_admin"
content-changed="updateForDescription" field-title="'team description'"></div>
<div class="panel panel-default">
<div class="panel-heading">Team Members
<i class="info-icon fa fa-info-circle" data-placement="left" data-content="Users that inherit all permissions delegated to this team"></i>
</div>
<div class="panel-body">
<table class="permissions">
<tr ng-repeat="(name, member) in members">
<td class="user entity">
<i class="fa fa-user"></i>
<span>{{ member.username }}</span>
</td>
<td>
<span class="delete-ui" tabindex="0" title="Remove User" ng-show="canEditMembers">
<span class="delete-ui-button" ng-click="removeMember(member.username)"><button class="btn btn-danger">Remove</button></span>
<i class="fa fa-times"></i>
</span>
</td>
</tr>
<tr ng-show="canEditMembers">
<td colspan="2">
<span class="entity-search" organization="''" input-title="'Add a user...'" entity-selected="addNewMember"></span>
</td>
</tr>
</table>
</div>
</div>
</div>
<!-- Modal message dialog -->
<div class="modal fade" id="cannotChangeTeamModal">
<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">Cannot change team</h4>
</div>
<div class="modal-body">
You do not have permission to change properties of this team.
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
<!-- Modal message dialog -->
<div class="modal fade" id="cannotChangeMembersModal">
<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">Cannot change members</h4>
</div>
<div class="modal-body">
You do not have permission to change the members of this team.
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->

View file

@ -1,80 +1,181 @@
<div class="container user-admin">
<div class="loading" ng-show="planLoading || planChanging">
<i class="fa fa-spinner fa-spin fa-3x"></i>
</div>
<div class="row" ng-show="errorMessage">
<div class="col-md-12">
<div class="alert alert-danger">{{ errorMessage }}</div>
<div class="loading" ng-show="loading">
<i class="fa fa-spinner fa-spin fa-3x"></i>
</div>
<div class="loading" ng-show="!loading && !user">
No matching user found
</div>
<div class="user-admin container" ng-show="!loading && user">
<div class="row">
<div class="organization-header-element">
<img src="//www.gravatar.com/avatar/{{ user.gravatar }}?s=24&amp;d=identicon">
<span class="organization-name">
{{ user.username }}
</span>
</div>
</div>
<div class="row" ng-show="askForPassword">
<div class="col-md-12">
<div class="alert alert-warning">Your account does not currently have a password. You will need to create a password before you will be able to <strong>push</strong> or <strong>pull</strong> repositories.</div>
</div>
</div>
<div class="row" ng-hide="planLoading">
<div class="col-md-3" ng-repeat='plan in plans'>
<div class="panel" ng-class="{'panel-success': subscription.plan == plan.stripeId, 'panel-default': subscription.plan != plan.stripeId}">
<div class="panel-heading">
{{ plan.title }}
<span class="pull-right" ng-show="subscription.plan == plan.stripeId">
<i class="fa fa-ok"></i>
Subscribed
</span>
</div>
<div class="panel-body panel-plan">
<div class="plan-price">${{ plan.price / 100 }}</div>
<div class="plan-description"><b>{{ plan.privateRepos }}</b> Private Repositories</div>
<div ng-switch='plan.stripeId'>
<div ng-switch-when='free'>
<button class="btn button-hidden">Hidden!</button>
</div>
<div ng-switch-default>
<button class="btn btn-primary" ng-show="subscription.plan === 'free'" ng-click="subscribe(plan.stripeId)">Subscribe</button>
<button class="btn btn-default" ng-hide="subscription.plan === 'free' || subscription.plan === plan.stripeId" ng-click="changeSubscription(plan.stripeId)">Change</button>
<button class="btn btn-danger" ng-show="subscription.plan === plan.stripeId" ng-click="cancelSubscription()">Cancel</button>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row" ng-show="subscription">
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">
Plan Usage
</div>
<div class="panel-body">
<div class="used-description">
<b>{{ subscription.usedPrivateRepos }}</b> of <b>{{ subscribedPlan.privateRepos }}</b> private repositories used
</div>
<div class="progress">
<div ng-class="'progress-bar ' + (planUsagePercent > 90 ? 'progress-bar-danger' : '')" role="progressbar" aria-valuenow="{{ subscription.usedPrivateRepos }}" aria-valuemin="0" aria-valuemax="{{ subscribedPlan.privateRepos }}" style="width: {{ planUsagePercent }}%;">
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="loading" ng-show="updatingUser">
<i class="fa fa-spinner fa-spin fa-3x"></i>
<!-- 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="#password">Set Password</a></li>
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#migrate" id="migrateTab">Convert to Organization</a></li>
</ul>
</div>
<div class="col-md-3">
<div class="panel panel-default">
<div class="panel-heading">
Change Password
<!-- Content -->
<div class="col-md-10">
<div class="tab-content">
<!-- Plans tab -->
<div id="plan" class="tab-pane active">
<div class="plan-manager" user="user.username" ready-for-plan="readyForPlan()"></div>
</div>
<div class="panel-body">
<form class="form-change-pw" 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="user.password" required>
<input type="password" class="form-control" placeholder="Verify your new password" ng-model="user.repeatPassword" match="user.password" required>
<button class="btn btn-danger" ng-disabled="changePasswordForm.$invalid" type="submit" analytics-on analytics-event="register">Change Password</button>
<span class="help-block" ng-show="changePasswordSuccess">Password changed successfully</span>
</form>
<!-- Change password tab -->
<div id="password" class="tab-pane">
<div class="loading" ng-show="updatingUser">
<i class="fa fa-spinner fa-spin fa-3x"></i>
</div>
<div class="row">
<form class="form-change-pw col-md-6" 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="user.password" required>
<input type="password" class="form-control" placeholder="Verify your new password" ng-model="user.repeatPassword"
match="user.password" required>
<button class="btn btn-danger" ng-disabled="changePasswordForm.$invalid" type="submit"
analytics-on analytics-event="register">Change Password</button>
<span class="help-block" ng-show="changePasswordSuccess">Password changed successfully</span>
</form>
</div>
</div>
<!-- Convert to organization tab -->
<div id="migrate" class="tab-pane">
<!-- Step 0 -->
<div class="panel" ng-show="convertStep == 0">
<div class="panel-body" ng-show="user.organizations.length > 0">
<div class="alert alert-info">
Cannot convert this account into an organization, as it is a member of {{user.organizations.length}} other
organization{{user.organizations.length > 1 ? 's' : ''}}. Please leave
{{user.organizations.length > 1 ? 'those organizations' : 'that organization'}} first.
</div>
</div>
<div class="panel-body" ng-show="user.organizations.length == 0">
<div class="alert alert-danger">
Converting a user account into an organization <b>cannot be undone</b>.<br> Here be many fire-breathing dragons!
</div>
<button class="btn btn-danger" ng-click="showConvertForm()">Start conversion process</button>
</div>
</div>
<!-- Step 1 -->
<div class="convert-form" ng-show="convertStep == 1">
<h3>Convert to organization</h3>
<form method="post" name="convertForm" id="convertForm" ng-submit="convertToOrg()">
<div class="form-group">
<label for="orgName">Organization Name</label>
<div class="existing-data">
<img src="//www.gravatar.com/avatar/{{ user.gravatar }}?s=24&amp;d=identicon">
{{ user.username }}</div>
<span class="description">This will continue to be the namespace for your repositories</span>
</div>
<div class="form-group">
<label for="orgName">Admin User</label>
<input id="adminUsername" name="adminUsername" type="text" class="form-control" placeholder="Admin Username"
ng-model="org.adminUser" required autofocus>
<input id="adminPassword" name="adminPassword" type="password" class="form-control" placeholder="Admin Password"
ng-model="org.adminPassword" required>
<span class="description">The username and password for an <b>existing account</b> that will become administrator of the organization</span>
</div>
<!-- Plans Table -->
<div class="form-group plan-group">
<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">
Convert To Organization
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Modal message dialog -->
<div class="modal fade" id="cannotconvertModal">
<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">Cannot convert account</h4>
</div>
<div class="modal-body">
Your account could not be converted. Please try again in a moment.
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
<!-- Modal message dialog -->
<div class="modal fade" id="invalidadminModal">
<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">Username or password invalid</h4>
</div>
<div class="modal-body">
The username or password specified for the admin account is not valid.
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
<!-- Modal message dialog -->
<div class="modal fade" id="reallyconvertModal">
<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">Convert to organization?</h4>
</div>
<div class="modal-body">
<div class="alert alert-danger">You will not be able to login to this account once converted</div>
<div>Are you <b>absolutely sure</b> you would like to convert this account to an organization? Once done, there is no going back.</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-danger" data-dismiss="modal" ng-click="reallyConvert()">Absolutely: Convert Now</button>
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->

View file

@ -14,7 +14,7 @@
<span style="color: #aaa;"> {{repo.namespace}}</span> <span style="color: #ccc">/</span> {{repo.name}}
<span class="settings-cog" ng-show="repo.can_admin" title="Repository Settings">
<span class="settings-cog" ng-show="repo.can_admin" title="Repository Settings" bs-tooltip="tooltip.title" data-placement="bottom">
<a href="{{ '/repository/' + repo.namespace + '/' + repo.name + '/admin' }}">
<i class="fa fa-cog fa-lg"></i>
</a>
@ -52,12 +52,8 @@
</div>
<!-- Description -->
<div class="description">
<p ng-class="'lead ' + (repo.can_write ? 'editable' : 'noteditable')" ng-click="editDescription()">
<span class="content" ng-bind-html-unsafe="getMarkedDown(repo.description)"></span>
<i class="fa fa-edit"></i>
</p>
</div>
<div class="description markdown-input" content="repo.description" can-write="repo.can_write"
content-changed="updateForDescription" field-title="'repository description'"></div>
<!-- Empty message -->
<div class="repo-content" ng-show="!currentTag.image && !repo.is_building">
@ -79,7 +75,7 @@
<div class="panel panel-default">
<div class="panel-heading">
<!-- Tag dropdown -->
<div class="tag-dropdown dropdown" title="Tags">
<div class="tag-dropdown dropdown" title="Tags" bs-tooltip="tooltip.title" data-placement="top">
<i class="fa fa-tag"><span class="tag-count">{{getTagCount(repo)}}</span></i>
<a href="javascript:void(0)" class="dropdown-toggle" data-toggle="dropdown">{{currentTag.name}} <b class="caret"></b></a>
<ul class="dropdown-menu">
@ -107,7 +103,7 @@
<div class="panel panel-default">
<div class="panel-heading">
<!-- Image dropdown -->
<div class="tag-dropdown dropdown" title="Images">
<div class="tag-dropdown dropdown" title="Images" bs-tooltip="tooltip.title" data-placement="top">
<i class="fa fa-archive"><span class="tag-count">{{imageHistory.length}}</span></i>
<a href="javascript:void(0)" class="dropdown-toggle" data-toggle="dropdown">{{currentImage.id.substr(0, 12)}} <b class="caret"></b></a>
<ul class="dropdown-menu">
@ -122,7 +118,8 @@
<div class="panel-body">
<div id="current-image">
<div ng-show="currentImage.comment">
<blockquote style="margin-top: 10px;" ng-bind-html-unsafe="getMarkedDown(currentImage.comment)">
<blockquote style="margin-top: 10px;">
<span class="markdown-view" content="currentImage.comment"></span>
</blockquote>
</div>
@ -141,15 +138,18 @@
<div class="changes-container small-changes-container"
ng-show="currentImageChanges.changed.length || currentImageChanges.added.length || currentImageChanges.removed.length">
<div class="changes-count-container accordion-toggle" data-toggle="collapse" data-parent="#accordion" data-target="#collapseChanges">
<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" title="Files Added"
bs-tooltip="tooltip.title" data-placement="top">
<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" title="Files Removed"
bs-tooltip="tooltip.title" data-placement="top">
<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" title="Files Changed"
bs-tooltip="tooltip.title" data-placement="top">
<i class="fa fa-pencil-square"></i>
<b>{{currentImageChanges.changed.length}}</b>
</span>
@ -182,29 +182,4 @@
</div>
</div>
</div>
<!-- Modal edit for the description -->
<div class="modal fade" id="editModal">
<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">Edit Repository Description</h4>
</div>
<div class="modal-body">
<div class="wmd-panel">
<div id="wmd-button-bar-description"></div>
<textarea class="wmd-input" id="wmd-input-description" placeholder="Enter description">{{ repo.description }}</textarea>
</div>
<div id="wmd-preview-description" class="wmd-panel wmd-preview"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" ng-click="saveDescription()">Save changes</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
</div>