Merge remote-tracking branch 'origin/orgs'
Conflicts: static/partials/repo-admin.html
|
@ -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;
|
||||
}
|
1
static/directives/entity-search.html
Normal file
|
@ -0,0 +1 @@
|
|||
<input class="entity-search-control form-control">
|
31
static/directives/markdown-input.html
Normal 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">×</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>
|
1
static/directives/markdown-view.html
Normal file
|
@ -0,0 +1 @@
|
|||
<span class="markdown-view-content" ng-bind-html-unsafe="getMarkedDown(content, firstLineOnly)"></span>
|
34
static/directives/namespace-selector.html
Normal 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>
|
18
static/directives/organization-header.html
Normal file
|
@ -0,0 +1,18 @@
|
|||
<div class="organization-header-element">
|
||||
<img src="//www.gravatar.com/avatar/{{ organization.gravatar }}?s=24&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>
|
62
static/directives/plan-manager.html
Normal 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>
|
23
static/directives/plans-table.html
Normal 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>
|
5
static/directives/role-group.html
Normal 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>
|
25
static/directives/signin-form.html
Normal 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
After Width: | Height: | Size: 50 KiB |
BIN
static/img/org-repo-admin.png
Normal file
After Width: | Height: | Size: 64 KiB |
BIN
static/img/org-repo-list.png
Normal file
After Width: | Height: | Size: 37 KiB |
BIN
static/img/org-teams.png
Normal file
After Width: | Height: | Size: 38 KiB |
BIN
static/img/quay-icon-stripe.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 61 KiB |
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 80 KiB |
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 99 KiB |
Before Width: | Height: | Size: 197 KiB After Width: | Height: | Size: 194 KiB |
709
static/js/app.js
|
@ -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) {
|
||||
|
|
|
@ -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
|
@ -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);
|
2
static/lib/angular-strap.min.js
vendored
3
static/partials/create-team-dialog.html
Normal 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>
|
|
@ -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>
|
||||
|
|
|
@ -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 & 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>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
|
|
102
static/partials/new-organization.html
Normal 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>
|
|
@ -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, you’ll 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>
|
||||
|
|
67
static/partials/org-admin.html
Normal 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>
|
86
static/partials/org-view.html
Normal 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">×</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">×</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 -->
|
129
static/partials/organizations.html
Normal 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&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>
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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">×</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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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> -->
|
||||
|
|
79
static/partials/team-view.html
Normal 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">×</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">×</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 -->
|
|
@ -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&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&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">×</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">×</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">×</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 -->
|
||||
|
|
|
@ -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">×</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>
|
||||
|
|