Merge branch 'master' into star

This commit is contained in:
Jimmy Zelinskie 2015-02-18 17:36:58 -05:00
commit 917dd6b674
229 changed files with 10807 additions and 3003 deletions

720
static/css/core-ui.css Normal file
View file

@ -0,0 +1,720 @@
.co-options-menu .fa-gear {
color: #999;
cursor: pointer;
}
.co-options-menu .dropdown.open .fa-gear {
color: #428BCA;
}
.co-img-bg-network {
background: url('/static/img/network-tile.png') left top repeat, linear-gradient(30deg, #2277ad, #144768) no-repeat left top fixed;
background-color: #2277ad;
background-size: auto, 100% 100%;
}
.co-m-navbar {
background-color: white;
margin: 0;
padding-left: 10px;
}
.co-fx-box-shadow {
-webkit-box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2);
-moz-box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2);
-ms-box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2);
-o-box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2);
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2);
}
.co-fx-box-shadow-heavy {
-webkit-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.4);
-moz-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.4);
-ms-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.4);
-o-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.4);
box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.4);
}
.co-fx-text-shadow {
text-shadow: rgba(0, 0, 0, 1) 1px 1px 2px;
}
.co-nav-title {
height: 70px;
margin-top: -22px;
}
.co-nav-title .co-nav-title-content {
color: white;
text-align: center;
}
.co-tab-container {
padding: 0px;
}
.co-tabs {
margin: 0px;
padding: 0px;
width: 82px;
background-color: #e8f1f6;
border-right: 1px solid #DDE7ED;
display: table-cell;
float: none;
vertical-align: top;
}
.co-tab-content {
width: 100%;
display: table-cell;
float: none;
padding: 20px;
}
.co-tabs li {
list-style: none;
display: block;
border-bottom: 1px solid #DDE7ED;
}
.co-tabs li.active {
background-color: white;
border-right: 1px solid white;
margin-right: -1px;
}
.co-tabs li a {
display: block;
width: 82px;
height: 82px;
line-height: 82px;
text-align: center;
font-size: 36px;
color: gray;
}
.co-tabs li.active a {
color: black;
}
.co-main-content-panel {
margin-bottom: 20px;
background-color: #fff;
border: 1px solid transparent;
padding: 10px;
-webkit-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.4);
-moz-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.4);
-ms-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.4);
-o-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.4);
box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.4);
}
.co-tab-panel {
padding: 0px;
}
.cor-log-box {
width: 100%;
height: 550px;
position: relative;
}
.co-log-viewer {
position: absolute;
top: 20px;
left: 20px;
right: 20px;
height: 500px;
padding: 20px;
background: rgb(55, 55, 55);
border: 1px solid black;
color: white;
overflow: scroll;
}
.co-log-viewer .co-log-content {
font-family: Consolas, "Lucida Console", Monaco, monospace;
font-size: 12px;
white-space: pre;
}
.cor-log-box .co-log-viewer-new-logs i {
margin-left: 10px;
display: inline-block;
}
.cor-log-box .co-log-viewer-new-logs {
cursor: pointer;
position: absolute;
bottom: 40px;
right: 30px;
padding: 10px;
color: white;
border-radius: 10px;
background: rgba(72, 158, 72, 0.8);
}
.co-panel {
margin-bottom: 40px;
/*border: 1px solid #eee;*/
}
.co-panel .co-panel-heading img {
margin-right: 6px;
width: 24px;
}
.co-panel .co-panel-heading i.fa {
margin-right: 6px;
width: 24px;
text-align: center;
}
.co-panel .co-panel-heading {
padding: 6px;
/*background: #eee;*/
border-bottom: 1px solid #eee;
margin-bottom: 4px;
font-size: 135%;
padding-left: 10px;
}
.co-panel .co-panel-body {
padding: 10px;
}
.co-panel .co-panel-button-bar {
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid #eee;
}
.config-setup-tool-element .help-text {
margin-top: 6px;
color: #aaa;
}
.config-setup-tool-element .description {
padding: 6px;
}
.config-setup-tool-element .config-table > tbody > tr > td:first-child {
padding-top: 14px;
font-weight: bold;
}
.config-setup-tool-element .config-table > tbody > tr > td.non-input {
padding-top: 8px;
}
.config-setup-tool-element .config-table > tbody > tr > td {
padding: 8px;
vertical-align: top;
}
.config-setup-tool-element .config-table > tbody > tr > td .config-numeric-field-element {
width: 100px;
}
.config-setup-tool-element .config-table > tbody > tr > td .config-string-field-element {
width: 400px;
}
.config-contact-field {
margin-bottom: 4px;
}
.config-contact-field .dropdown button {
width: 100px;
text-align: left;
}
.config-contact-field .dropdown button .caret {
float: right;
margin-top: 9px;
}
.config-contact-field .dropdown button i.fa {
margin-right: 6px;
width: 14px;
text-align: center;
display: inline-block;
}
.config-contact-field .form-control {
width: 350px;
}
.config-list-field-element .empty {
color: #ccc;
margin-bottom: 10px;
display: block;
}
.config-list-field-element input {
width: 350px;
}
.config-setup-tool-element .inner-table {
margin-left: 10px;
}
.config-setup-tool-element .inner-table tr td:first-child {
font-weight: bold;
}
.config-setup-tool-element .inner-table td {
padding: 6px;
}
.config-file-field-element input {
display: inline-block;
margin-left: 10px;
}
.co-checkbox {
position: relative;
}
.co-checkbox input {
display: none;
}
.co-checkbox label {
position: relative;
padding-left: 28px;
cursor: pointer;
}
.co-checkbox label:before {
content: '';
cursor: pointer;
position: absolute;
width: 20px;
height: 20px;
top: 0;
left: 0;
border-radius: 4px;
-webkit-box-shadow: inset 0px 1px 1px rgba(0,0,0,0.5), 0px 1px 0px rgba(255,255,255,.4);
-moz-box-shadow: inset 0px 1px 1px rgba(0,0,0,0.5), 0px 1px 0px rgba(255,255,255,.4);
box-shadow: inset 0px 1px 1px rgba(0,0,0,0.5), 0px 1px 0px rgba(255,255,255,.4);
background: -webkit-linear-gradient(top, #222 0%, #45484d 100%);
background: -moz-linear-gradient(top, #222 0%, #45484d 100%);
background: -o-linear-gradient(top, #222 0%, #45484d 100%);
background: -ms-linear-gradient(top, #222 0%, #45484d 100%);
background: linear-gradient(top, #222 0%, #45484d 100%);
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#222', endColorstr='#45484d',GradientType=0 );
}
.co-checkbox label:after {
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)";
filter: alpha(opacity=0);
opacity: 0;
content: '';
position: absolute;
width: 11px;
height: 7px;
background: transparent;
top: 5px;
left: 4px;
border: 3px solid #fcfff4;
border-top: none;
border-right: none;
-webkit-transform: rotate(-45deg);
-moz-transform: rotate(-45deg);
-o-transform: rotate(-45deg);
-ms-transform: rotate(-45deg);
transform: rotate(-45deg);
}
.co-checkbox label:hover::after {
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=30)";
filter: alpha(opacity=30);
opacity: 0.3;
}
.co-checkbox input[type=checkbox]:checked + label:after {
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=100)";
filter: alpha(opacity=100);
opacity: 1;
border: 3px solid rgb(26, 255, 26);
border-top: none;
border-right: none;
}
.co-floating-bottom-bar {
height: 50px;
}
.co-floating-bottom-bar.floating {
position: fixed;
bottom: 0px;
}
.config-setup-tool .cor-floating-bottom-bar button i.fa {
margin-right: 6px;
}
.config-setup-tool .service-verification {
padding: 20px;
background: #343434;
color: white;
margin-bottom: -14px;
}
.config-setup-tool .service-verification-row {
margin-bottom: 6px;
}
.config-setup-tool .service-verification-row .service-title {
font-variant: small-caps;
font-size: 145%;
vertical-align: middle;
}
#validateAndSaveModal .fa-warning {
font-size: 22px;
margin-right: 10px;
vertical-align: middle;
color: rgb(255, 186, 53);
}
#validateAndSaveModal .fa-check-circle {
font-size: 22px;
margin-right: 10px;
vertical-align: middle;
color: rgb(53, 186, 53);
}
.config-setup-tool .service-verification-error {
white-space: pre;
margin-top: 10px;
margin-left: 36px;
margin-bottom: 20px;
max-height: 250px;
overflow: auto;
border: 1px solid #797979;
background: black;
padding: 6px;
font-family: Consolas, "Lucida Console", Monaco, monospace;
font-size: 12px;
}
.co-m-loader, .co-m-inline-loader {
min-width: 28px; }
.co-m-loader {
display: block;
position: absolute;
left: 50%;
top: 50%;
margin: -11px 0 0 -13px; }
.co-m-inline-loader {
display: inline-block;
cursor: default; }
.co-m-inline-loader:hover {
text-decoration: none; }
.co-m-loader-dot__one, .co-m-loader-dot__two, .co-m-loader-dot__three {
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
-ms-border-radius: 3px;
-o-border-radius: 3px;
border-radius: 3px;
animation-fill-mode: both;
-webkit-animation-fill-mode: both;
-moz-animation-fill-mode: both;
-ms-animation-fill-mode: both;
-o-animation-fill-mode: both;
animation-name: bouncedelay;
animation-duration: 1s;
animation-timing-function: ease-in-out;
animation-delay: 0;
animation-direction: normal;
animation-iteration-count: infinite;
animation-fill-mode: forwards;
animation-play-state: running;
-webkit-animation-name: bouncedelay;
-webkit-animation-duration: 1s;
-webkit-animation-timing-function: ease-in-out;
-webkit-animation-delay: 0;
-webkit-animation-direction: normal;
-webkit-animation-iteration-count: infinite;
-webkit-animation-fill-mode: forwards;
-webkit-animation-play-state: running;
-moz-animation-name: bouncedelay;
-moz-animation-duration: 1s;
-moz-animation-timing-function: ease-in-out;
-moz-animation-delay: 0;
-moz-animation-direction: normal;
-moz-animation-iteration-count: infinite;
-moz-animation-fill-mode: forwards;
-moz-animation-play-state: running;
display: inline-block;
height: 6px;
width: 6px;
background: #419eda;
border-radius: 100%;
display: inline-block; }
.co-m-loader-dot__one {
animation-delay: -0.32s;
-webkit-animation-delay: -0.32s;
-moz-animation-delay: -0.32s;
-ms-animation-delay: -0.32s;
-o-animation-delay: -0.32s; }
.co-m-loader-dot__two {
animation-delay: -0.16s;
-webkit-animation-delay: -0.16s;
-moz-animation-delay: -0.16s;
-ms-animation-delay: -0.16s;
-o-animation-delay: -0.16s; }
@-webkit-keyframes bouncedelay {
0%, 80%, 100% {
-webkit-transform: scale(0.25, 0.25);
-moz-transform: scale(0.25, 0.25);
-ms-transform: scale(0.25, 0.25);
-o-transform: scale(0.25, 0.25);
transform: scale(0.25, 0.25); }
40% {
-webkit-transform: scale(1, 1);
-moz-transform: scale(1, 1);
-ms-transform: scale(1, 1);
-o-transform: scale(1, 1);
transform: scale(1, 1); } }
@-moz-keyframes bouncedelay {
0%, 80%, 100% {
-webkit-transform: scale(0.25, 0.25);
-moz-transform: scale(0.25, 0.25);
-ms-transform: scale(0.25, 0.25);
-o-transform: scale(0.25, 0.25);
transform: scale(0.25, 0.25); }
40% {
-webkit-transform: scale(1, 1);
-moz-transform: scale(1, 1);
-ms-transform: scale(1, 1);
-o-transform: scale(1, 1);
transform: scale(1, 1); } }
@-ms-keyframes bouncedelay {
0%, 80%, 100% {
-webkit-transform: scale(0.25, 0.25);
-moz-transform: scale(0.25, 0.25);
-ms-transform: scale(0.25, 0.25);
-o-transform: scale(0.25, 0.25);
transform: scale(0.25, 0.25); }
40% {
-webkit-transform: scale(1, 1);
-moz-transform: scale(1, 1);
-ms-transform: scale(1, 1);
-o-transform: scale(1, 1);
transform: scale(1, 1); } }
@keyframes bouncedelay {
0%, 80%, 100% {
-webkit-transform: scale(0.25, 0.25);
-moz-transform: scale(0.25, 0.25);
-ms-transform: scale(0.25, 0.25);
-o-transform: scale(0.25, 0.25);
transform: scale(0.25, 0.25); }
40% {
-webkit-transform: scale(1, 1);
-moz-transform: scale(1, 1);
-ms-transform: scale(1, 1);
-o-transform: scale(1, 1);
transform: scale(1, 1); } }
.co-dialog .modal-body {
padding: 10px;
min-height: 100px;
}
.co-dialog .modal-body h4 {
margin-bottom: 20px;
}
.co-dialog .modal-content {
border-radius: 0px;
}
.co-dialog.fatal-error .modal-content {
padding-left: 175px;
}
.co-dialog.fatal-error .alert-icon-container-container {
position: absolute;
top: -36px;
left: -175px;
bottom: 20px;
}
.co-dialog.fatal-error .alert-icon-container {
height: 100%;
display: table;
}
.co-dialog.fatal-error .alert-icon {
display: table-cell;
vertical-align: middle;
border-right: 1px solid #eee;
margin-right: 20px;
}
.co-dialog.fatal-error .alert-icon:before {
content: "\f071";
font-family: FontAwesome;
font-size: 60px;
padding-left: 50px;
padding-right: 50px;
color: #c53c3f;
text-align: center;
}
.co-dialog .modal-header .cor-step-bar {
float: right;
}
.co-dialog .modal-footer.working {
text-align: left;
}
.co-dialog .modal-footer.working .btn {
float: right;
}
.co-dialog .modal-footer.working .cor-loader-inline {
margin-right: 10px;
}
.co-dialog .modal-footer .left-align {
float: left;
vertical-align: middle;
font-size: 16px;
margin-top: 8px;
}
.co-dialog .modal-footer .left-align i.fa-warning {
color: #ffba35;
display: inline-block;
margin-right: 6px;
}
.co-dialog .modal-footer .left-align i.fa-check {
color: green;
display: inline-block;
margin-right: 6px;
}
.co-step-bar .co-step-element {
cursor: default;
display: inline-block;
width: 28px;
height: 28px;
position: relative;
color: #ddd;
text-align: center;
line-height: 24px;
font-size: 16px;
}
.co-step-bar .co-step-element.text {
margin-left: 24px;
background: white;
}
.co-step-bar .co-step-element.icon {
margin-left: 22px;
}
.co-step-bar .co-step-element:first-child {
margin-left: 0px;
}
.co-step-bar .co-step-element.active {
color: #53a3d9;
}
.co-step-bar .co-step-element:first-child:before {
display: none;
}
.co-step-bar .co-step-element:before {
content: "";
position: absolute;
top: 12px;
width: 14px;
border-top: 2px solid #ddd;
}
.co-step-bar .co-step-element.icon:before {
left: -20px;
}
.co-step-bar .co-step-element.text:before {
left: -22px;
}
.co-step-bar .co-step-element.active:before {
border-top: 2px solid #53a3d9;
}
.co-step-bar .co-step-element.text {
border-radius: 100%;
border: 2px solid #ddd;
}
.co-step-bar .co-step-element.text.active {
border: 2px solid #53a3d9;
}
@media screen and (min-width: 900px) {
.co-dialog .modal-dialog {
width: 800px;
}
}
.co-alert .co-step-bar {
float: right;
margin-top: 6px;
}
.realtime-area-chart, .realtime-line-chart {
margin: 10px;
text-align: center;
}
.rickshaw_graph {
overflow: hidden;
padding-bottom: 40px;
}
.cor-container {
padding-left: 15px;
padding-right: 15px;
}

View file

@ -1,8 +1,12 @@
* {
font-family: 'Droid Sans', sans-serif;
font-family: 'Source Sans Pro', sans-serif;
margin: 0;
}
.btn {
outline: none !important;
}
@media (max-width: 410px) {
.olrk-normal {
display: none;
@ -38,7 +42,12 @@
}
#quay-logo {
height: 36px;
width: 100px;
background-repeat: no-repeat;
background-size: contain;
background-position: center;
display: inline-block;
}
#padding-container {
@ -83,34 +92,6 @@
margin: 0;
}
.co-img-bg-network {
background: url('/static/img/network-tile.png') left top repeat, linear-gradient(30deg, #2277ad, #144768) no-repeat left top fixed;
background-color: #2277ad;
background-size: auto, 100% 100%;
}
.co-m-navbar {
background-color: white;
margin: 0;
padding-left: 10px;
}
.co-fx-box-shadow {
-webkit-box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2);
-moz-box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2);
-ms-box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2);
-o-box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2);
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2);
}
.co-fx-box-shadow-heavy {
-webkit-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.4);
-moz-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.4);
-ms-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.4);
-o-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.4);
box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.4);
}
.main-panel {
margin-bottom: 20px;
background-color: #fff;
@ -182,7 +163,7 @@ nav.navbar-default .navbar-nav>li>a.active {
.notification-view-element .circle {
position: absolute;
top: 14px;
top: 15px;
left: 0px;
width: 12px;
@ -202,13 +183,13 @@ nav.navbar-default .navbar-nav>li>a.active {
margin-bottom: 4px;
}
.notification-view-element .container {
.notification-view-element .notification-content {
padding: 10px;
border-radius: 6px;
margin-left: 16px;
}
.notification-view-element .container:hover {
.notification-view-element .notification-content:hover {
background: rgba(66, 139, 202, 0.1);
}
@ -868,28 +849,24 @@ i.toggle-icon:hover {
background-color: #DFFF00;
}
.phase-icon.waiting, .phase-icon.unpacking, .phase-icon.starting, .phase-icon.initializing {
background-color: #ddd;
.phase-icon.waiting, .phase-icon.build-scheduled {
background-color: rgba(66, 139, 202, 0.2);
}
.phase-icon.pulling {
background-color: #cab442;
.phase-icon.unpacking, .phase-icon.starting, .phase-icon.initializing {
background-color: rgba(66, 139, 202, 0.4);
}
.phase-icon.building {
background-color: #f0ad4e;
.phase-icon.pulling, .phase-icon.priming-cache, .phase-icon.checking-cache {
background-color: rgba(66, 139, 202, 0.6);
}
.phase-icon.priming-cache {
background-color: #ddd;
}
.phase-icon.pushing {
background-color: #5cb85c;
.phase-icon.pushing, .phase-icon.building {
background-color: rgba(66, 139, 202, 0.8);
}
.phase-icon.complete {
background-color: #428bca;
background-color: rgba(66, 139, 202, 1);
}
.build-status {
@ -1092,12 +1069,6 @@ i.toggle-icon:hover {
border: 1px dashed #ccc;
}
.new-repo .initialize-repo .init-description {
color: #444;
font-size: 12px;
text-align: center;
}
.new-repo .initialize-repo .file-drop {
margin: 10px;
}
@ -1174,59 +1145,59 @@ i.toggle-icon:hover {
}
.visible-sm-inline {
display: none;
display: none !important;
}
.visible-md-inline {
display: none;
display: none !important;
}
.hidden-sm-inline {
display: inline;
display: inline !important;
}
.hidden-xs-inline {
display: inline;
display: inline !important;
}
@media (min-width: 991px) {
.visible-md-inline {
display: inline;
display: inline !important;
}
}
@media (max-width: 991px) and (min-width: 768px) {
.visible-sm-inline {
display: inline;
display: inline !important;
}
.hidden-sm-inline {
display: none;
display: none !important;
}
}
@media (max-width: 700px) {
.hidden-xs-inline {
display: none;
display: none !important;
}
}
.visible-xl {
display: none;
display: none !important;
}
.visible-xl-inline {
display: none;
display: none !important;
}
@media (min-width: 1200px) {
.visible-xl {
display: block;
display: block !important;
}
.visible-xl-inline {
display: inline-block;
display: inline-block !important;
}
}
@ -1343,13 +1314,16 @@ i.toggle-icon:hover {
position: relative;
}
.plan-price:after {
content: "/ mo";
position: absolute;
bottom: 0px;
right: 20px;
font-size: 12px;
color: #aaa;
@media (min-width: 768px) {
.plan-price:after {
content: "/ mo";
position: absolute;
bottom: 0px;
right: 20px;
font-size: 12px;
color: #aaa;
}
}
.plans-list .plan .count {
@ -1444,6 +1418,10 @@ i.toggle-icon:hover {
background: transparent;
}
.jumbotron p {
font-size: 100%;
}
.jumbotron .disclaimer-link {
font-size: .3em;
vertical-align: 23px;
@ -1512,11 +1490,10 @@ i.toggle-icon:hover {
right: 0px;
}
.landing-filter.signedin {
}
.landing-content {
z-index: 2;
padding-left: 20px;
padding-right: 20px;
}
.landing .call-to-action i.fa {
@ -1524,7 +1501,6 @@ i.toggle-icon:hover {
}
.landing .call-to-action {
height: 40px;
font-size: 18px;
padding-left: 14px;
padding-right: 14px;
@ -1663,7 +1639,7 @@ i.toggle-icon:hover {
padding-left: 70px;
}
.landing-page .twitter-tweet .avatar img {
.landing-page .twitter-tweet .twitter-avatar img {
border-radius: 4px;
border: 2px solid rgb(70, 70, 70);
width: 50px;
@ -2694,7 +2670,7 @@ p.editable:hover i {
}
.repo-build .build-pane .build-logs .log-container.command {
margin-left: 42px;
margin-left: 22px;
}
.repo-build .build-pane .build-logs .container-header.building {
@ -2942,10 +2918,8 @@ p.editable:hover i {
.navbar-brand {
padding: 6px;
}
.navbar-brand img {
height: 36px;
line-height: 1px;
font-size: 1px;
}
.user-dropdown > img {
@ -3622,6 +3596,22 @@ p.editable:hover i {
font-size: 16px;
}
.plans-table ul {
margin-top: 10px;
padding: 0px;
}
.plans-table ul li {
padding: 4px;
margin: 0px;
}
.plans-table ul li .plan-info {
padding: 4px;
}
.repo-breadcrumb-element .crumb {
cursor: pointer;
}
@ -3860,7 +3850,7 @@ p.editable:hover i {
text-align: center;
position: relative;
color: white;
left: -42px;
left: -38px;
top: -9px;
font-weight: bold;
font-size: .4em;
@ -3870,23 +3860,6 @@ p.editable:hover i {
margin-bottom: 40px;
}
.landing .social-alternate {
color: #777;
font-size: 2em;
margin-left: 43px;
line-height: 1em;
}
.landing .social-alternate .inner-text {
text-align: center;
position: relative;
color: white;
left: -43px;
top: -9px;
font-weight: bold;
font-size: .4em;
}
.contact-options {
margin-top: 60px;
}
@ -4505,14 +4478,28 @@ pre.command:before {
padding: 6px;
}
.user-row.super-user td {
background-color: #eeeeee;
.user-row {
border-bottom: 0px;
}
.user-row td {
vertical-align: middle;
}
.user-row .user-class {
text-transform: uppercase;
}
.user-row .labels {
float: right;
white-space: nowrap;
}
.user-row .labels .label {
text-transform: uppercase;
margin-right: 10px;
}
.form-change input {
margin-top: 12px;
margin-bottom: 12px;
@ -4975,3 +4962,90 @@ i.slack-icon {
#gen-token input[type="checkbox"] {
margin-right: 10px;
}
.system-log-download-panel {
padding: 20px;
text-align: center;
font-size: 18px;
}
.system-log-download-panel a {
margin-top: 20px;
}
.initial-setup-modal .quay-spinner {
vertical-align: middle;
margin-right: 10px;
display: inline-block;
}
.initial-setup-modal .valid-database p {
font-size: 18px;
}
.verified {
font-size: 16px;
margin-bottom: 16px;
}
.verified i.fa {
font-size: 26px;
margin-right: 10px;
vertical-align: middle;
color: rgb(53, 186, 53);
}
.registry-logo-preview {
border: 1px solid #eee;
vertical-align: middle;
padding: 4px;
max-width: 150px;
}
.modal-footer.alert {
text-align: left;
margin-bottom: -16px;
}
.dockerfile-build-form table td {
vertical-align: top;
white-space: nowrap;
}
.dockerfile-build-form input[type="file"] {
margin: 0px;
}
.dockerfile-build-form .help-text {
font-size: 13px;
color: #aaa;
margin-bottom: 20px;
padding-left: 22px;
}
.restart-required {
position: relative;
padding-left: 54px;
}
.restart-required button {
float: right;
margin-top: 4px;
}
.restart-required button i.fa {
margin-right: 6px;
}
.restart-required i.fa-warning {
position: absolute;
top: 24px;
left: 16px;
font-size: 28px;
}
.chart-col h4, .chart-col h5 {
display: block;
text-align: center;
}

View file

@ -1,7 +1,7 @@
<div class="application-manager-element">
<div class="quay-spinner" ng-show="loading"></div>
<div class="container" ng-show="!loading">
<div class="cor-container" ng-show="!loading">
<div class="side-controls">
<span class="popup-input-button" placeholder="'Application Name'" submitted="createApplication(value)">
<i class="fa fa-plus"></i> Create New Application

View file

@ -0,0 +1,5 @@
<div class="config-bool-field-element">
<form name="fieldform" novalidate>
<input type="checkbox" ng-model="binding">
</form>
</div>

View file

@ -0,0 +1,46 @@
<div class="config-contact-field-element">
<table>
<tr>
<td>
<div class="dropdown">
<button class="btn btn-default dropdown-toggle" type="button" data-toggle="dropdown">
<span ng-switch="kind">
<span ng-switch-when="mailto"><i class="fa fa-envelope"></i>E-mail</span>
<span ng-switch-when="irc"><i class="fa fa-comment"></i>IRC</span>
<span ng-switch-when="tel"><i class="fa fa-phone"></i>Phone</span>
<span ng-switch-default><i class="fa fa-ticket"></i>URL</span>
</span>
<span class="caret"></span>
</button>
<ul class="dropdown-menu" role="menu">
<li role="presentation">
<a role="menuitem" tabindex="-1" href="javascript:void(0)" ng-click="kind = 'mailto'">
<i class="fa fa-envelope"></i> E-mail
</a>
</li>
<li role="presentation">
<a role="menuitem" tabindex="-1" href="javascript:void(0)" ng-click="kind = 'irc'">
<i class="fa fa-comment"></i> IRC
</a>
</li>
<li role="presentation">
<a role="menuitem" tabindex="-1" href="javascript:void(0)" ng-click="kind = 'tel'">
<i class="fa fa-phone"></i> Telephone
</a>
</li>
<li role="presentation">
<a role="menuitem" tabindex="-1" href="javascript:void(0)" ng-click="kind = 'http'">
<i class="fa fa-ticket"></i> URL
</a>
</li>
</ul>
</div>
</td>
<td>
<form>
<input class="form-control" placeholder="{{ getPlaceholder(kind) }}" ng-model="value">
</form>
</td>
</tr>
</table>
</div>

View file

@ -0,0 +1,4 @@
<div class="config-contacts-field-element">
<div class="config-contact-field" binding="item.value" ng-repeat="item in items">
</div>
</div>

View file

@ -0,0 +1,10 @@
<div class="config-file-field-element">
<span ng-show="uploadProgress == null">
<span ng-if="hasFile"><code>{{ filename }}</code></span>
<span class="nofile" ng-if="!hasFile"><code>{{ filename }}</code> not found in mounted config directory: </span>
<input type="file" ng-file-select="onFileSelect($files)">
</span>
<span ng-show="uploadProgress != null">
Uploading file as <strong>{{ filename }}</strong>... {{ uploadProgress }}%
</span>
</div>

View file

@ -0,0 +1,16 @@
<div class="config-list-field-element">
<ul ng-show="binding && binding.length">
<li class="item" ng-repeat="item in binding">
<span class="item-title">{{ item }}</span>
<span class="item-delete">
<a href="javascript:void(0)" ng-click="removeItem(item)">Remove</a>
</span>
</li>
</ul>
<span class="empty" ng-if="!binding || binding.length == 0">No {{ itemTitle }}s defined</span>
<form class="form-control-container" ng-submit="addItem()">
<input type="text" class="form-control" placeholder="{{ placeholder }}"
ng-model="newItemName" style="display: inline-block">
<button class="btn btn-default" style="display: inline-block">Add</button>
</form>
</div>

View file

@ -0,0 +1,6 @@
<div class="config-numeric-field-element">
<form name="fieldform" novalidate>
<input type="number" class="form-control" placeholder="{{ placeholder || '' }}"
ng-model="bindinginternal" ng-trim="false" ng-minlength="1" required>
</form>
</div>

View file

@ -0,0 +1 @@
<div class="config-parsed-field-element"></div>

View file

@ -0,0 +1,625 @@
<div class="config-setup-tool-element">
<div class="cor-loader" ng-if="!config"></div>
<div ng-show="config && config['SUPER_USERS']">
<form id="configform" name="configform">
<!-- Basic Configuration -->
<div class="co-panel">
<div class="co-panel-heading">
<i class="fa fa-gears"></i> Basic Configuration
</div>
<div class="co-panel-body">
<table class="config-table">
<tr>
<td>Enterprise Logo URL:</td>
<td>
<span class="config-string-field" binding="config.ENTERPRISE_LOGO_URL"
placeholder="http://example.com/logo.png"></span>
<div class="help-text">
Enter the full URL to your company's logo.
</div>
</td>
<td>
<img class="registry-logo-preview" ng-src="{{ config.ENTERPRISE_LOGO_URL }}">
</td>
</tr>
<tr>
<td class="non-input">Contact Information:</td>
<td colspan="2">
<span class="config-contacts-field" binding="config.CONTACT_INFO"></span>
<div class="help-text" style="margin-top: 10px;">
Information to show in the Contact Page. If none specified, CoreOS contact information
is displayed.
</div>
</td>
</tr>
<tr>
<td>User Creation:</td>
<td colspan="2">
<div class="co-checkbox">
<input id="ftuc" type="checkbox" ng-model="config.FEATURE_USER_CREATION">
<label for="ftuc">Enable Open User Creation</label>
</div>
<div class="help-text">
If enabled, user accounts can be created by anyone.
Users can always be created in the users panel under this superuser view.
</div>
</td>
</tr>
</table>
</div>
</div>
<!-- Server Configuration -->
<div class="co-panel">
<div class="co-panel-heading">
<i class="fa fa-cloud"></i> Server Configuration
</div>
<div class="co-panel-body">
<table class="config-table">
<tr>
<td>Server Hostname:</td>
<td>
<span class="config-string-field" binding="config.SERVER_HOSTNAME"
placeholder="Hostname (and optional port if non-standard)"
pattern="{{ HOSTNAME_REGEX }}"></span>
<div class="help-text">
The HTTP host (and optionally the port number if a non-standard HTTP/HTTPS port) of the location
where the registry will be accessible on the network
</div>
</td>
</tr>
<tr>
<td>SSL:</td>
<td>
<div class="co-checkbox">
<input id="enable-ssl" type="checkbox" ng-model="config.PREFERRED_URL_SCHEME"
ng-true-value="https" ng-false-value="http">
<label for="enable-ssl">Enable SSL</label>
</div>
<div class="help-text" style="margin-bottom: 10px">
A valid SSL certificate and private key files are required to use this option.
</div>
<table class="config-table" ng-if="config.PREFERRED_URL_SCHEME == 'https'">
<tr>
<td class="non-input">Certificate:</td>
<td>
<span class="config-file-field" filename="ssl.cert"></span>
<div class="help-text">
The certificate must be in PEM format.
</div
</td>
</tr>
<tr>
<td class="non-input">Private key:</td>
<td>
<span class="config-file-field" filename="ssl.key"></span>
</td>
</tr>
</table>
</td>
</tr>
</table>
</div>
</div>
<!-- Redis -->
<div class="co-panel">
<div class="co-panel-heading">
<img src="/static/img/redis-small.png"> redis
</div>
<div class="co-panel-body">
<div class="description">
<p>A <a href="http://redis.io" target="_blank">redis</a> key-value store is required for real-time events and build logs.</p>
</div>
<table class="config-table">
<tr>
<td>Redis Hostname:</td>
<td>
<span class="config-string-field" binding="mapped.redis.host"
placeholder="The redis server hostname"
pattern="{{ HOSTNAME_REGEX }}"
validator="validateHostname(value)">></span>
</td>
</tr>
<tr>
<td>Redis port:</td>
<td>
<span class="config-numeric-field" binding="mapped.redis.port" default-value="6379"></span>
<div class="help-text">
Access to this port and hostname must be allowed from all hosts running
the enterprise registry
</div>
</td>
</tr>
<tr>
<td>Redis password:</td>
<td>
<input class="form-control" type="password" ng-model="mapped.redis.password"
placeholder="Optional password for connecting to redis">
</td>
</tr>
</table>
</div>
</div> <!-- /Redis -->
<!-- Registry Storage -->
<div class="co-panel">
<div class="co-panel-heading">
<i class="fa fa-download"></i> Registry Storage
</div>
<div class="co-panel-body">
<div class="description">
<p>
Registry images can be stored either locally or in a remote storage system.
<strong>A remote storage system is required for high-avaliability systems.</strong>
</p>
<table class="config-table">
<tr>
<td class="non-input">Storage Engine:</td>
<td>
<select ng-model="config.DISTRIBUTED_STORAGE_CONFIG.local[0]">
<option value="LocalStorage">Locally mounted directory</option>
<option value="S3Storage">Amazon S3</option>
<option value="GoogleCloudStorage">Google Cloud Storage</option>
<option value="RadosGWStorage">Ceph Object Gateway (RADOS)</option>
</select>
</td>
</tr>
<!-- Fields -->
<tr ng-repeat="field in STORAGE_CONFIG_FIELDS[config.DISTRIBUTED_STORAGE_CONFIG.local[0]]">
<td>{{ field.title }}:</td>
<td>
<span class="config-string-field"
binding="config.DISTRIBUTED_STORAGE_CONFIG.local[1][field.name]"
placeholder="{{ field.placeholder }}"
ng-if="field.kind == 'text'"></span>
<div class="co-checkbox" ng-if="field.kind == 'bool'">
<input id="dsc-{{ field.name }}" type="checkbox"
ng-model="config.DISTRIBUTED_STORAGE_CONFIG.local[1][field.name]">
<label for="dsc-{{ field.name }}">{{ field.placeholder }}</label>
</div>
<div class="help-text" ng-if="field.help_url">
See <a href="{{ field.help_url }}" target="_blank">Documentation</a> for more information
</div>
</td>
</tr>
</table>
</div>
</div>
</div>
<!-- E-mail -->
<div class="co-panel">
<div class="co-panel-heading">
<i class="fa fa-envelope"></i> E-mail
</div>
<div class="co-panel-body">
<div class="description">
<p>Valid e-mail server configuration is required for notification e-mails and the ability of
users to reset their passwords.</p>
</div>
<div class="co-checkbox">
<input id="ftmail" type="checkbox" ng-model="config.FEATURE_MAILING">
<label for="ftmail">Enable E-mails</label>
</div>
<table class="config-table" ng-if="config.FEATURE_MAILING">
<tr>
<td>SMTP Server:</td>
<td>
<span class="config-string-field" binding="config.MAIL_SERVER"
placeholder="SMTP server for sending e-mail"
pattern="{{ HOSTNAME_REGEX }}"
validator="validateHostname(value)">></span>
</td>
</tr>
<tr>
<td>SMTP Server Port:</td>
<td>
<span class="config-numeric-field" binding="config.MAIL_PORT"
default-value="587"></span>
</td>
</tr>
<tr>
<td>TLS:</td>
<td>
<div class="co-checkbox">
<input id="mut" type="checkbox" ng-model="config.MAIL_USE_TLS">
<label for="mut">Use TLS</label>
</div>
</td>
</tr>
<tr>
<td>Mail Sender:</td>
<td>
<input class="form-control" type="email" ng-model="config.DEFAULT_MAIL_SENDER"
placeholder="E-mail address"></span>
<div class="help-text">
E-mail address from which all e-mails are sent. If not specified,
<code>support@quay.io</code> will be used.
</div>
</td>
</tr>
<tr>
<td>Authentication:</td>
<td>
<div class="co-checkbox">
<input id="uma" type="checkbox" ng-model="config.MAIL_USE_AUTH">
<label for="uma">Requires Authentication</label>
</div>
<table class="config-table" ng-if="config.MAIL_USE_AUTH">
<tr>
<td>Username:</td>
<td>
<span class="config-string-field" binding="config.MAIL_USERNAME"
placeholder="Username for authentication"></span>
</td>
</tr>
<tr>
<td>Password:</td>
<td>
<input class="form-control" type="password"
ng-model="config.MAIL_PASSWORD"
placeholder="Password for authentication"></span>
</td>
</tr>
</table>
</td>
</tr>
</table>
</div>
</div> <!-- /E-mail -->
<!-- Authentication -->
<div class="co-panel">
<div class="co-panel-heading">
<i class="fa fa-users"></i> Authentication
</div>
<div class="co-panel-body">
<div class="description">
<p>
Authentication for the registry can be handled by either the registry itself or LDAP.
External authentication providers (such as GitHub) can be used on top of this choice.
</p>
</div>
<table class="config-table">
<tr>
<td class="non-input">Authentication:</td>
<td>
<select ng-model="config.AUTHENTICATION_TYPE">
<option value="Database">Local Database</option>
<option value="LDAP">LDAP</option>
</select>
</td>
</tr>
</table>
<table class="config-table" ng-if="config.AUTHENTICATION_TYPE == 'LDAP'">
<tr>
<td>LDAP URI:</td>
<td><span class="config-string-field" binding="config.LDAP_URI"></span></td>
</tr>
<tr>
<td>Administrator DN:</td>
<td><span class="config-string-field" binding="config.LDAP_ADMIN_DN"></span></td>
</tr>
<tr>
<td>Base DN:</td>
<td><span class="config-list-field" item-title="DN" binding="config.LDAP_BASE_DN"></span></td>
</tr>
<tr>
<td>Administrator Password:</td>
<td><span class="config-string-field" binding="config.LDAP_ADMIN_PASSWD"></span></td>
</tr>
<tr>
<td>E-mail Attribute:</td>
<td><span class="config-string-field" binding="config.LDAP_EMAIL_ATTR"></span></td>
</tr>
<tr>
<td>UID Attribute:</td>
<td><span class="config-string-field" binding="config.LDAP_UID_ATTR"></span></td>
</tr>
<tr>
<td>User RDN:</td>
<td><span class="config-list-field" item-title="RDN" binding="config.LDAP_USER_RDN"></span></td>
</tr>
</table>
</div>
</div> <!-- /Authentication -->
<!-- GitHub Authentication -->
<div class="co-panel">
<div class="co-panel-heading">
<i class="fa fa-github"></i> GitHub (Enterprise) Authentication
</div>
<div class="co-panel-body">
<div class="description">
<p>
If enabled, users can use GitHub or GitHub Enterprise to authenticate to the registry.
</p>
<p>
<strong>Note:</strong> A registered GitHub (Enterprise) OAuth application is required.
View instructions on how to
<a href="https://coreos.com/docs/enterprise-registry/github-app/" target="_blank">
Create an OAuth Application in GitHub
</a>
</p>
</div>
<div class="co-checkbox">
<input id="ftghl" type="checkbox" ng-model="config.FEATURE_GITHUB_LOGIN">
<label for="ftghl">Enable GitHub Authentication</label>
</div>
<table class="config-table" ng-if="config.FEATURE_GITHUB_LOGIN">
<tr>
<td>GitHub:</td>
<td>
<select ng-model="mapped.GITHUB_LOGIN_KIND">
<option value="hosted">GitHub.com</option>
<option value="enterprise">GitHub Enterprise</option>
</select>
</td>
</tr>
<tr ng-if="mapped.GITHUB_LOGIN_KIND == 'enterprise'">
<td>GitHub Endpoint:</td>
<td>
<span class="config-string-field"
binding="config.GITHUB_LOGIN_CONFIG.GITHUB_ENDPOINT"
placeholder="https://my.githubserver"
pattern="{{ GITHUB_REGEX }}">
</span>
<div class="help-text">
The GitHub Enterprise endpoint. Must start with http:// or https://.
</div>
</td>
</tr>
<tr>
<td>OAuth Client ID:</td>
<td>
<span class="config-string-field" binding="config.GITHUB_LOGIN_CONFIG.CLIENT_ID">
</span>
</td>
</tr>
<tr>
<td>OAuth Client Secret:</td>
<td>
<span class="config-string-field" binding="config.GITHUB_LOGIN_CONFIG.CLIENT_SECRET">
</span>
</td>
</tr>
</table>
</div>
</div> <!-- /GitHub Authentication -->
<!-- Google Authentication -->
<div class="co-panel">
<div class="co-panel-heading">
<i class="fa fa-google"></i> Google Authentication
</div>
<div class="co-panel-body">
<div class="description">
<p>
If enabled, users can use Google to authenticate to the registry.
</p>
<p>
<strong>Note:</strong> A registered Google OAuth application is required.
Visit the
<a href="https://console.developers.google.com" target="_blank">
Google Developer Console
</a>
to register an application.
</p>
</div>
<div class="co-checkbox">
<input id="ftgoa" type="checkbox" ng-model="config.FEATURE_GOOGLE_LOGIN">
<label for="ftgoa">Enable Google Authentication</label>
</div>
<table class="config-table" ng-if="config.FEATURE_GOOGLE_LOGIN">
<tr>
<td>OAuth Client ID:</td>
<td>
<span class="config-string-field" binding="config.GOOGLE_LOGIN_CONFIG.CLIENT_ID">
</span>
</td>
</tr>
<tr>
<td>OAuth Client Secret:</td>
<td>
<span class="config-string-field" binding="config.GOOGLE_LOGIN_CONFIG.CLIENT_SECRET">
</span>
</td>
</tr>
</table>
</div>
</div> <!-- /Google Authentication -->
<!-- Build Support -->
<div class="co-panel">
<div class="co-panel-heading">
<i class="fa fa-tasks"></i> Dockerfile Build Support
</div>
<div class="co-panel-body">
<div class="description">
If enabled, users can submit Dockerfiles to be built and pushed by the Enterprise Registry.
</div>
<div class="co-checkbox">
<input id="ftbs" type="checkbox" ng-model="config.FEATURE_BUILD_SUPPORT">
<label for="ftbs">Enable Dockerfile Build</label>
</div>
<div ng-if="config.FEATURE_BUILD_SUPPORT" style="margin-top: 10px">
<strong>Note: Build workers are required for this feature.</strong>
See <a href="https://coreos.com/docs/enterprise-registry/build-support/" target="_blank">Adding Build Workers</a> for instructions on how to setup build workers.
</div>
</div>
</div> <!-- /Build Support -->
<!-- GitHub Trigger -->
<div class="co-panel" ng-if="config.FEATURE_BUILD_SUPPORT" style="margin-top: 20px;">
<div class="co-panel-heading">
<i class="fa fa-github"></i> GitHub (Enterprise) Build Triggers
</div>
<div class="co-panel-body">
<div class="description">
<p>
If enabled, users can setup GitHub or GitHub Enterprise triggers to invoke Registry builds.
</p>
<p>
<strong>Note:</strong> A registered GitHub (Enterprise) OAuth application (<strong>separate from GitHub Authentication</strong>) is required.
View instructions on how to
<a href="https://coreos.com/docs/enterprise-registry/github-app/" target="_blank">
Create an OAuth Application in GitHub
</a>
</p>
</div>
<div class="co-checkbox">
<input id="ftgb" type="checkbox" ng-model="config.FEATURE_GITHUB_BUILD">
<label for="ftgb">Enable GitHub Triggers</label>
</div>
<table class="config-table" ng-if="config.FEATURE_GITHUB_BUILD">
<tr>
<td>GitHub:</td>
<td>
<select ng-model="mapped.GITHUB_TRIGGER_KIND">
<option value="hosted">GitHub.com</option>
<option value="enterprise">GitHub Enterprise</option>
</select>
</td>
</tr>
<tr ng-if="mapped.GITHUB_TRIGGER_KIND == 'enterprise'">
<td>GitHub Endpoint:</td>
<td>
<span class="config-string-field"
binding="config.GITHUB_TRIGGER_CONFIG.GITHUB_ENDPOINT"
placeholder="https://my.githubserver"
pattern="{{ GITHUB_REGEX }}">
</span>
<div class="help-text">
The GitHub Enterprise endpoint. Must start with http:// or https://.
</div>
</td>
</tr>
<tr>
<td>OAuth Client ID:</td>
<td>
<span class="config-string-field" binding="config.GITHUB_TRIGGER_CONFIG.CLIENT_ID">
</span>
</td>
</tr>
<tr>
<td>OAuth Client Secret:</td>
<td>
<span class="config-string-field" binding="config.GITHUB_TRIGGER_CONFIG.CLIENT_SECRET">
</span>
</td>
</tr>
</table>
</div>
</div> <!-- /GitHub Trigger -->
</form>
<!-- Save Bar -->
<div class="cor-floating-bottom-bar">
<button class="btn" ng-class="mapped.$hasChanges ? 'btn-primary' : 'btn-success'"
ng-click="checkValidateAndSave()" ng-show="configform.$valid">
<i class="fa fa-lg" ng-class="mapped.$hasChanges ? 'fa-dot-circle-o' : 'fa-check-circle'"></i>
<span ng-if="mapped.$hasChanges">Save Configuration Changes</span>
<span ng-if="!mapped.$hasChanges">Configuration Saved</span>
</button>
<button class="btn btn-warning" ng-click="checkValidateAndSave()" ng-show="!configform.$valid"
ng-click="checkValidateAndSave()">
<i class="fa fa-lg fa-sort"></i>
{{ configform.$error['required'].length }} configuration field<span ng-show="configform.$error['required'].length != 1">s</span> remaining
</button>
</div>
<!-- Modal message dialog -->
<div class="modal co-dialog fade initial-setup-modal" id="validateAndSaveModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">
Checking your settings
</h4>
</div>
<div class="modal-body">
<div class="service-verification">
<div class="service-verification-row" ng-repeat="serviceInfo in validating">
<span class="quay-spinner" ng-show="serviceInfo.status == 'validating'"></span>
<i class="fa fa-lg fa-check-circle" ng-show="serviceInfo.status == 'success'"></i>
<i class="fa fa-lg fa-warning" ng-show="serviceInfo.status == 'error'"></i>
<span class="service-title">{{ serviceInfo.service.title }}</span>
<div class="service-verification-error" ng-show="serviceInfo.status == 'error'">{{ serviceInfo.errorMessage }}</div>
</div>
</div>
</div>
<!-- Footer: Saving configuration -->
<div class="modal-footer working" ng-show="savingConfiguration">
<span class="cor-loader-inline"></span> Saving Configuration...
</div>
<!-- Footer: Validating -->
<div class="modal-footer working"
ng-show="!savingConfiguration && validationStatus(validating) == 'validating'">
<span class="cor-loader-inline"></span> Validating settings...
<button class="btn btn-default" ng-click="cancelValidation()">
Stop Validating
</button>
</div>
<!-- Footer: Valid Config -->
<div class="modal-footer"
ng-show="!savingConfiguration && validationStatus(validating) == 'success'">
<span class="left-align">
<i class="fa fa-check"></i>
Configuration Validated
</span>
<button class="btn btn-primary"
ng-click="saveConfiguration()"
ng-disabled="savingConfiguration">
<i class="fa fa-upload" style="margin-right: 10px;"></i>Save Configuration
</button>
</div>
<!-- Footer: Invalid Config -->
<div class="modal-footer"
ng-show="!savingConfiguration && validationStatus(validating) == 'failed'">
<span class="left-align">
<i class="fa fa-warning"></i>
Problem Detected
</span>
<button class="btn btn-default" data-dismiss="modal">
Continue Editing
</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
</div>
</div>

View file

@ -0,0 +1,10 @@
<div class="config-string-field-element">
<form name="fieldform" novalidate>
<input type="text" class="form-control" placeholder="{{ placeholder || '' }}"
ng-model="binding" ng-trim="false" ng-minlength="1"
ng-pattern="getRegexp(pattern)" required>
<div class="alert alert-danger" ng-show="errorMessage">
{{ errorMessage }}
</div>
</form>
</div>

View file

@ -0,0 +1,10 @@
<div class="config-variable-field-element">
<div class="btn-group">
<button type="button" class="btn btn-default"
ng-repeat="section in sections"
ng-click="setSection(section)"
ng-class="section == currentSection ? 'active' : ''">{{ section.title }}</button>
</div>
<span ng-transclude></span>
</div>

View file

@ -0,0 +1,3 @@
<div class="co-floating-bottom-bar">
<span ng-transclude/>
</div>

View file

@ -0,0 +1,5 @@
<div class="co-m-inline-loader co-an-fade-in-out">
<div class="co-m-loader-dot__one"></div>
<div class="co-m-loader-dot__two"></div>
<div class="co-m-loader-dot__three"></div>
</div>

View file

@ -0,0 +1,5 @@
<div class="co-m-loader co-an-fade-in-out">
<div class="co-m-loader-dot__one"></div>
<div class="co-m-loader-dot__two"></div>
<div class="co-m-loader-dot__three"></div>
</div>

View file

@ -0,0 +1,11 @@
<div class="co-log-box-element">
<div id="co-log-viewer" class="co-log-viewer" ng-if="logs">
<div class="quay-spinner" ng-if="!logs"></div>
<div class="co-log-container">
<div id="co-log-content" class="co-log-content">{{ logs }}</div>
</div>
</div>
<div class="co-log-viewer-new-logs" ng-show="hasNewLogs" ng-click="moveToBottom()">
New Logs <i class="fa fa-lg fa-arrow-circle-down"></i>
</div>
</div>

View file

@ -0,0 +1,3 @@
<li>
<a href="javascript:void(0)" ng-click="optionClick()" ng-transclude></a>
</li>

View file

@ -0,0 +1,6 @@
<span class="co-options-menu">
<div class="dropdown" style="text-align: left;">
<i class="fa fa-gear fa-lg dropdown-toggle" data-toggle="dropdown" data-title="Options" bs-tooltip></i>
<ul class="dropdown-menu pull-right" ng-transclude></ul>
</div>
</span>

View file

@ -0,0 +1,3 @@
<div class="co-step-bar">
<span class="transclude" ng-transclude/>
</div>

View file

@ -0,0 +1,6 @@
<span ng-class="text ? 'co-step-element text' : 'co-step-element icon'">
<span data-title="{{ title }}" bs-tooltip>
<span class="text" ng-if="text">{{ text }}</span>
<i class="fa fa-lg" ng-if="icon" ng-class="'fa-' + icon"></i>
</span>
</span>

View file

@ -0,0 +1 @@
<div class="co-tab-content tab-content col-md-11" ng-transclude></div>

View file

@ -0,0 +1,3 @@
<div class="co-main-content-panel co-tab-panel co-fx-box-shadow-heavy">
<div class="co-tab-container" ng-transclude></div>
</div>

View file

@ -0,0 +1,11 @@
<li ng-class="tabActive == 'true' ? 'active' : ''">
<a href="javascript:void(0)" data-title="{{ tabTitle }}"
data-toggle="tab"
data-target="{{ tabTarget }}"
data-placement="right"
data-container="body"
ng-click="tabInit()"
bs-tooltip>
<span ng-transclude/>
</a>
</li>

View file

@ -0,0 +1 @@
<ul class="co-tabs col-md-1" ng-transclude></ul>

View file

@ -0,0 +1,3 @@
<div class="col-lg-6 col-md-6 col-sm-6 col-xs-12">
<h2 class="co-nav-title-content co-fx-text-shadow" ng-transclude></h2>
</div>

View file

@ -0,0 +1 @@
<div class="col-lg-3 col-md-3 col-sm-3 col-xs-6" ng-transclude></div>

View file

@ -0,0 +1,2 @@
<div class="co-nav-title" ng-transclude></div>

View file

@ -73,7 +73,7 @@
<tr ng-if="currentMethod.fields.length"><td colspan="2"><hr></td></tr>
<tr ng-repeat="field in currentMethod.fields">
<td valign="top">{{ field.title }}:</td>
<td valign="top" style="padding-top: 10px">{{ field.title }}:</td>
<td>
<div ng-switch on="field.type">
<span ng-switch-when="email">
@ -81,6 +81,9 @@
</span>
<input type="url" class="form-control" ng-model="currentConfig[field.name]" ng-switch-when="url" required>
<input type="text" class="form-control" ng-model="currentConfig[field.name]" ng-switch-when="string" required>
<input type="text" class="form-control" ng-model="currentConfig[field.name]" ng-switch-when="regex" required
ng-pattern="getPattern(field)"
placeholder="{{ field.placeholder }}">
<div class="entity-search" namespace="repository.namespace"
placeholder="''"
current-entity="currentConfig[field.name]"

View file

@ -15,7 +15,7 @@
</div>
<div class="dockerfile-build-form" repository="repository" upload-failed="handleBuildFailed(message)"
build-started="handleBuildStarted(build)" build-failed="handleBuildFailed(message)" start-now="startCounter"
has-dockerfile="hasDockerfile" uploading="uploading" building="building"></div>
is-ready="hasDockerfile" uploading="uploading" building="building"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" ng-click="startBuild()" ng-disabled="building || uploading || !hasDockerfile">Start Build</button>

View file

@ -1,8 +1,8 @@
<div class="dockerfile-build-form-element">
<div class="container" ng-show="building">
<div ng-show="building">
<div class="quay-spinner"></div>
</div>
<div class="container" ng-show="uploading">
<div ng-show="uploading">
<span class="message">Uploading file {{ upload_file }}</span>
<div class="progress progress-striped active">
<div class="progress-bar" role="progressbar" aria-valuenow="{{ upload_progress }}" aria-valuemin="0" aria-valuemax="100" style="{{ 'width: ' + upload_progress + '%' }}">
@ -10,10 +10,45 @@
</div>
</div>
<div class="container" ng-show="!uploading && !building">
<div class="init-description">
Upload a <b>Dockerfile</b> or an archive (<code>.zip</code> or <code>.tar.gz</code>) containing a Dockerfile <b>in the root directory</b>
</div>
<input id="file-drop" class="file-drop" type="file" file-present="internal.hasDockerfile">
<div ng-show="!uploading && !building">
<table>
<tr>
<td style="vertical-align: middle;">Dockerfile or <code>.tar.gz</code> or <code>.zip</code>:</td>
<td><input id="file-drop" class="file-drop" type="file" file-present="internal.hasDockerfile">
</tr>
<tr>
<td></td>
<td>
<div class="help-text">If an archive, the Dockerfile must be at the root</div>
</td>
</tr>
<tr>
<td>Base Image Pull Credentials:</td>
<td>
<!-- Select credentials -->
<div class="btn-group btn-group-sm">
<button type="button" class="btn btn-default"
ng-class="is_public ? 'active btn-info' : ''"
ng-click="is_public = true">
None
</button>
<button type="button" class="btn btn-default"
ng-class="is_public ? '' : 'active btn-info'"
ng-click="is_public = false">
<i class="fa fa-wrench"></i>
Robot account
</button>
</div>
<!-- Robot Select -->
<div ng-show="!is_public" style="margin-top: 10px">
<div class="entity-search" namespace="repository.namespace"
placeholder="'Select robot account for pulling...'"
current-entity="pull_entity"
allowed-entities="['robot']"></div>
</div>
</td>
</tr>
</table>
</div>
</div>

View file

@ -4,7 +4,7 @@
&equiv;
</button>
<a class="navbar-brand" href="/" target="{{ appLinkTarget() }}">
<img id="quay-logo" src="/static/img/quay-logo.png">
<span id="quay-logo" ng-style="{'background-image': 'url(' + getEnterpriseLogo() + ')'}"></span>
</a>
</div>
@ -19,8 +19,21 @@
<li><a ng-href="{{ user.organizations.length ? '/organizations/' : '/tour/organizations/' }}" target="{{ appLinkTarget() }}" quay-section="organization">Organizations</a></li>
</ul>
<!-- Phone -->
<ul class="nav navbar-nav navbar-right visible-xs" ng-switch on="user.anonymous">
<li ng-switch-when="false">
<a href="/user/" class="user-view" target="{{ appLinkTarget() }}">
<span class="avatar" size="32" hash="user.avatar"></span>
{{ user.username }}
</a>
</li>
<li ng-switch-default>
<a class="user-view" href="/signin/" target="{{ appLinkTarget() }}">Sign in</a>
</li>
</ul>
<ul class="nav navbar-nav navbar-right" ng-switch on="user.anonymous">
<!-- Normal -->
<ul class="nav navbar-nav navbar-right hidden-xs" ng-switch on="user.anonymous">
<li>
<form class="navbar-form navbar-left" role="search">
<div class="form-group">

View file

@ -1,4 +1,4 @@
<div class="container loading-status-element">
<div class="loading-status-element">
<div ng-show="hasError && !loading">
<span ng-transclude></span>
</div>

View file

@ -1,6 +1,6 @@
<div class="logs-view-element">
<div class="container header">
<div class="header">
<span class="header-text">
<span ng-show="!performer">Usage Logs</span>
<span class="entity-reference" entity="performer" ng-show="performer"></span>
@ -20,9 +20,7 @@
</span>
</div>
<div ng-show="loading">
<div class="quay-spinner 3x"></div>
</div>
<div class="cor-loader-inline" ng-show="loading"></div>
<div ng-show="!loading">
<div id="bar-chart" style="width: 800px; height: 500px;" ng-show="chartVisible">
<svg style="width: 800px; height: 500px;"></svg>
@ -56,7 +54,8 @@
<td>
<span class="log-performer" ng-if="log.metadata.oauth_token_application">
<div>
<span class="application-reference" data-title="log.metadata.oauth_token_application"
<span class="application-reference"
data-title="log.metadata.oauth_token_application"
client-id="log.metadata.oauth_token_application_id"></span>
</div>
<div style="text-align: center; font-size: 12px; color: #aaa; padding: 4px;">on behalf of</div>

View file

@ -1,5 +1,5 @@
<div class="notification-view-element">
<div class="container" ng-click="showNotification();">
<div class="notification-content" ng-click="showNotification();">
<div class="circle" ng-class="getClass(notification)"></div>
<div class="message" ng-bind-html="getMessage(notification)"></div>
<div class="orginfo" ng-if="notification.organization">

View file

@ -31,11 +31,16 @@
ng-show="!planLoading"></div>
<!-- Plans Table -->
<div class="visible-xs" style="margin-top: 10px"></div>
<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>
<span class="hidden-xs">Private Repositories</span>
<span class="visible-xs"><i class="fa fa-hdd-o"></i></span>
</td>
<td style="min-width: 64px"><span class="hidden-xs">Price</span><span class="visible-xs">$/mo</span></td>
<td></td>
</thead>

View file

@ -1,23 +1,41 @@
<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>
<ul class="plans-table-list visible-xs">
<li ng-repeat="plan in plans" ng-class="currentPlan == plan ? 'active' : ''">
<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' }}
{{ plan.title }}
</a>
</td>
</tr>
</table>
<div class="plan-info">
${{ plan.price / 100 }} / month -
{{ plan.privateRepos }} repositories
</div>
</li>
</ul>
<div class="hidden-xs">
<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>
</div>

View file

@ -0,0 +1,75 @@
<div class="ps-usage-graph-element">
<!-- Build Charts -->
<div quay-show="Features.BUILD_SUPPORT">
<div class="alert alert-warning" ng-if="data.build && data.build.job_total == null">
Cannot load build system status. Please restart your container.
</div>
<div ng-if="data.build && data.build.job_total >= 0">
<div class="col-md-6 chart-col">
<h4>Build Queue</h4>
<h5>
Running Jobs: {{ data.build.running_total }} | Total Jobs: {{ data.build.job_total }}
</h5>
<div class="realtime-area-chart"
data="[data.build.job_total, data.build.running_total]"
labels="['Queued Build Jobs', 'Running Build Jobs']"
colors="['rgb(157, 194, 211)', 'rgb(56, 122, 163)']"
counter="counter"
minimum="-10"
maximum="auto"></div>
</div>
<div class="col-md-6 chart-col">
<h4>Local Build Workers</h4>
<h5>
Local Workers: {{ data.build.workers }} | Working: {{ data.build.running_local }}
</h5>
<div class="realtime-area-chart"
data="[data.build.job_total, data.build.workers, data.build.running_local]"
labels="['Queued Build Jobs', 'Build Workers (local)', 'Running Build Jobs (local)']"
colors="['rgb(157, 194, 211)', 'rgb(161, 208, 93)', 'rgb(210, 237, 130)']"
counter="counter"
minimum="-10"
maximum="auto"></div>
</div>
</div>
</div>
<!-- CPU, Memory and Network -->
<div class="col-md-4 chart-col">
<h4>CPU Usage %</h4>
<div class="realtime-line-chart" data="data.count.cpu" counter="counter"
label-template="CPU #{x} %"
minimum="-10" maximum="110"></div>
</div>
<div class="col-md-4 chart-col">
<h4>Process Count</h4>
<div class="realtime-line-chart" data="data.count.processes" counter="counter"
label-template="Process Count"></div>
</div>
<div class="col-md-4 chart-col">
<h4>Virtual Memory %</h4>
<div class="realtime-line-chart" data="data.count.virtual_mem[2]" counter="counter"
label-template="Virtual Memory %"
minimum="-10" maximum="110"></div>
</div>
<div class="col-md-4 chart-col">
<h4>Swap Memory</h4>
<div class="realtime-line-chart" data="data.count.swap_mem[3]" counter="counter"
label-template="Swap Memory %"></div>
</div>
<div class="col-md-4 chart-col">
<h4>Network Connections</h4>
<div class="realtime-line-chart" data="data.count.connections" counter="counter"
label-template="Network Connection Count"></div>
</div>
<div class="col-md-4 chart-col">
<h4>Network Usage (Bytes)</h4>
<div class="realtime-line-chart" data="data.count.network" labels="['Bytes In', 'Bytes Out']" counter="counter"></div>
</div>
</div>

View file

@ -0,0 +1,6 @@
<div class="realtime-area-chart-element">
<div ng-show="counter >= 1">
<div class="chart"></div>
</div>
<div class="cor-loader-inline" ng-if="counter < 1"></div>
</div>

View file

@ -0,0 +1,6 @@
<div class="realtime-line-chart-element">
<div ng-show="counter >= 1">
<div class="chart"></div>
</div>
<div class="cor-loader-inline" ng-if="counter < 1"></div>
</div>

View file

@ -2,7 +2,7 @@
<div class="quay-spinner" ng-show="loading"></div>
<div class="alert alert-info">Robot accounts allow for delegating access in multiple repositories to role-based accounts that you manage</div>
<div class="container" ng-show="!loading">
<div ng-show="!loading">
<div class="side-controls">
<span class="popup-input-button" pattern="ROBOT_PATTERN" placeholder="'Robot Account Name'"
submitted="createRobot(value)">

View file

@ -1,6 +1,6 @@
<div class="team-view-add-element" focusable-popover-content>
<div class="entity-search"
namespace="orgname" placeholder="'Add a registered user or robot...'"
namespace="orgname" placeholder="allowEmail ? 'Add a registered user, robot or email address...' : 'Add a registered user or robot...'"
entity-selected="addNewMember(entity)"
email-selected="inviteEmail(email)"
current-entity="selectedMember"
@ -12,6 +12,7 @@
ng-show="!addingMember"></div>
<div class="quay-spinner" ng-show="addingMember"></div>
<div class="help-text" ng-show="!addingMember">
Search by Quay.io username or robot account name
<span ng-if="allowEmail">Search by registry username, robot account name or enter an email address to invite</span>
<span ng-if="!allowEmail">Search by registry username or robot account name</span>
</div>
</div>

View file

@ -4,7 +4,7 @@
</p>
<div class="attribute">
<span class="info-wrap">
<span class="avatar"><img ng-src="{{ avatarUrl }}" fallback-src="/static/img/default-twitter.png"></span>
<span class="twitter-avatar"><img ng-src="{{ avatarUrl }}" fallback-src="/static/img/default-twitter.png"></span>
<span class="info">
<span class="author">{{ authorName }} (@{{authorUser}})</span>
<a class="reference" ng-href="{{ messageUrl }}">{{ messageDate }}</a>

BIN
static/img/redis-small.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View file

@ -129,7 +129,7 @@ function getMarkedDown(string) {
quayDependencies = ['ngRoute', 'chieffancypants.loadingBar', 'angular-tour', 'restangular', 'angularMoment',
'mgcrea.ngStrap', 'ngCookies', 'ngSanitize', 'angular-md5', 'pasvaz.bindonce', 'ansiToHtml',
'ngAnimate'];
'ngAnimate', 'core-ui', 'core-config-setup'];
if (window.__config && window.__config.MIXPANEL_KEY) {
quayDependencies.push('angulartics');
@ -980,7 +980,7 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
return resource;
};
var buildUrl = function(path, parameters) {
var buildUrl = function(path, parameters, opt_forcessl) {
// We already have /api/v1/ on the URLs, so remove them from the paths.
path = path.substr('/api/v1/'.length, path.length);
@ -1020,6 +1020,11 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
}
}
// If we are forcing SSL, return an absolutel URL with an SSL prefix.
if (opt_forcessl) {
path = 'https://' + window.location.host + '/api/v1/' + path;
}
return url;
};
@ -1050,12 +1055,35 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
}
};
var freshLoginInProgress = [];
var reject = function(msg) {
for (var i = 0; i < freshLoginInProgress.length; ++i) {
freshLoginInProgress[i].deferred.reject({'data': {'message': msg}});
}
freshLoginInProgress = [];
};
var retry = function() {
for (var i = 0; i < freshLoginInProgress.length; ++i) {
freshLoginInProgress[i].retry();
}
freshLoginInProgress = [];
};
var freshLoginFailCheck = function(opName, opArgs) {
return function(resp) {
var deferred = $q.defer();
// If the error is a fresh login required, show the dialog.
if (resp.status == 401 && resp.data['error_type'] == 'fresh_login_required') {
var retryOperation = function() {
apiService[opName].apply(apiService, opArgs).then(function(resp) {
deferred.resolve(resp);
}, function(resp) {
deferred.reject(resp);
});
};
var verifyNow = function() {
var info = {
'password': $('#freshPassword').val()
@ -1065,19 +1093,27 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
// Conduct the sign in of the user.
apiService.verifyUser(info).then(function() {
// On success, retry the operation. if it succeeds, then resolve the
// On success, retry the operations. if it succeeds, then resolve the
// deferred promise with the result. Otherwise, reject the same.
apiService[opName].apply(apiService, opArgs).then(function(resp) {
deferred.resolve(resp);
}, function(resp) {
deferred.reject(resp);
});
retry();
}, function(resp) {
// Reject with the sign in error.
deferred.reject({'data': {'message': 'Invalid verification credentials'}});
reject('Invalid verification credentials');
});
};
// Add the retry call to the in progress list. If there is more than a single
// in progress call, we skip showing the dialog (since it has already been
// shown).
freshLoginInProgress.push({
'deferred': deferred,
'retry': retryOperation
})
if (freshLoginInProgress.length > 1) {
return deferred.promise;
}
var box = bootbox.dialog({
"message": 'It has been more than a few minutes since you last logged in, ' +
'so please verify your password to perform this sensitive operation:' +
@ -1095,7 +1131,7 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
"label": "Cancel",
"className": "btn-default",
"callback": function() {
deferred.reject({'data': {'message': 'Verification canceled'}});
reject('Verification canceled')
}
}
}
@ -1127,8 +1163,8 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
var path = resource['path'];
// Add the operation itself.
apiService[operationName] = function(opt_options, opt_parameters, opt_background) {
var one = Restangular.one(buildUrl(path, opt_parameters));
apiService[operationName] = function(opt_options, opt_parameters, opt_background, opt_forcessl) {
var one = Restangular.one(buildUrl(path, opt_parameters, opt_forcessl));
if (opt_background) {
one.withHttpConfig({
'ignoreLoadingBar': true
@ -1247,6 +1283,39 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
return cookieService;
}]);
$provide.factory('ContainerService', ['ApiService', '$timeout',
function(ApiService, $timeout) {
var containerService = {};
containerService.restartContainer = function(callback) {
ApiService.scShutdownContainer(null, null).then(function(resp) {
$timeout(callback, 2000);
}, ApiService.errorDisplay('Cannot restart container. Please report this to support.'))
};
containerService.scheduleStatusCheck = function(callback) {
$timeout(function() {
containerService.checkStatus(callback);
}, 2000);
};
containerService.checkStatus = function(callback, force_ssl) {
var errorHandler = function(resp) {
if (resp.status == 404 || resp.status == 502) {
// Container has not yet come back up, so we schedule another check.
containerService.scheduleStatusCheck(callback);
return;
}
return ApiService.errorDisplay('Cannot load status. Please report this to support')(resp);
};
ApiService.scRegistryStatus(null, null)
.then(callback, errorHandler, /* background */true, /* force ssl*/force_ssl);
};
return containerService;
}]);
$provide.factory('UserService', ['ApiService', 'CookieService', '$rootScope', 'Config',
function(ApiService, CookieService, $rootScope, Config) {
var userResponse = {
@ -1487,15 +1556,12 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
'icon': 'slack-icon',
'fields': [
{
'name': 'subdomain',
'type': 'string',
'title': 'Slack Subdomain'
},
{
'name': 'token',
'type': 'string',
'title': 'Token',
'help_url': 'https://{subdomain}.slack.com/services/new/incoming-webhook'
'name': 'url',
'type': 'regex',
'title': 'Webhook URL',
'regex': '^https://hooks\\.slack\\.com/services/[A-Z0-9]+/[A-Z0-9]+/[a-zA-Z0-9]+$',
'help_url': 'https://slack.com/services/new/incoming-webhook',
'placeholder': 'https://hooks.slack.com/service/{some}/{token}/{here}'
}
]
}
@ -2231,8 +2297,10 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
templateUrl: '/static/partials/repo-list.html', controller: RepoListCtrl, reloadOnSearch: false}).
when('/user/', {title: 'Account Settings', description:'Account settings for ' + title, templateUrl: '/static/partials/user-admin.html',
reloadOnSearch: false, controller: UserAdminCtrl}).
when('/superuser/', {title: 'Superuser Admin Panel', description:'Admin panel for ' + title, templateUrl: '/static/partials/super-user.html',
reloadOnSearch: false, controller: SuperUserAdminCtrl}).
when('/superuser/', {title: 'Enterprise Registry Management', description:'Admin panel for ' + title, templateUrl: '/static/partials/super-user.html',
reloadOnSearch: false, controller: SuperUserAdminCtrl, newLayout: true}).
when('/setup/', {title: 'Enterprise Registry Setup', description:'Setup for ' + title, templateUrl: '/static/partials/setup.html',
reloadOnSearch: false, controller: SetupCtrl, newLayout: true}).
when('/guide/', {title: 'Guide', description:'Guide to using private docker repositories on ' + title,
templateUrl: '/static/partials/guide.html',
controller: GuideCtrl}).
@ -2652,7 +2720,7 @@ quayApp.directive('focusablePopoverContent', ['$timeout', '$popover', function (
if (!scope) { return; }
scope.$apply(function() {
if (!scope) { return; }
if (!scope || !$scope.$hide) { return; }
scope.$hide();
});
};
@ -3250,7 +3318,11 @@ quayApp.directive('logsView', function () {
}
if (metadata.token) {
prefix += ' via token {token}';
if (metadata.token_type == 'build-worker') {
prefix += ' by <b>build worker</b>';
} else {
prefix += ' via token';
}
} else if (metadata.username) {
prefix += ' by {username}';
} else {
@ -3261,7 +3333,13 @@ quayApp.directive('logsView', function () {
},
'pull_repo': function(metadata) {
if (metadata.token) {
return 'Pull repository {repo} via token {token}';
var prefix = 'Pull of repository'
if (metadata.token_type == 'build-worker') {
prefix += ' by <b>build worker</b>';
} else {
prefix += ' via token';
}
return prefix;
} else if (metadata.username) {
return 'Pull repository {repo} by {username}';
} else {
@ -3915,9 +3993,11 @@ quayApp.directive('registryName', function () {
replace: false,
transclude: true,
restrict: 'C',
scope: {},
scope: {
'isShort': '=isShort'
},
controller: function($scope, $element, Config) {
$scope.name = Config.REGISTRY_TITLE;
$scope.name = $scope.isShort ? Config.REGISTRY_TITLE_SHORT : Config.REGISTRY_TITLE;
}
};
return directiveDefinitionObject;
@ -4076,7 +4156,7 @@ quayApp.directive('headerBar', function () {
restrict: 'C',
scope: {
},
controller: function($scope, $element, $location, UserService, PlanService, ApiService, NotificationService) {
controller: function($scope, $element, $location, UserService, PlanService, ApiService, NotificationService, Config) {
$scope.notificationService = NotificationService;
// Monitor any user changes and place the current user into the scope.
@ -4095,6 +4175,14 @@ quayApp.directive('headerBar', function () {
}
return "";
};
$scope.getEnterpriseLogo = function() {
if (!Config.ENTERPRISE_LOGO_URL) {
return '/static/img/quay-logo.png';
}
return Config.ENTERPRISE_LOGO_URL;
};
}
};
return directiveDefinitionObject;
@ -4353,6 +4441,8 @@ quayApp.directive('entitySearch', function () {
if (classes.length > 1) {
classes[classes.length - 1] = 'or ' + classes[classes.length - 1];
} else if (classes.length == 0) {
return '<div class="tt-empty">No matching entities found</div>';
}
var class_string = '';
@ -4438,7 +4528,6 @@ quayApp.directive('entitySearch', function () {
$scope.$watch('namespace', function(namespace) {
if (!namespace) { return; }
$scope.isAdmin = UserService.isNamespaceAdmin(namespace);
$scope.isOrganization = !!UserService.getOrganization(namespace);
});
@ -5749,9 +5838,15 @@ quayApp.directive('buildMessage', function () {
case 'building':
return 'Building image from Dockerfile';
case 'checking-cache':
return 'Looking up cached images';
case 'priming-cache':
return 'Priming cache for build';
case 'build-scheduled':
return 'Preparing build node';
case 'pushing':
return 'Pushing image built from Dockerfile';
@ -5805,6 +5900,7 @@ quayApp.directive('buildProgress', function () {
break;
case 'initializing':
case 'checking-cache':
case 'starting':
case 'waiting':
case 'cannot_load':
@ -5901,6 +5997,10 @@ quayApp.directive('createExternalNotificationDialog', function () {
$scope.events = ExternalNotificationData.getSupportedEvents();
$scope.methods = ExternalNotificationData.getSupportedMethods();
$scope.getPattern = function(field) {
return new RegExp(field.regex);
};
$scope.setEvent = function(event) {
$scope.currentEvent = event;
};
@ -6224,7 +6324,7 @@ quayApp.directive('dockerfileBuildForm', function () {
scope: {
'repository': '=repository',
'startNow': '=startNow',
'hasDockerfile': '=hasDockerfile',
'isReady': '=isReady',
'uploadFailed': '&uploadFailed',
'uploadStarted': '&uploadStarted',
'buildStarted': '&buildStarted',
@ -6235,6 +6335,8 @@ quayApp.directive('dockerfileBuildForm', function () {
},
controller: function($scope, $element, ApiService) {
$scope.internal = {'hasDockerfile': false};
$scope.pull_entity = null;
$scope.is_public = true;
var handleBuildFailed = function(message) {
message = message || 'Dockerfile build failed to start';
@ -6308,8 +6410,12 @@ quayApp.directive('dockerfileBuildForm', function () {
'file_id': fileId
};
if (!$scope.is_public && $scope.pull_entity) {
data['pull_robot'] = $scope.pull_entity['name'];
}
var params = {
'repository': repo.namespace + '/' + repo.name
'repository': repo.namespace + '/' + repo.name,
};
ApiService.requestRepoBuild(data, params).then(function(resp) {
@ -6387,9 +6493,13 @@ quayApp.directive('dockerfileBuildForm', function () {
});
};
$scope.$watch('internal.hasDockerfile', function(d) {
$scope.hasDockerfile = d;
});
var checkIsReady = function() {
$scope.isReady = $scope.internal.hasDockerfile && ($scope.is_public || $scope.pull_entity);
};
$scope.$watch('pull_entity', checkIsReady);
$scope.$watch('is_public', checkIsReady);
$scope.$watch('internal.hasDockerfile', checkIsReady);
$scope.$watch('startNow', function() {
if ($scope.startNow && $scope.repository && !$scope.uploading && !$scope.building) {
@ -6506,6 +6616,54 @@ quayApp.directive('locationView', function () {
});
quayApp.directive('psUsageGraph', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/ps-usage-graph.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
'isEnabled': '=isEnabled'
},
controller: function($scope, $element) {
$scope.counter = -1;
$scope.data = null;
var source = null;
var connect = function() {
if (source) { return; }
source = new EventSource('/realtime/ps');
source.onmessage = function(e) {
$scope.$apply(function() {
$scope.counter++;
$scope.data = JSON.parse(e.data);
});
};
};
var disconnect = function() {
if (!source) { return; }
source.close();
source = null;
};
$scope.$watch('isEnabled', function(value) {
if (value) {
connect();
} else {
disconnect();
}
});
$scope.$on("$destroy", disconnect);
}
};
return directiveDefinitionObject;
});
quayApp.directive('avatar', function () {
var directiveDefinitionObject = {
priority: 0,
@ -6684,6 +6842,7 @@ quayApp.directive('ngBlur', function() {
};
});
quayApp.directive("filePresent", [function () {
return {
restrict: 'A',
@ -6757,7 +6916,6 @@ quayApp.run(['$location', '$rootScope', 'Restangular', 'UserService', 'PlanServi
var changeTab = function(activeTab, opt_timeout) {
var checkCount = 0;
$timeout(function() {
if (checkCount > 5) { return; }
checkCount++;
@ -6821,6 +6979,8 @@ quayApp.run(['$location', '$rootScope', 'Restangular', 'UserService', 'PlanServi
$rootScope.pageClass = current.$$route.pageClass;
}
$rootScope.newLayout = !!current.$$route.newLayout;
if (current.$$route.description) {
$rootScope.description = current.$$route.description;
} else {
@ -6836,26 +6996,28 @@ quayApp.run(['$location', '$rootScope', 'Restangular', 'UserService', 'PlanServi
// Setup deep linking of tabs. This will change the search field of the URL whenever a tab
// is changed in the UI.
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
var tabName = e.target.getAttribute('data-target').substr(1);
$rootScope.$apply(function() {
var isDefaultTab = $('a[data-toggle="tab"]')[0] == e.target;
var newSearch = $.extend($location.search(), {});
if (isDefaultTab) {
delete newSearch['tab'];
} else {
newSearch['tab'] = tabName;
}
$timeout(function() {
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
var tabName = e.target.getAttribute('data-target').substr(1);
$rootScope.$apply(function() {
var isDefaultTab = $('a[data-toggle="tab"]')[0] == e.target;
var newSearch = $.extend($location.search(), {});
if (isDefaultTab) {
delete newSearch['tab'];
} else {
newSearch['tab'] = tabName;
}
$location.search(newSearch);
$location.search(newSearch);
});
e.preventDefault();
});
e.preventDefault();
});
if (activeTab) {
changeTab(activeTab);
}
if (activeTab) {
changeTab(activeTab);
}
}, 400); // 400ms to make sure angular has rendered.
});
var initallyChecked = false;

View file

@ -1011,257 +1011,6 @@ function BuildPackageCtrl($scope, Restangular, ApiService, DataFileService, $rou
getBuildInfo();
}
function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope, $location, $interval, $sanitize,
ansi2html, AngularViewArray, AngularPollChannel) {
var namespace = $routeParams.namespace;
var name = $routeParams.name;
// Watch for changes to the current parameter.
$scope.$on('$routeUpdate', function(){
if ($location.search().current) {
$scope.setCurrentBuild($location.search().current, false);
}
});
$scope.builds = null;
$scope.pollChannel = null;
$scope.buildDialogShowCounter = 0;
$scope.showNewBuildDialog = function() {
$scope.buildDialogShowCounter++;
};
$scope.handleBuildStarted = function(newBuild) {
if (!$scope.builds) { return; }
$scope.builds.unshift(newBuild);
$scope.setCurrentBuild(newBuild['id'], true);
};
$scope.adjustLogHeight = function() {
var triggerOffset = 0;
if ($scope.currentBuild && $scope.currentBuild.trigger) {
triggerOffset = 85;
}
$('.build-logs').height($(window).height() - 415 - triggerOffset);
};
$scope.askRestartBuild = function(build) {
$('#confirmRestartBuildModal').modal({});
};
$scope.restartBuild = function(build) {
$('#confirmRestartBuildModal').modal('hide');
var subdirectory = '';
if (build['job_config']) {
subdirectory = build['job_config']['build_subdir'] || '';
}
var data = {
'file_id': build['resource_key'],
'subdirectory': subdirectory,
'docker_tags': build['job_config']['docker_tags']
};
if (build['pull_robot']) {
data['pull_robot'] = build['pull_robot']['name'];
}
var params = {
'repository': namespace + '/' + name
};
ApiService.requestRepoBuild(data, params).then(function(newBuild) {
if (!$scope.builds) { return; }
$scope.builds.unshift(newBuild);
$scope.setCurrentBuild(newBuild['id'], true);
});
};
$scope.hasLogs = function(container) {
return container.logs.hasEntries;
};
$scope.setCurrentBuild = function(buildId, opt_updateURL) {
if (!$scope.builds) { return; }
// Find the build.
for (var i = 0; i < $scope.builds.length; ++i) {
if ($scope.builds[i].id == buildId) {
$scope.setCurrentBuildInternal(i, $scope.builds[i], opt_updateURL);
return;
}
}
};
$scope.processANSI = function(message, container) {
var filter = container.logs._filter = (container.logs._filter || ansi2html.create());
// Note: order is important here.
var setup = filter.getSetupHtml();
var stream = filter.addInputToStream(message);
var teardown = filter.getTeardownHtml();
return setup + stream + teardown;
};
$scope.setCurrentBuildInternal = function(index, build, opt_updateURL) {
if (build == $scope.currentBuild) { return; }
$scope.logEntries = null;
$scope.logStartIndex = null;
$scope.currentParentEntry = null;
$scope.currentBuild = build;
if (opt_updateURL) {
if (build) {
$location.search('current', build.id);
} else {
$location.search('current', null);
}
}
// Timeout needed to ensure the log element has been created
// before its height is adjusted.
setTimeout(function() {
$scope.adjustLogHeight();
}, 1);
// Stop any existing polling.
if ($scope.pollChannel) {
$scope.pollChannel.stop();
}
// Create a new channel for polling the build status and logs.
var conductStatusAndLogRequest = function(callback) {
getBuildStatusAndLogs(build, callback);
};
$scope.pollChannel = AngularPollChannel.create($scope, conductStatusAndLogRequest, 5 * 1000 /* 5s */);
$scope.pollChannel.start();
};
var processLogs = function(logs, startIndex, endIndex) {
if (!$scope.logEntries) { $scope.logEntries = []; }
// If the start index given is less than that requested, then we've received a larger
// pool of logs, and we need to only consider the new ones.
if (startIndex < $scope.logStartIndex) {
logs = logs.slice($scope.logStartIndex - startIndex);
}
for (var i = 0; i < logs.length; ++i) {
var entry = logs[i];
var type = entry['type'] || 'entry';
if (type == 'command' || type == 'phase' || type == 'error') {
entry['logs'] = AngularViewArray.create();
entry['index'] = $scope.logStartIndex + i;
$scope.logEntries.push(entry);
$scope.currentParentEntry = entry;
} else if ($scope.currentParentEntry) {
$scope.currentParentEntry['logs'].push(entry);
}
}
return endIndex;
};
var getBuildStatusAndLogs = function(build, callback) {
var params = {
'repository': namespace + '/' + name,
'build_uuid': build.id
};
ApiService.getRepoBuildStatus(null, params, true).then(function(resp) {
if (build != $scope.currentBuild) { callback(false); return; }
// Note: We use extend here rather than replacing as Angular is depending on the
// root build object to remain the same object.
var matchingBuilds = $.grep($scope.builds, function(elem) {
return elem['id'] == resp['id']
});
var currentBuild = matchingBuilds.length > 0 ? matchingBuilds[0] : null;
if (currentBuild) {
currentBuild = $.extend(true, currentBuild, resp);
} else {
currentBuild = resp;
$scope.builds.push(currentBuild);
}
// Load the updated logs for the build.
var options = {
'start': $scope.logStartIndex
};
ApiService.getRepoBuildLogsAsResource(params, true).withOptions(options).get(function(resp) {
if (build != $scope.currentBuild) { callback(false); return; }
// Process the logs we've received.
$scope.logStartIndex = processLogs(resp['logs'], resp['start'], resp['total']);
// If the build status is an error, open the last two log entries.
if (currentBuild['phase'] == 'error' && $scope.logEntries.length > 1) {
var openLogEntries = function(entry) {
if (entry.logs) {
entry.logs.setVisible(true);
}
};
openLogEntries($scope.logEntries[$scope.logEntries.length - 2]);
openLogEntries($scope.logEntries[$scope.logEntries.length - 1]);
}
// If the build phase is an error or a complete, then we mark the channel
// as closed.
callback(currentBuild['phase'] != 'error' && currentBuild['phase'] != 'complete');
}, function() {
callback(false);
});
}, function() {
callback(false);
});
};
var fetchRepository = function() {
var params = {'repository': namespace + '/' + name};
$rootScope.title = 'Loading Repository...';
$scope.repository = ApiService.getRepoAsResource(params).get(function(repo) {
if (!repo.can_write) {
$rootScope.title = 'Unknown builds';
$scope.accessDenied = true;
return;
}
$rootScope.title = 'Repository Builds';
$scope.repo = repo;
getBuildInfo();
});
};
var getBuildInfo = function(repo) {
var params = {
'repository': namespace + '/' + name
};
ApiService.getRepoBuilds(null, params).then(function(resp) {
$scope.builds = resp.builds;
if ($location.search().current) {
$scope.setCurrentBuild($location.search().current, false);
} else if ($scope.builds.length > 0) {
$scope.setCurrentBuild($scope.builds[0].id, true);
}
});
};
fetchRepository();
}
function RepoAdminCtrl($scope, Restangular, ApiService, KeyService, TriggerService, $routeParams,
$rootScope, $location, UserService, Config, Features, ExternalNotificationData) {
@ -2748,138 +2497,6 @@ function ManageApplicationCtrl($scope, $routeParams, $rootScope, $location, $tim
loadApplicationInfo();
}
function SuperUserAdminCtrl($scope, ApiService, Features, UserService) {
if (!Features.SUPER_USERS) {
return;
}
// Monitor any user changes and place the current user into the scope.
UserService.updateUserIn($scope);
$scope.logsCounter = 0;
$scope.newUser = {};
$scope.createdUsers = [];
$scope.systemUsage = null;
$scope.getUsage = function() {
if ($scope.systemUsage) { return; }
ApiService.getSystemUsage().then(function(resp) {
$scope.systemUsage = resp;
}, ApiService.errorDisplay('Cannot load system usage. Please contact support.'))
}
$scope.loadLogs = function() {
$scope.logsCounter++;
};
$scope.loadUsers = function() {
if ($scope.users) {
return;
}
$scope.loadUsersInternal();
};
$scope.loadUsersInternal = function() {
ApiService.listAllUsers().then(function(resp) {
$scope.users = resp['users'];
$scope.showInterface = true;
}, function(resp) {
$scope.users = [];
$scope.usersError = resp['data']['message'] || resp['data']['error_description'];
});
};
$scope.showChangePassword = function(user) {
$scope.userToChange = user;
$('#changePasswordModal').modal({});
};
$scope.createUser = function() {
$scope.creatingUser = true;
var errorHandler = ApiService.errorDisplay('Cannot create user', function() {
$scope.creatingUser = false;
});
ApiService.createInstallUser($scope.newUser, null).then(function(resp) {
$scope.creatingUser = false;
$scope.newUser = {};
$scope.createdUsers.push(resp);
}, errorHandler)
};
$scope.showDeleteUser = function(user) {
if (user.username == UserService.currentUser().username) {
bootbox.dialog({
"message": 'Cannot delete yourself!',
"title": "Cannot delete user",
"buttons": {
"close": {
"label": "Close",
"className": "btn-primary"
}
}
});
return;
}
$scope.userToDelete = user;
$('#confirmDeleteUserModal').modal({});
};
$scope.changeUserPassword = function(user) {
$('#changePasswordModal').modal('hide');
var params = {
'username': user.username
};
var data = {
'password': user.password
};
ApiService.changeInstallUser(data, params).then(function(resp) {
$scope.loadUsersInternal();
}, ApiService.errorDisplay('Could not change user'));
};
$scope.deleteUser = function(user) {
$('#confirmDeleteUserModal').modal('hide');
var params = {
'username': user.username
};
ApiService.deleteInstallUser(null, params).then(function(resp) {
$scope.loadUsersInternal();
}, ApiService.errorDisplay('Cannot delete user'));
};
$scope.sendRecoveryEmail = function(user) {
var params = {
'username': user.username
};
ApiService.sendInstallUserRecoveryEmail(null, params).then(function(resp) {
bootbox.dialog({
"message": "A recovery email has been sent to " + resp['email'],
"title": "Recovery email sent",
"buttons": {
"close": {
"label": "Close",
"className": "btn-primary"
}
}
});
}, ApiService.errorDisplay('Cannot send recovery email'))
};
$scope.loadUsers();
}
function TourCtrl($scope, $location) {
$scope.kind = $location.path().substring('/tour/'.length);
}

View file

@ -0,0 +1,272 @@
function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope, $location, $interval, $sanitize,
ansi2html, AngularViewArray, AngularPollChannel) {
var namespace = $routeParams.namespace;
var name = $routeParams.name;
// Watch for changes to the current parameter.
$scope.$on('$routeUpdate', function(){
if ($location.search().current) {
$scope.setCurrentBuild($location.search().current, false);
}
});
$scope.builds = null;
$scope.pollChannel = null;
$scope.buildDialogShowCounter = 0;
$scope.showNewBuildDialog = function() {
$scope.buildDialogShowCounter++;
};
$scope.handleBuildStarted = function(newBuild) {
if (!$scope.builds) { return; }
$scope.builds.unshift(newBuild);
$scope.setCurrentBuild(newBuild['id'], true);
};
$scope.adjustLogHeight = function() {
var triggerOffset = 0;
if ($scope.currentBuild && $scope.currentBuild.trigger) {
triggerOffset = 85;
}
$('.build-logs').height($(window).height() - 415 - triggerOffset);
};
$scope.askRestartBuild = function(build) {
$('#confirmRestartBuildModal').modal({});
};
$scope.askCancelBuild = function(build) {
bootbox.confirm('Are you sure you want to cancel this build?', function(r) {
if (r) {
var params = {
'repository': namespace + '/' + name,
'build_uuid': build.id
};
ApiService.cancelRepoBuild(null, params).then(function() {
if (!$scope.builds) { return; }
$scope.builds.splice($.inArray(build, $scope.builds), 1);
if ($scope.builds.length) {
$scope.currentBuild = $scope.builds[0];
} else {
$scope.currentBuild = null;
}
}, ApiService.errorDisplay('Cannot cancel build'));
}
});
};
$scope.restartBuild = function(build) {
$('#confirmRestartBuildModal').modal('hide');
var subdirectory = '';
if (build['job_config']) {
subdirectory = build['job_config']['build_subdir'] || '';
}
var data = {
'file_id': build['resource_key'],
'subdirectory': subdirectory,
'docker_tags': build['job_config']['docker_tags']
};
if (build['pull_robot']) {
data['pull_robot'] = build['pull_robot']['name'];
}
var params = {
'repository': namespace + '/' + name
};
ApiService.requestRepoBuild(data, params).then(function(newBuild) {
if (!$scope.builds) { return; }
$scope.builds.unshift(newBuild);
$scope.setCurrentBuild(newBuild['id'], true);
});
};
$scope.hasLogs = function(container) {
return container.logs.hasEntries;
};
$scope.setCurrentBuild = function(buildId, opt_updateURL) {
if (!$scope.builds) { return; }
// Find the build.
for (var i = 0; i < $scope.builds.length; ++i) {
if ($scope.builds[i].id == buildId) {
$scope.setCurrentBuildInternal(i, $scope.builds[i], opt_updateURL);
return;
}
}
};
$scope.processANSI = function(message, container) {
var filter = container.logs._filter = (container.logs._filter || ansi2html.create());
// Note: order is important here.
var setup = filter.getSetupHtml();
var stream = filter.addInputToStream(message);
var teardown = filter.getTeardownHtml();
return setup + stream + teardown;
};
$scope.setCurrentBuildInternal = function(index, build, opt_updateURL) {
if (build == $scope.currentBuild) { return; }
$scope.logEntries = null;
$scope.logStartIndex = null;
$scope.currentParentEntry = null;
$scope.currentBuild = build;
if (opt_updateURL) {
if (build) {
$location.search('current', build.id);
} else {
$location.search('current', null);
}
}
// Timeout needed to ensure the log element has been created
// before its height is adjusted.
setTimeout(function() {
$scope.adjustLogHeight();
}, 1);
// Stop any existing polling.
if ($scope.pollChannel) {
$scope.pollChannel.stop();
}
// Create a new channel for polling the build status and logs.
var conductStatusAndLogRequest = function(callback) {
getBuildStatusAndLogs(build, callback);
};
$scope.pollChannel = AngularPollChannel.create($scope, conductStatusAndLogRequest, 5 * 1000 /* 5s */);
$scope.pollChannel.start();
};
var processLogs = function(logs, startIndex, endIndex) {
if (!$scope.logEntries) { $scope.logEntries = []; }
// If the start index given is less than that requested, then we've received a larger
// pool of logs, and we need to only consider the new ones.
if (startIndex < $scope.logStartIndex) {
logs = logs.slice($scope.logStartIndex - startIndex);
}
for (var i = 0; i < logs.length; ++i) {
var entry = logs[i];
var type = entry['type'] || 'entry';
if (type == 'command' || type == 'phase' || type == 'error') {
entry['logs'] = AngularViewArray.create();
entry['index'] = $scope.logStartIndex + i;
$scope.logEntries.push(entry);
$scope.currentParentEntry = entry;
} else if ($scope.currentParentEntry) {
$scope.currentParentEntry['logs'].push(entry);
}
}
return endIndex;
};
var getBuildStatusAndLogs = function(build, callback) {
var params = {
'repository': namespace + '/' + name,
'build_uuid': build.id
};
ApiService.getRepoBuildStatus(null, params, true).then(function(resp) {
if (build != $scope.currentBuild) { callback(false); return; }
// Note: We use extend here rather than replacing as Angular is depending on the
// root build object to remain the same object.
var matchingBuilds = $.grep($scope.builds, function(elem) {
return elem['id'] == resp['id']
});
var currentBuild = matchingBuilds.length > 0 ? matchingBuilds[0] : null;
if (currentBuild) {
currentBuild = $.extend(true, currentBuild, resp);
} else {
currentBuild = resp;
$scope.builds.push(currentBuild);
}
// Load the updated logs for the build.
var options = {
'start': $scope.logStartIndex
};
ApiService.getRepoBuildLogsAsResource(params, true).withOptions(options).get(function(resp) {
if (build != $scope.currentBuild) { callback(false); return; }
// Process the logs we've received.
$scope.logStartIndex = processLogs(resp['logs'], resp['start'], resp['total']);
// If the build status is an error, open the last two log entries.
if (currentBuild['phase'] == 'error' && $scope.logEntries.length > 1) {
var openLogEntries = function(entry) {
if (entry.logs) {
entry.logs.setVisible(true);
}
};
openLogEntries($scope.logEntries[$scope.logEntries.length - 2]);
openLogEntries($scope.logEntries[$scope.logEntries.length - 1]);
}
// If the build phase is an error or a complete, then we mark the channel
// as closed.
callback(currentBuild['phase'] != 'error' && currentBuild['phase'] != 'complete');
}, function() {
callback(false);
});
}, function() {
callback(false);
});
};
var fetchRepository = function() {
var params = {'repository': namespace + '/' + name};
$rootScope.title = 'Loading Repository...';
$scope.repository = ApiService.getRepoAsResource(params).get(function(repo) {
if (!repo.can_write) {
$rootScope.title = 'Unknown builds';
$scope.accessDenied = true;
return;
}
$rootScope.title = 'Repository Builds';
$scope.repo = repo;
getBuildInfo();
});
};
var getBuildInfo = function(repo) {
var params = {
'repository': namespace + '/' + name
};
ApiService.getRepoBuilds(null, params).then(function(resp) {
$scope.builds = resp.builds;
if ($location.search().current) {
$scope.setCurrentBuild($location.search().current, false);
} else if ($scope.builds.length > 0) {
$scope.setCurrentBuild($scope.builds[0].id, true);
}
});
};
fetchRepository();
}

View file

@ -0,0 +1,282 @@
function SetupCtrl($scope, $timeout, ApiService, Features, UserService, ContainerService, CoreDialog) {
if (!Features.SUPER_USERS) {
return;
}
$scope.HOSTNAME_REGEX = '^[a-zA-Z-0-9\.]+(:[0-9]+)?$';
$scope.validateHostname = function(hostname) {
if (hostname.indexOf('127.0.0.1') == 0 || hostname.indexOf('localhost') == 0) {
return 'Please specify a non-localhost hostname. "localhost" will refer to the container, not your machine.'
}
return null;
};
// Note: The values of the enumeration are important for isStepFamily. For example,
// *all* states under the "configuring db" family must start with "config-db".
$scope.States = {
// Loading the state of the product.
'LOADING': 'loading',
// The configuration directory is missing.
'MISSING_CONFIG_DIR': 'missing-config-dir',
// The config.yaml exists but it is invalid.
'INVALID_CONFIG': 'config-invalid',
// DB is being configured.
'CONFIG_DB': 'config-db',
// DB information is being validated.
'VALIDATING_DB': 'config-db-validating',
// DB information is being saved to the config.
'SAVING_DB': 'config-db-saving',
// A validation error occurred with the database.
'DB_ERROR': 'config-db-error',
// Database is being setup.
'DB_SETUP': 'setup-db',
// Database setup has succeeded.
'DB_SETUP_SUCCESS': 'setup-db-success',
// An error occurred when setting up the database.
'DB_SETUP_ERROR': 'setup-db-error',
// The container is being restarted for the database changes.
'DB_RESTARTING': 'setup-db-restarting',
// A superuser is being configured.
'CREATE_SUPERUSER': 'create-superuser',
// The superuser is being created.
'CREATING_SUPERUSER': 'create-superuser-creating',
// An error occurred when setting up the superuser.
'SUPERUSER_ERROR': 'create-superuser-error',
// The superuser was created successfully.
'SUPERUSER_CREATED': 'create-superuser-created',
// General configuration is being setup.
'CONFIG': 'config',
// The configuration is fully valid.
'VALID_CONFIG': 'valid-config',
// The container is being restarted for the configuration changes.
'CONFIG_RESTARTING': 'config-restarting',
// The product is ready for use.
'READY': 'ready'
}
$scope.csrf_token = window.__token;
$scope.currentStep = $scope.States.LOADING;
$scope.errors = {};
$scope.stepProgress = [];
$scope.hasSSL = false;
$scope.hostname = null;
$scope.$watch('currentStep', function(currentStep) {
$scope.stepProgress = $scope.getProgress(currentStep);
switch (currentStep) {
case $scope.States.CONFIG:
$('#setupModal').modal('hide');
break;
case $scope.States.MISSING_CONFIG_DIR:
$scope.showMissingConfigDialog();
break;
case $scope.States.INVALID_CONFIG:
$scope.showInvalidConfigDialog();
break;
case $scope.States.DB_SETUP:
$scope.performDatabaseSetup();
// Fall-through.
case $scope.States.CREATE_SUPERUSER:
case $scope.States.DB_RESTARTING:
case $scope.States.CONFIG_DB:
case $scope.States.VALID_CONFIG:
case $scope.States.READY:
$('#setupModal').modal({
keyboard: false,
backdrop: 'static'
});
break;
}
});
$scope.restartContainer = function(state) {
$scope.currentStep = state;
ContainerService.restartContainer(function() {
$scope.checkStatus()
});
};
$scope.showSuperuserPanel = function() {
$('#setupModal').modal('hide');
var prefix = $scope.hasSSL ? 'https' : 'http';
var hostname = $scope.hostname;
window.location = prefix + '://' + hostname + '/superuser';
};
$scope.configurationSaved = function(config) {
$scope.hasSSL = config['PREFERRED_URL_SCHEME'] == 'https';
$scope.hostname = config['SERVER_HOSTNAME'];
$scope.currentStep = $scope.States.VALID_CONFIG;
};
$scope.getProgress = function(step) {
var isStep = $scope.isStep;
var isStepFamily = $scope.isStepFamily;
var States = $scope.States;
return [
isStepFamily(step, States.CONFIG_DB),
isStepFamily(step, States.DB_SETUP),
isStep(step, States.DB_RESTARTING),
isStepFamily(step, States.CREATE_SUPERUSER),
isStep(step, States.CONFIG),
isStep(step, States.VALID_CONFIG),
isStep(step, States.CONFIG_RESTARTING),
isStep(step, States.READY)
];
};
$scope.isStepFamily = function(step, family) {
if (!step) { return false; }
return step.indexOf(family) == 0;
};
$scope.isStep = function(step) {
for (var i = 1; i < arguments.length; ++i) {
if (arguments[i] == step) {
return true;
}
}
return false;
};
$scope.showInvalidConfigDialog = function() {
var message = "The <code>config.yaml</code> file found in <code>conf/stack</code> could not be parsed."
var title = "Invalid configuration file";
CoreDialog.fatal(title, message);
};
$scope.showMissingConfigDialog = function() {
var message = "A volume should be mounted into the container at <code>/conf/stack</code>: " +
"<br><br><pre>docker run -v /path/to/config:/conf/stack</pre>" +
"<br>Once fixed, restart the container. For more information, " +
"<a href='https://coreos.com/docs/enterprise-registry/initial-setup/'>" +
"Read the Setup Guide</a>"
var title = "Missing configuration volume";
CoreDialog.fatal(title, message);
};
$scope.parseDbUri = function(value) {
if (!value) { return null; }
// Format: mysql+pymysql://<username>:<url escaped password>@<hostname>/<database_name>
var uri = URI(value);
return {
'kind': uri.protocol(),
'username': uri.username(),
'password': uri.password(),
'server': uri.host(),
'database': uri.path() ? uri.path().substr(1) : ''
};
};
$scope.serializeDbUri = function(fields) {
if (!fields['server']) { return ''; }
try {
if (!fields['server']) { return ''; }
if (!fields['database']) { return ''; }
var uri = URI();
uri = uri && uri.host(fields['server']);
uri = uri && uri.protocol(fields['kind']);
uri = uri && uri.username(fields['username']);
uri = uri && uri.password(fields['password']);
uri = uri && uri.path('/' + (fields['database'] || ''));
uri = uri && uri.toString();
} catch (ex) {
return '';
}
return uri;
};
$scope.createSuperUser = function() {
$scope.currentStep = $scope.States.CREATING_SUPERUSER;
ApiService.scCreateInitialSuperuser($scope.superUser, null).then(function(resp) {
UserService.load();
$scope.checkStatus();
}, function(resp) {
$scope.currentStep = $scope.States.SUPERUSER_ERROR;
$scope.errors.SuperuserCreationError = ApiService.getErrorMessage(resp, 'Could not create superuser');
});
};
$scope.performDatabaseSetup = function() {
$scope.currentStep = $scope.States.DB_SETUP;
ApiService.scSetupDatabase(null, null).then(function(resp) {
if (resp['error']) {
$scope.currentStep = $scope.States.DB_SETUP_ERROR;
$scope.errors.DatabaseSetupError = resp['error'];
} else {
$scope.currentStep = $scope.States.DB_SETUP_SUCCESS;
}
}, ApiService.errorDisplay('Could not setup database. Please report this to support.'))
};
$scope.validateDatabase = function() {
$scope.currentStep = $scope.States.VALIDATING_DB;
$scope.databaseInvalid = null;
var data = {
'config': {
'DB_URI': $scope.databaseUri
},
'hostname': window.location.host
};
var params = {
'service': 'database'
};
ApiService.scValidateConfig(data, params).then(function(resp) {
var status = resp.status;
if (status) {
$scope.currentStep = $scope.States.SAVING_DB;
ApiService.scUpdateConfig(data, null).then(function(resp) {
$scope.checkStatus();
}, ApiService.errorDisplay('Cannot update config. Please report this to support'));
} else {
$scope.currentStep = $scope.States.DB_ERROR;
$scope.errors.DatabaseValidationError = resp.reason;
}
}, ApiService.errorDisplay('Cannot validate database. Please report this to support'));
};
$scope.checkStatus = function() {
ContainerService.checkStatus(function(resp) {
$scope.currentStep = resp['status'];
}, $scope.hasSSL);
};
// Load the initial status.
$scope.checkStatus();
}

View file

@ -0,0 +1,229 @@
function SuperUserAdminCtrl($scope, $timeout, ApiService, Features, UserService, ContainerService, AngularPollChannel, CoreDialog) {
if (!Features.SUPER_USERS) {
return;
}
// Monitor any user changes and place the current user into the scope.
UserService.updateUserIn($scope);
$scope.configStatus = null;
$scope.requiresRestart = null;
$scope.logsCounter = 0;
$scope.newUser = {};
$scope.createdUser = null;
$scope.systemUsage = null;
$scope.debugServices = null;
$scope.debugLogs = null;
$scope.pollChannel = null;
$scope.logsScrolled = false;
$scope.csrf_token = encodeURIComponent(window.__token);
$scope.dashboardActive = false;
$scope.setDashboardActive = function(active) {
$scope.dashboardActive = active;
};
$scope.configurationSaved = function() {
$scope.requiresRestart = true;
};
$scope.showCreateUser = function() {
$scope.createdUser = null;
$('#createUserModal').modal('show');
};
$scope.viewSystemLogs = function(service) {
if ($scope.pollChannel) {
$scope.pollChannel.stop();
}
$scope.debugService = service;
$scope.debugLogs = null;
$scope.pollChannel = AngularPollChannel.create($scope, $scope.loadServiceLogs, 2 * 1000 /* 2s */);
$scope.pollChannel.start();
};
$scope.loadServiceLogs = function(callback) {
if (!$scope.debugService) { return; }
var params = {
'service': $scope.debugService
};
var errorHandler = ApiService.errorDisplay('Cannot load system logs. Please contact support.',
function() {
callback(false);
})
ApiService.getSystemLogs(null, params, /* background */true).then(function(resp) {
$scope.debugLogs = resp['logs'];
callback(true);
}, errorHandler);
};
$scope.loadDebugServices = function() {
if ($scope.pollChannel) {
$scope.pollChannel.stop();
}
$scope.debugService = null;
ApiService.listSystemLogServices().then(function(resp) {
$scope.debugServices = resp['services'];
}, ApiService.errorDisplay('Cannot load system logs. Please contact support.'))
};
$scope.getUsage = function() {
if ($scope.systemUsage) { return; }
ApiService.getSystemUsage().then(function(resp) {
$scope.systemUsage = resp;
}, ApiService.errorDisplay('Cannot load system usage. Please contact support.'))
}
$scope.loadUsageLogs = function() {
$scope.logsCounter++;
};
$scope.loadUsers = function() {
if ($scope.users) {
return;
}
$scope.loadUsersInternal();
};
$scope.loadUsersInternal = function() {
ApiService.listAllUsers().then(function(resp) {
$scope.users = resp['users'];
$scope.showInterface = true;
}, function(resp) {
$scope.users = [];
$scope.usersError = resp['data']['message'] || resp['data']['error_description'];
});
};
$scope.showChangePassword = function(user) {
$scope.userToChange = user;
$('#changePasswordModal').modal({});
};
$scope.createUser = function() {
$scope.creatingUser = true;
$scope.createdUser = null;
var errorHandler = ApiService.errorDisplay('Cannot create user', function() {
$scope.creatingUser = false;
$('#createUserModal').modal('hide');
});
ApiService.createInstallUser($scope.newUser, null).then(function(resp) {
$scope.creatingUser = false;
$scope.newUser = {};
$scope.createdUser = resp;
$scope.loadUsersInternal();
}, errorHandler)
};
$scope.showDeleteUser = function(user) {
if (user.username == UserService.currentUser().username) {
bootbox.dialog({
"message": 'Cannot delete yourself!',
"title": "Cannot delete user",
"buttons": {
"close": {
"label": "Close",
"className": "btn-primary"
}
}
});
return;
}
$scope.userToDelete = user;
$('#confirmDeleteUserModal').modal({});
};
$scope.changeUserPassword = function(user) {
$('#changePasswordModal').modal('hide');
var params = {
'username': user.username
};
var data = {
'password': user.password
};
ApiService.changeInstallUser(data, params).then(function(resp) {
$scope.loadUsersInternal();
}, ApiService.errorDisplay('Could not change user'));
};
$scope.deleteUser = function(user) {
$('#confirmDeleteUserModal').modal('hide');
var params = {
'username': user.username
};
ApiService.deleteInstallUser(null, params).then(function(resp) {
$scope.loadUsersInternal();
}, ApiService.errorDisplay('Cannot delete user'));
};
$scope.sendRecoveryEmail = function(user) {
var params = {
'username': user.username
};
ApiService.sendInstallUserRecoveryEmail(null, params).then(function(resp) {
bootbox.dialog({
"message": "A recovery email has been sent to " + resp['email'],
"title": "Recovery email sent",
"buttons": {
"close": {
"label": "Close",
"className": "btn-primary"
}
}
});
}, ApiService.errorDisplay('Cannot send recovery email'))
};
$scope.restartContainer = function() {
$('#restartingContainerModal').modal({
keyboard: false,
backdrop: 'static'
});
ContainerService.restartContainer(function() {
$scope.checkStatus()
});
};
$scope.checkStatus = function() {
ContainerService.checkStatus(function(resp) {
$('#restartingContainerModal').modal('hide');
$scope.configStatus = resp['status'];
$scope.requiresRestart = resp['requires_restart'];
if ($scope.configStatus == 'ready') {
$scope.loadUsers();
} else {
var message = "Installation of this product has not yet been completed." +
"<br><br>Please read the " +
"<a href='https://coreos.com/docs/enterprise-registry/initial-setup/'>" +
"Setup Guide</a>"
var title = "Installation Incomplete";
CoreDialog.fatal(title, message);
}
});
};
// Load the initial status.
$scope.checkStatus();
}

View file

@ -0,0 +1,765 @@
angular.module("core-config-setup", ['angularFileUpload'])
.directive('configSetupTool', function() {
var directiveDefinitionObject = {
priority: 1,
templateUrl: '/static/directives/config/config-setup-tool.html',
replace: true,
transclude: true,
restrict: 'C',
scope: {
'isActive': '=isActive',
'configurationSaved': '&configurationSaved'
},
controller: function($rootScope, $scope, $element, $timeout, ApiService) {
$scope.HOSTNAME_REGEX = '^[a-zA-Z-0-9\.]+(:[0-9]+)?$';
$scope.GITHUB_REGEX = '^https?://([a-zA-Z0-9]+\.?\/?)+$';
$scope.SERVICES = [
{'id': 'redis', 'title': 'Redis'},
{'id': 'registry-storage', 'title': 'Registry Storage'},
{'id': 'ssl', 'title': 'SSL certificate and key', 'condition': function(config) {
return config.PREFERRED_URL_SCHEME == 'https';
}},
{'id': 'ldap', 'title': 'LDAP Authentication', 'condition': function(config) {
return config.AUTHENTICATION_TYPE == 'LDAP';
}},
{'id': 'mail', 'title': 'E-mail Support', 'condition': function(config) {
return config.FEATURE_MAILING;
}},
{'id': 'github-login', 'title': 'Github (Enterprise) Authentication', 'condition': function(config) {
return config.FEATURE_GITHUB_LOGIN;
}},
{'id': 'google-login', 'title': 'Google Authentication', 'condition': function(config) {
return config.FEATURE_GOOGLE_LOGIN;
}},
{'id': 'github-trigger', 'title': 'Github (Enterprise) Build Triggers', 'condition': function(config) {
return config.FEATURE_GITHUB_BUILD;
}}
];
$scope.STORAGE_CONFIG_FIELDS = {
'LocalStorage': [
{'name': 'storage_path', 'title': 'Storage Directory', 'placeholder': '/some/directory', 'kind': 'text'}
],
'S3Storage': [
{'name': 's3_access_key', 'title': 'AWS Access Key', 'placeholder': 'accesskeyhere', 'kind': 'text'},
{'name': 's3_secret_key', 'title': 'AWS Secret Key', 'placeholder': 'secretkeyhere', 'kind': 'text'},
{'name': 's3_bucket', 'title': 'S3 Bucket', 'placeholder': 'my-cool-bucket', 'kind': 'text'},
{'name': 'storage_path', 'title': 'Storage Directory', 'placeholder': '/path/inside/bucket', 'kind': 'text'}
],
'GoogleCloudStorage': [
{'name': 'access_key', 'title': 'Cloud Access Key', 'placeholder': 'accesskeyhere', 'kind': 'text'},
{'name': 'secret_key', 'title': 'Cloud Secret Key', 'placeholder': 'secretkeyhere', 'kind': 'text'},
{'name': 'bucket_name', 'title': 'GCS Bucket', 'placeholder': 'my-cool-bucket', 'kind': 'text'},
{'name': 'storage_path', 'title': 'Storage Directory', 'placeholder': '/path/inside/bucket', 'kind': 'text'}
],
'RadosGWStorage': [
{'name': 'hostname', 'title': 'Rados Server Hostname', 'placeholder': 'my.rados.hostname', 'kind': 'text'},
{'name': 'is_secure', 'title': 'Is Secure', 'placeholder': 'Require SSL', 'kind': 'bool'},
{'name': 'access_key', 'title': 'Access Key', 'placeholder': 'accesskeyhere', 'kind': 'text', 'help_url': 'http://ceph.com/docs/master/radosgw/admin/'},
{'name': 'secret_key', 'title': 'Secret Key', 'placeholder': 'secretkeyhere', 'kind': 'text'},
{'name': 'bucket_name', 'title': 'Bucket Name', 'placeholder': 'my-cool-bucket', 'kind': 'text'},
{'name': 'storage_path', 'title': 'Storage Directory', 'placeholder': '/path/inside/bucket', 'kind': 'text'}
]
};
$scope.validateHostname = function(hostname) {
if (hostname.indexOf('127.0.0.1') == 0 || hostname.indexOf('localhost') == 0) {
return 'Please specify a non-localhost hostname. "localhost" will refer to the container, not your machine.'
}
return null;
};
$scope.config = null;
$scope.mapped = {
'$hasChanges': false
};
$scope.validating = null;
$scope.savingConfiguration = false;
$scope.getServices = function(config) {
var services = [];
if (!config) { return services; }
for (var i = 0; i < $scope.SERVICES.length; ++i) {
var service = $scope.SERVICES[i];
if (!service.condition || service.condition(config)) {
services.push({
'service': service,
'status': 'validating'
});
}
}
return services;
};
$scope.validationStatus = function(serviceInfos) {
if (!serviceInfos) { return 'validating'; }
var hasError = false;
for (var i = 0; i < serviceInfos.length; ++i) {
if (serviceInfos[i].status == 'validating') {
return 'validating';
}
if (serviceInfos[i].status == 'error') {
hasError = true;
}
}
return hasError ? 'failed' : 'success';
};
$scope.cancelValidation = function() {
$('#validateAndSaveModal').modal('hide');
$scope.validating = null;
$scope.savingConfiguration = false;
};
$scope.validateService = function(serviceInfo) {
var params = {
'service': serviceInfo.service.id
};
ApiService.scValidateConfig({'config': $scope.config}, params).then(function(resp) {
serviceInfo.status = resp.status ? 'success' : 'error';
serviceInfo.errorMessage = $.trim(resp.reason || '');
}, ApiService.errorDisplay('Could not validate configuration. Please report this error.'));
};
$scope.checkValidateAndSave = function() {
if ($scope.configform.$valid) {
$scope.validateAndSave();
return;
}
$element.find("input.ng-invalid:first")[0].scrollIntoView();
$element.find("input.ng-invalid:first").focus();
};
$scope.validateAndSave = function() {
$scope.savingConfiguration = false;
$scope.validating = $scope.getServices($scope.config);
$('#validateAndSaveModal').modal({
keyboard: false,
backdrop: 'static'
});
for (var i = 0; i < $scope.validating.length; ++i) {
var serviceInfo = $scope.validating[i];
$scope.validateService(serviceInfo);
}
};
$scope.saveConfiguration = function() {
$scope.savingConfiguration = true;
// Make sure to note that fully verified setup is completed. We use this as a signal
// in the setup tool.
$scope.config['SETUP_COMPLETE'] = true;
var data = {
'config': $scope.config,
'hostname': window.location.host
};
ApiService.scUpdateConfig(data).then(function(resp) {
$scope.savingConfiguration = false;
$scope.mapped.$hasChanges = false;
$('#validateAndSaveModal').modal('hide');
$scope.configurationSaved({'config': $scope.config});
}, ApiService.errorDisplay('Could not save configuration. Please report this error.'));
};
var githubSelector = function(key) {
return function(value) {
if (!value || !$scope.config) { return; }
if (!$scope.config[key]) {
$scope.config[key] = {};
}
if (value == 'enterprise') {
if ($scope.config[key]['GITHUB_ENDPOINT'] == 'https://github.com/') {
$scope.config[key]['GITHUB_ENDPOINT'] = '';
}
delete $scope.config[key]['API_ENDPOINT'];
} else if (value == 'hosted') {
$scope.config[key]['GITHUB_ENDPOINT'] = 'https://github.com/';
$scope.config[key]['API_ENDPOINT'] = 'https://api.github.com/';
}
};
};
var getKey = function(config, path) {
if (!config) {
return null;
}
var parts = path.split('.');
var current = config;
for (var i = 0; i < parts.length; ++i) {
var part = parts[i];
if (!current[part]) { return null; }
current = current[part];
}
return current;
};
var initializeMappedLogic = function(config) {
var gle = getKey(config, 'GITHUB_LOGIN_CONFIG.GITHUB_ENDPOINT');
var gte = getKey(config, 'GITHUB_TRIGGER_CONFIG.GITHUB_ENDPOINT');
$scope.mapped['GITHUB_LOGIN_KIND'] = gle == 'https://github.com/' ? 'hosted' : 'enterprise';
$scope.mapped['GITHUB_TRIGGER_KIND'] = gte == 'https://github.com/' ? 'hosted' : 'enterprise';
$scope.mapped['redis'] = {};
$scope.mapped['redis']['host'] = getKey(config, 'BUILDLOGS_REDIS.host') || getKey(config, 'USER_EVENTS_REDIS.host');
$scope.mapped['redis']['port'] = getKey(config, 'BUILDLOGS_REDIS.port') || getKey(config, 'USER_EVENTS_REDIS.port');
$scope.mapped['redis']['password'] = getKey(config, 'BUILDLOGS_REDIS.password') || getKey(config, 'USER_EVENTS_REDIS.password');
};
var redisSetter = function(keyname) {
return function(value) {
if (value == null || !$scope.config) { return; }
if (!$scope.config['BUILDLOGS_REDIS']) {
$scope.config['BUILDLOGS_REDIS'] = {};
}
if (!$scope.config['USER_EVENTS_REDIS']) {
$scope.config['USER_EVENTS_REDIS'] = {};
}
if (!value) {
delete $scope.config['BUILDLOGS_REDIS'][keyname];
delete $scope.config['USER_EVENTS_REDIS'][keyname];
return;
}
$scope.config['BUILDLOGS_REDIS'][keyname] = value;
$scope.config['USER_EVENTS_REDIS'][keyname] = value;
};
};
// Add mapped logic.
$scope.$watch('mapped.GITHUB_LOGIN_KIND', githubSelector('GITHUB_LOGIN_CONFIG'));
$scope.$watch('mapped.GITHUB_TRIGGER_KIND', githubSelector('GITHUB_TRIGGER_CONFIG'));
$scope.$watch('mapped.redis.host', redisSetter('host'));
$scope.$watch('mapped.redis.port', redisSetter('port'));
$scope.$watch('mapped.redis.password', redisSetter('password'));
// Add a watch to remove any fields not allowed by the current storage configuration.
// We have to do this otherwise extra fields (which are not allowed) can end up in the
// configuration.
$scope.$watch('config.DISTRIBUTED_STORAGE_CONFIG.local[0]', function(value) {
// Remove any fields not associated with the current kind.
if (!value || !$scope.STORAGE_CONFIG_FIELDS[value]
|| !$scope.config.DISTRIBUTED_STORAGE_CONFIG
|| !$scope.config.DISTRIBUTED_STORAGE_CONFIG.local
|| !$scope.config.DISTRIBUTED_STORAGE_CONFIG.local[1]) { return; }
var allowedFields = $scope.STORAGE_CONFIG_FIELDS[value];
var configObject = $scope.config.DISTRIBUTED_STORAGE_CONFIG.local[1];
// Remove any fields not allowed.
for (var fieldName in configObject) {
if (!configObject.hasOwnProperty(fieldName)) {
continue;
}
var isValidField = $.grep(allowedFields, function(field) {
return field.name == fieldName;
}).length > 0;
if (!isValidField) {
delete configObject[fieldName];
}
}
// Set any boolean fields to false.
for (var i = 0; i < allowedFields.length; ++i) {
if (allowedFields[i].kind == 'bool') {
configObject[allowedFields[i].name] = false;
}
}
});
$scope.$watch('config', function(value) {
$scope.mapped['$hasChanges'] = true;
}, true);
$scope.$watch('isActive', function(value) {
if (!value) { return; }
ApiService.scGetConfig().then(function(resp) {
$scope.config = resp['config'] || {};
initializeMappedLogic($scope.config);
$scope.mapped['$hasChanges'] = false;
}, ApiService.errorDisplay('Could not load config'));
});
}
};
return directiveDefinitionObject;
})
.directive('configParsedField', function ($timeout) {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/config/config-parsed-field.html',
replace: false,
transclude: true,
restrict: 'C',
scope: {
'binding': '=binding',
'parser': '&parser',
'serializer': '&serializer'
},
controller: function($scope, $element, $transclude) {
$scope.childScope = null;
$transclude(function(clone, scope) {
$scope.childScope = scope;
$scope.childScope['fields'] = {};
$element.append(clone);
});
$scope.childScope.$watch('fields', function(value) {
// Note: We need the timeout here because Angular starts the digest of the
// parent scope AFTER the child scope, which means it can end up one action
// behind. The timeout ensures that the parent scope will be fully digest-ed
// and then we update the binding. Yes, this is a hack :-/.
$timeout(function() {
$scope.binding = $scope.serializer({'fields': value});
});
}, true);
$scope.$watch('binding', function(value) {
var parsed = $scope.parser({'value': value});
for (var key in parsed) {
if (parsed.hasOwnProperty(key)) {
$scope.childScope['fields'][key] = parsed[key];
}
}
});
}
};
return directiveDefinitionObject;
})
.directive('configVariableField', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/config/config-variable-field.html',
replace: false,
transclude: true,
restrict: 'C',
scope: {
'binding': '=binding'
},
controller: function($scope, $element) {
$scope.sections = {};
$scope.currentSection = null;
$scope.setSection = function(section) {
$scope.binding = section.value;
};
this.addSection = function(section, element) {
$scope.sections[section.value] = {
'title': section.valueTitle,
'value': section.value,
'element': element
};
element.hide();
if (!$scope.binding) {
$scope.binding = section.value;
}
};
$scope.$watch('binding', function(binding) {
if (!binding) { return; }
if ($scope.currentSection) {
$scope.currentSection.element.hide();
}
if ($scope.sections[binding]) {
$scope.sections[binding].element.show();
$scope.currentSection = $scope.sections[binding];
}
});
}
};
return directiveDefinitionObject;
})
.directive('variableSection', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/config/config-variable-field.html',
priority: 1,
require: '^configVariableField',
replace: false,
transclude: true,
restrict: 'C',
scope: {
'value': '@value',
'valueTitle': '@valueTitle'
},
controller: function($scope, $element) {
var parentCtrl = $element.parent().controller('configVariableField');
parentCtrl.addSection($scope, $element);
}
};
return directiveDefinitionObject;
})
.directive('configListField', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/config/config-list-field.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
'binding': '=binding',
'placeholder': '@placeholder',
'defaultValue': '@defaultValue',
'itemTitle': '@itemTitle'
},
controller: function($scope, $element) {
$scope.removeItem = function(item) {
var index = $scope.binding.indexOf(item);
if (index >= 0) {
$scope.binding.splice(index, 1);
}
};
$scope.addItem = function() {
if (!$scope.newItemName) {
return;
}
if (!$scope.binding) {
$scope.binding = [];
}
if ($scope.binding.indexOf($scope.newItemName) >= 0) {
return;
}
$scope.binding.push($scope.newItemName);
$scope.newItemName = null;
};
$scope.$watch('binding', function(binding) {
if (!binding && $scope.defaultValue) {
$scope.binding = eval($scope.defaultValue);
}
});
}
};
return directiveDefinitionObject;
})
.directive('configFileField', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/config/config-file-field.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
'filename': '@filename'
},
controller: function($scope, $element, Restangular, $upload) {
$scope.hasFile = false;
$scope.onFileSelect = function(files) {
if (files.length < 1) { return; }
$scope.uploadProgress = 0;
$scope.upload = $upload.upload({
url: '/api/v1/superuser/config/file/' + $scope.filename,
method: 'POST',
data: {'_csrf_token': window.__token},
file: files[0],
}).progress(function(evt) {
$scope.uploadProgress = parseInt(100.0 * evt.loaded / evt.total);
if ($scope.uploadProgress == 100) {
$scope.uploadProgress = null;
$scope.hasFile = true;
}
}).success(function(data, status, headers, config) {
$scope.uploadProgress = null;
$scope.hasFile = true;
});
};
var loadStatus = function(filename) {
Restangular.one('superuser/config/file/' + filename).get().then(function(resp) {
$scope.hasFile = resp['exists'];
});
};
if ($scope.filename) {
loadStatus($scope.filename);
}
}
};
return directiveDefinitionObject;
})
.directive('configBoolField', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/config/config-bool-field.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
'binding': '=binding'
},
controller: function($scope, $element) {
}
};
return directiveDefinitionObject;
})
.directive('configNumericField', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/config/config-numeric-field.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
'binding': '=binding',
'placeholder': '@placeholder',
'defaultValue': '@defaultValue'
},
controller: function($scope, $element) {
$scope.bindinginternal = 0;
$scope.$watch('binding', function(binding) {
if ($scope.binding == 0 && $scope.defaultValue) {
$scope.binding = $scope.defaultValue * 1;
}
$scope.bindinginternal = $scope.binding;
});
$scope.$watch('bindinginternal', function(binding) {
var newValue = $scope.bindinginternal * 1;
if (isNaN(newValue)) {
newValue = 0;
}
$scope.binding = newValue;
});
}
};
return directiveDefinitionObject;
})
.directive('configContactsField', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/config/config-contacts-field.html',
priority: 1,
replace: false,
transclude: false,
restrict: 'C',
scope: {
'binding': '=binding'
},
controller: function($scope, $element) {
var padItems = function(items) {
// Remove the last item if both it and the second to last items are empty.
if (items.length > 1 && !items[items.length - 2].value && !items[items.length - 1].value) {
items.splice(items.length - 1, 1);
return;
}
// If the last item is non-empty, add a new item.
if (items.length == 0 || items[items.length - 1].value) {
items.push({'value': ''});
return;
}
};
$scope.itemHash = null;
$scope.$watch('items', function(items) {
if (!items) { return; }
padItems(items);
var itemHash = '';
var binding = [];
for (var i = 0; i < items.length; ++i) {
var item = items[i];
if (item.value && (URI(item.value).host() || URI(item.value).path())) {
binding.push(item.value);
itemHash += item.value;
}
}
$scope.itemHash = itemHash;
$scope.binding = binding;
}, true);
$scope.$watch('binding', function(binding) {
var current = binding || [];
var items = [];
var itemHash = '';
for (var i = 0; i < current.length; ++i) {
items.push({'value': current[i]})
itemHash += current[i];
}
if ($scope.itemHash != itemHash) {
$scope.items = items;
}
});
}
};
return directiveDefinitionObject;
})
.directive('configContactField', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/config/config-contact-field.html',
priority: 1,
replace: false,
transclude: true,
restrict: 'C',
scope: {
'binding': '=binding'
},
controller: function($scope, $element) {
$scope.kind = null;
$scope.value = null;
var updateBinding = function() {
if ($scope.value == null) { return; }
var value = $scope.value || '';
switch ($scope.kind) {
case 'mailto':
$scope.binding = 'mailto:' + value;
return;
case 'tel':
$scope.binding = 'tel:' + value;
return;
case 'irc':
$scope.binding = 'irc://' + value;
return;
default:
$scope.binding = value;
return;
}
};
$scope.$watch('kind', updateBinding);
$scope.$watch('value', updateBinding);
$scope.$watch('binding', function(value) {
if (!value) {
$scope.kind = null;
$scope.value = null;
return;
}
var uri = URI(value);
$scope.kind = uri.scheme();
switch ($scope.kind) {
case 'mailto':
case 'tel':
$scope.value = uri.path();
break;
case 'irc':
$scope.value = value.substr('irc://'.length);
break;
default:
$scope.kind = 'http';
$scope.value = value;
break;
}
});
$scope.getPlaceholder = function(kind) {
switch (kind) {
case 'mailto':
return 'some@example.com';
case 'tel':
return '555-555-5555';
case 'irc':
return 'myserver:port/somechannel';
default:
return 'http://some/url';
}
};
}
};
return directiveDefinitionObject;
})
.directive('configStringField', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/config/config-string-field.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
'binding': '=binding',
'placeholder': '@placeholder',
'pattern': '@pattern',
'defaultValue': '@defaultValue',
'validator': '&validator'
},
controller: function($scope, $element) {
$scope.getRegexp = function(pattern) {
if (!pattern) {
pattern = '.*';
}
return new RegExp(pattern);
};
$scope.$watch('binding', function(binding) {
if (!binding && $scope.defaultValue) {
$scope.binding = $scope.defaultValue;
}
$scope.errorMessage = $scope.validator({'value': binding || ''});
});
}
};
return directiveDefinitionObject;
});

568
static/js/core-ui.js Normal file
View file

@ -0,0 +1,568 @@
angular.module("core-ui", [])
.factory('CoreDialog', [function() {
var service = {};
service['fatal'] = function(title, message) {
bootbox.dialog({
"title": title,
"message": "<div class='alert-icon-container-container'><div class='alert-icon-container'><div class='alert-icon'></div></div></div>" + message,
"buttons": {},
"className": "co-dialog fatal-error",
"closeButton": false
});
};
return service;
}])
.directive('corLogBox', function() {
var directiveDefinitionObject = {
priority: 1,
templateUrl: '/static/directives/cor-log-box.html',
replace: true,
transclude: true,
restrict: 'C',
scope: {
'logs': '=logs'
},
controller: function($rootScope, $scope, $element, $timeout) {
$scope.hasNewLogs = false;
var scrollHandlerBound = false;
var isAnimatedScrolling = false;
var isScrollBottom = true;
var scrollHandler = function() {
if (isAnimatedScrolling) { return; }
var element = $element.find("#co-log-viewer")[0];
isScrollBottom = element.scrollHeight - element.scrollTop === element.clientHeight;
if (isScrollBottom) {
$scope.hasNewLogs = false;
}
};
var animateComplete = function() {
isAnimatedScrolling = false;
};
$scope.moveToBottom = function() {
$scope.hasNewLogs = false;
isAnimatedScrolling = true;
isScrollBottom = true;
$element.find("#co-log-viewer").animate(
{ scrollTop: $element.find("#co-log-content").height() }, "slow", null, animateComplete);
};
$scope.$watch('logs', function(value, oldValue) {
if (!value) { return; }
$timeout(function() {
if (!scrollHandlerBound) {
$element.find("#co-log-viewer").on('scroll', scrollHandler);
scrollHandlerBound = true;
}
if (!isScrollBottom) {
$scope.hasNewLogs = true;
return;
}
$scope.moveToBottom();
}, 500);
});
}
};
return directiveDefinitionObject;
})
.directive('corOptionsMenu', function() {
var directiveDefinitionObject = {
priority: 1,
templateUrl: '/static/directives/cor-options-menu.html',
replace: true,
transclude: true,
restrict: 'C',
scope: {},
controller: function($rootScope, $scope, $element) {
}
};
return directiveDefinitionObject;
})
.directive('corOption', function() {
var directiveDefinitionObject = {
priority: 1,
templateUrl: '/static/directives/cor-option.html',
replace: true,
transclude: true,
restrict: 'C',
scope: {
'optionClick': '&optionClick'
},
controller: function($rootScope, $scope, $element) {
}
};
return directiveDefinitionObject;
})
.directive('corTitle', function() {
var directiveDefinitionObject = {
priority: 1,
templateUrl: '/static/directives/cor-title.html',
replace: true,
transclude: true,
restrict: 'C',
scope: {},
controller: function($rootScope, $scope, $element) {
}
};
return directiveDefinitionObject;
})
.directive('corTitleContent', function() {
var directiveDefinitionObject = {
priority: 1,
templateUrl: '/static/directives/cor-title-content.html',
replace: true,
transclude: true,
restrict: 'C',
scope: {},
controller: function($rootScope, $scope, $element) {
}
};
return directiveDefinitionObject;
})
.directive('corTitleLink', function() {
var directiveDefinitionObject = {
priority: 1,
templateUrl: '/static/directives/cor-title-link.html',
replace: true,
transclude: true,
restrict: 'C',
scope: {},
controller: function($rootScope, $scope, $element) {
}
};
return directiveDefinitionObject;
})
.directive('corTabPanel', function() {
var directiveDefinitionObject = {
priority: 1,
templateUrl: '/static/directives/cor-tab-panel.html',
replace: true,
transclude: true,
restrict: 'C',
scope: {},
controller: function($rootScope, $scope, $element) {
}
};
return directiveDefinitionObject;
})
.directive('corTabContent', function() {
var directiveDefinitionObject = {
priority: 2,
templateUrl: '/static/directives/cor-tab-content.html',
replace: true,
transclude: true,
restrict: 'C',
scope: {},
controller: function($rootScope, $scope, $element) {
}
};
return directiveDefinitionObject;
})
.directive('corTabs', function() {
var directiveDefinitionObject = {
priority: 3,
templateUrl: '/static/directives/cor-tabs.html',
replace: true,
transclude: true,
restrict: 'C',
scope: {},
controller: function($rootScope, $scope, $element) {
}
};
return directiveDefinitionObject;
})
.directive('corFloatingBottomBar', function() {
var directiveDefinitionObject = {
priority: 3,
templateUrl: '/static/directives/cor-floating-bottom-bar.html',
replace: true,
transclude: true,
restrict: 'C',
scope: {},
controller: function($rootScope, $scope, $element, $timeout, $interval) {
var handler = function() {
$element.removeClass('floating');
$element.css('width', $element[0].parentNode.clientWidth + 'px');
var windowHeight = $(window).height();
var rect = $element[0].getBoundingClientRect();
if (rect.bottom > windowHeight) {
$element.addClass('floating');
}
};
$(window).on("scroll", handler);
$(window).on("resize", handler);
var previousHeight = $element[0].parentNode.clientHeight;
var stop = $interval(function() {
var currentHeight = $element[0].parentNode.clientWidth;
if (previousHeight != currentHeight) {
currentHeight = previousHeight;
handler();
}
}, 100);
$scope.$on('$destroy', function() {
$(window).off("resize", handler);
$(window).off("scroll", handler);
$interval.cancel(stop);
});
}
};
return directiveDefinitionObject;
})
.directive('corLoaderInline', function() {
var directiveDefinitionObject = {
templateUrl: '/static/directives/cor-loader-inline.html',
replace: true,
restrict: 'C',
scope: {
},
controller: function($rootScope, $scope, $element) {
}
};
return directiveDefinitionObject;
})
.directive('corLoader', function() {
var directiveDefinitionObject = {
templateUrl: '/static/directives/cor-loader.html',
replace: true,
restrict: 'C',
scope: {
},
controller: function($rootScope, $scope, $element) {
}
};
return directiveDefinitionObject;
})
.directive('corTab', function() {
var directiveDefinitionObject = {
priority: 4,
templateUrl: '/static/directives/cor-tab.html',
replace: true,
transclude: true,
restrict: 'C',
scope: {
'tabActive': '@tabActive',
'tabTitle': '@tabTitle',
'tabTarget': '@tabTarget',
'tabInit': '&tabInit',
'tabShown': '&tabShown',
'tabHidden': '&tabHidden'
},
controller: function($rootScope, $scope, $element) {
$element.find('a[data-toggle="tab"]').on('hidden.bs.tab', function (e) {
$scope.tabHidden({});
});
$element.find('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
$scope.tabShown({});
});
}
};
return directiveDefinitionObject;
})
.directive('corStep', function() {
var directiveDefinitionObject = {
priority: 4,
templateUrl: '/static/directives/cor-step.html',
replace: true,
transclude: false,
requires: '^corStepBar',
restrict: 'C',
scope: {
'icon': '@icon',
'title': '@title',
'text': '@text'
},
controller: function($rootScope, $scope, $element) {
}
};
return directiveDefinitionObject;
})
.directive('realtimeAreaChart', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/realtime-area-chart.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
'data': '=data',
'labels': '=labels',
'colors': '=colors',
'counter': '=counter'
},
controller: function($scope, $element) {
var graph = null;
var series = [];
var palette = new Rickshaw.Color.Palette( { scheme: 'spectrum14' } );
var colors = $scope.colors || [];
var setupGraph = function() {
for (var i = 0; i < $scope.labels.length; ++i) {
series.push({
name: $scope.labels[i],
color: i >= colors.length ? palette.color(): $scope.colors[i],
stroke: 'rgba(0,0,0,0.15)',
data: []
});
}
var options = {
element: $element.find('.chart')[0],
renderer: 'area',
stroke: true,
series: series,
min: 0,
padding: {
'top': 0.3,
'left': 0,
'right': 0,
'bottom': 0.3
}
};
if ($scope.minimum != null) {
options['min'] = $scope.minimum == 'auto' ? 'auto' : $scope.minimum * 1;
} else {
options['min'] = 0;
}
if ($scope.maximum != null) {
options['max'] = $scope.maximum == 'auto' ? 'auto' : $scope.maximum * 1;
}
graph = new Rickshaw.Graph(options);
xaxes = new Rickshaw.Graph.Axis.Time({
graph: graph,
timeFixture: new Rickshaw.Fixtures.Time.Local()
});
yaxes = new Rickshaw.Graph.Axis.Y({
graph: graph,
tickFormat: Rickshaw.Fixtures.Number.formatKMBT
});
hoverDetail = new Rickshaw.Graph.HoverDetail({
graph: graph,
xFormatter: function(x) {
return new Date(x * 1000).toString();
}
});
};
var refresh = function(data) {
if (!data || $scope.counter < 0) { return; }
if (!graph) {
setupGraph();
}
var timecode = new Date().getTime() / 1000;
for (var i = 0; i < $scope.data.length; ++i) {
var arr = series[i].data;
arr.push(
{'x': timecode, 'y': $scope.data[i] }
);
if (arr.length > 10) {
series[i].data = arr.slice(arr.length - 10, arr.length);
}
}
graph.renderer.unstack = true;
graph.update();
};
$scope.$watch('counter', function() {
refresh($scope.data_raw);
});
$scope.$watch('data', function(data) {
$scope.data_raw = data;
refresh($scope.data_raw);
});
}
};
return directiveDefinitionObject;
})
.directive('realtimeLineChart', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/realtime-line-chart.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
'data': '=data',
'labels': '=labels',
'counter': '=counter',
'labelTemplate': '@labelTemplate',
'minimum': '@minimum',
'maximum': '@maximum'
},
controller: function($scope, $element) {
var graph = null;
var xaxes = null;
var yaxes = null;
var hoverDetail = null;
var series = [];
var counter = 0;
var palette = new Rickshaw.Color.Palette( { scheme: 'spectrum14' } );
var setupGraph = function() {
var options = {
element: $element.find('.chart')[0],
renderer: 'line',
series: series,
padding: {
'top': 0.3,
'left': 0,
'right': 0,
'bottom': 0.3
}
};
if ($scope.minimum != null) {
options['min'] = $scope.minimum == 'auto' ? 'auto' : $scope.minimum * 1;
} else {
options['min'] = 0;
}
if ($scope.maximum != null) {
options['max'] = $scope.maximum == 'auto' ? 'auto' : $scope.maximum * 1;
}
graph = new Rickshaw.Graph(options);
xaxes = new Rickshaw.Graph.Axis.Time({
graph: graph,
timeFixture: new Rickshaw.Fixtures.Time.Local()
});
yaxes = new Rickshaw.Graph.Axis.Y({
graph: graph,
tickFormat: Rickshaw.Fixtures.Number.formatKMBT
});
hoverDetail = new Rickshaw.Graph.HoverDetail({
graph: graph,
xFormatter: function(x) {
return new Date(x * 1000).toString();
}
});
};
var refresh = function(data) {
if (data == null) { return; }
if (!graph) {
setupGraph();
}
if (typeof data == 'number') {
data = [data];
}
if ($scope.labels) {
data = data.slice(0, $scope.labels.length);
}
if (series.length == 0){
for (var i = 0; i < data.length; ++i) {
var title = $scope.labels ? $scope.labels[i] : $scope.labelTemplate.replace('{x}', i + 1);
series.push({
'color': palette.color(),
'data': [],
'name': title
})
}
}
counter++;
var timecode = new Date().getTime() / 1000;
for (var i = 0; i < data.length; ++i) {
var arr = series[i].data;
arr.push({
'x': timecode,
'y': data[i]
})
if (arr.length > 10) {
series[i].data = arr.slice(arr.length - 10, arr.length);
}
}
graph.update();
};
$scope.$watch('counter', function(counter) {
refresh($scope.data_raw);
});
$scope.$watch('data', function(data) {
$scope.data_raw = data;
});
}
};
return directiveDefinitionObject;
})
.directive('corStepBar', function() {
var directiveDefinitionObject = {
priority: 4,
templateUrl: '/static/directives/cor-step-bar.html',
replace: true,
transclude: true,
restrict: 'C',
scope: {
'progress': '=progress'
},
controller: function($rootScope, $scope, $element) {
$scope.$watch('progress', function(progress) {
var index = 0;
for (var i = 0; i < progress.length; ++i) {
if (progress[i]) {
index = i;
}
}
$element.find('.transclude').children('.co-step-element').each(function(i, elem) {
$(elem).removeClass('active');
if (i <= index) {
$(elem).addClass('active');
}
});
});
}
};
return directiveDefinitionObject;
});

View file

@ -485,6 +485,9 @@ ImageHistoryTree.prototype.buildRoot_ = function() {
maxChildHeight = Math.max(maxChildHeight, key);
});
// Recursively prune the nodes that are not referenced by a tag
this.pruneUnreferenced_(root);
// Compact the graph so that any single chain of three (or more) images becomes a collapsed
// section. We only do this if the max width is > 1 (since for a single width tree, no long
// chain will hide a branch).
@ -505,6 +508,23 @@ ImageHistoryTree.prototype.buildRoot_ = function() {
};
/**
* Prunes images which are not referenced either directly or indirectly by any tag.
*/
ImageHistoryTree.prototype.pruneUnreferenced_ = function(node) {
if (node.children) {
var surviving_children = []
for (var i = 0; i < node.children.length; ++i) {
if (!this.pruneUnreferenced_(node.children[i])) {
surviving_children.push(node.children[i]);
}
}
node.children = surviving_children;
}
return (node.children.length == 0 && node.tags.length == 0);
};
/**
* Determines the height of the tree at its longest chain.
*/

View file

@ -19,6 +19,7 @@ typeahead - Permissive (https://github.com/twitter/typeahead.js/blob/master/LICE
zlib - MIT (https://github.com/imaya/zlib.js)
pagedown - Permissive
jquery.overscroll - MIT (https://github.com/azoff/overscroll/blob/master/mit.license)
URI.js - MIT (https://github.com/medialize/URI.js)
Issues:
>>>>> jquery.spotlight - GPLv3 (https://github.com/jameshalsall/jQuery-Spotlight)

78
static/lib/URI.min.js vendored Normal file
View file

@ -0,0 +1,78 @@
(function(f,l){"object"===typeof exports?module.exports=l():"function"===typeof define&&define.amd?define(l):f.IPv6=l(f)})(this,function(f){var l=f&&f.IPv6;return{best:function(g){g=g.toLowerCase().split(":");var m=g.length,b=8;""===g[0]&&""===g[1]&&""===g[2]?(g.shift(),g.shift()):""===g[0]&&""===g[1]?g.shift():""===g[m-1]&&""===g[m-2]&&g.pop();m=g.length;-1!==g[m-1].indexOf(".")&&(b=7);var k;for(k=0;k<m&&""!==g[k];k++);if(k<b)for(g.splice(k,1,"0000");g.length<b;)g.splice(k,0,"0000");for(k=0;k<b;k++){for(var m=
g[k].split(""),f=0;3>f;f++)if("0"===m[0]&&1<m.length)m.splice(0,1);else break;g[k]=m.join("")}var m=-1,l=f=0,h=-1,r=!1;for(k=0;k<b;k++)r?"0"===g[k]?l+=1:(r=!1,l>f&&(m=h,f=l)):"0"===g[k]&&(r=!0,h=k,l=1);l>f&&(m=h,f=l);1<f&&g.splice(m,f,"");m=g.length;b="";""===g[0]&&(b=":");for(k=0;k<m;k++){b+=g[k];if(k===m-1)break;b+=":"}""===g[m-1]&&(b+=":");return b},noConflict:function(){f.IPv6===this&&(f.IPv6=l);return this}}});(function(f){function l(b){throw RangeError(w[b]);}function g(b,e){for(var h=b.length;h--;)b[h]=e(b[h]);return b}function m(b,e){return g(b.split(v),e).join(".")}function b(b){for(var e=[],h=0,a=b.length,c,d;h<a;)c=b.charCodeAt(h++),55296<=c&&56319>=c&&h<a?(d=b.charCodeAt(h++),56320==(d&64512)?e.push(((c&1023)<<10)+(d&1023)+65536):(e.push(c),h--)):e.push(c);return e}function k(b){return g(b,function(b){var e="";65535<b&&(b-=65536,e+=B(b>>>10&1023|55296),b=56320|b&1023);return e+=B(b)}).join("")}function z(b,
e){return b+22+75*(26>b)-((0!=e)<<5)}function p(b,e,h){var a=0;b=h?q(b/700):b>>1;for(b+=q(b/e);455<b;a+=36)b=q(b/35);return q(a+36*b/(b+38))}function h(b){var e=[],h=b.length,a,c=0,d=128,u=72,x,y,g,f,m;x=b.lastIndexOf("-");0>x&&(x=0);for(y=0;y<x;++y)128<=b.charCodeAt(y)&&l("not-basic"),e.push(b.charCodeAt(y));for(x=0<x?x+1:0;x<h;){y=c;a=1;for(g=36;;g+=36){x>=h&&l("invalid-input");f=b.charCodeAt(x++);f=10>f-48?f-22:26>f-65?f-65:26>f-97?f-97:36;(36<=f||f>q((2147483647-c)/a))&&l("overflow");c+=f*a;m=
g<=u?1:g>=u+26?26:g-u;if(f<m)break;f=36-m;a>q(2147483647/f)&&l("overflow");a*=f}a=e.length+1;u=p(c-y,a,0==y);q(c/a)>2147483647-d&&l("overflow");d+=q(c/a);c%=a;e.splice(c++,0,d)}return k(e)}function r(e){var h,g,a,c,d,u,x,y,f,m=[],r,k,n;e=b(e);r=e.length;h=128;g=0;d=72;for(u=0;u<r;++u)f=e[u],128>f&&m.push(B(f));for((a=c=m.length)&&m.push("-");a<r;){x=2147483647;for(u=0;u<r;++u)f=e[u],f>=h&&f<x&&(x=f);k=a+1;x-h>q((2147483647-g)/k)&&l("overflow");g+=(x-h)*k;h=x;for(u=0;u<r;++u)if(f=e[u],f<h&&2147483647<
++g&&l("overflow"),f==h){y=g;for(x=36;;x+=36){f=x<=d?1:x>=d+26?26:x-d;if(y<f)break;n=y-f;y=36-f;m.push(B(z(f+n%y,0)));y=q(n/y)}m.push(B(z(y,0)));d=p(g,k,a==c);g=0;++a}++g;++h}return m.join("")}var C="object"==typeof exports&&exports,D="object"==typeof module&&module&&module.exports==C&&module,A="object"==typeof global&&global;if(A.global===A||A.window===A)f=A;var t,n=/^xn--/,e=/[^ -~]/,v=/\x2E|\u3002|\uFF0E|\uFF61/g,w={overflow:"Overflow: input needs wider integers to process","not-basic":"Illegal input >= 0x80 (not a basic code point)",
"invalid-input":"Invalid input"},q=Math.floor,B=String.fromCharCode,E;t={version:"1.2.3",ucs2:{decode:b,encode:k},decode:h,encode:r,toASCII:function(b){return m(b,function(b){return e.test(b)?"xn--"+r(b):b})},toUnicode:function(b){return m(b,function(b){return n.test(b)?h(b.slice(4).toLowerCase()):b})}};if("function"==typeof define&&"object"==typeof define.amd&&define.amd)define(function(){return t});else if(C&&!C.nodeType)if(D)D.exports=t;else for(E in t)t.hasOwnProperty(E)&&(C[E]=t[E]);else f.punycode=
t})(this);(function(f,l){"object"===typeof exports?module.exports=l():"function"===typeof define&&define.amd?define(l):f.SecondLevelDomains=l(f)})(this,function(f){var l=f&&f.SecondLevelDomains,g={list:{ac:" com gov mil net org ",ae:" ac co gov mil name net org pro sch ",af:" com edu gov net org ",al:" com edu gov mil net org ",ao:" co ed gv it og pb ",ar:" com edu gob gov int mil net org tur ",at:" ac co gv or ",au:" asn com csiro edu gov id net org ",ba:" co com edu gov mil net org rs unbi unmo unsa untz unze ",
bb:" biz co com edu gov info net org store tv ",bh:" biz cc com edu gov info net org ",bn:" com edu gov net org ",bo:" com edu gob gov int mil net org tv ",br:" adm adv agr am arq art ato b bio blog bmd cim cng cnt com coop ecn edu eng esp etc eti far flog fm fnd fot fst g12 ggf gov imb ind inf jor jus lel mat med mil mus net nom not ntr odo org ppg pro psc psi qsl rec slg srv tmp trd tur tv vet vlog wiki zlg ",bs:" com edu gov net org ",bz:" du et om ov rg ",ca:" ab bc mb nb nf nl ns nt nu on pe qc sk yk ",
ck:" biz co edu gen gov info net org ",cn:" ac ah bj com cq edu fj gd gov gs gx gz ha hb he hi hl hn jl js jx ln mil net nm nx org qh sc sd sh sn sx tj tw xj xz yn zj ",co:" com edu gov mil net nom org ",cr:" ac c co ed fi go or sa ",cy:" ac biz com ekloges gov ltd name net org parliament press pro tm ","do":" art com edu gob gov mil net org sld web ",dz:" art asso com edu gov net org pol ",ec:" com edu fin gov info med mil net org pro ",eg:" com edu eun gov mil name net org sci ",er:" com edu gov ind mil net org rochest w ",
es:" com edu gob nom org ",et:" biz com edu gov info name net org ",fj:" ac biz com info mil name net org pro ",fk:" ac co gov net nom org ",fr:" asso com f gouv nom prd presse tm ",gg:" co net org ",gh:" com edu gov mil org ",gn:" ac com gov net org ",gr:" com edu gov mil net org ",gt:" com edu gob ind mil net org ",gu:" com edu gov net org ",hk:" com edu gov idv net org ",hu:" 2000 agrar bolt casino city co erotica erotika film forum games hotel info ingatlan jogasz konyvelo lakas media news org priv reklam sex shop sport suli szex tm tozsde utazas video ",
id:" ac co go mil net or sch web ",il:" ac co gov idf k12 muni net org ","in":" ac co edu ernet firm gen gov i ind mil net nic org res ",iq:" com edu gov i mil net org ",ir:" ac co dnssec gov i id net org sch ",it:" edu gov ",je:" co net org ",jo:" com edu gov mil name net org sch ",jp:" ac ad co ed go gr lg ne or ",ke:" ac co go info me mobi ne or sc ",kh:" com edu gov mil net org per ",ki:" biz com de edu gov info mob net org tel ",km:" asso com coop edu gouv k medecin mil nom notaires pharmaciens presse tm veterinaire ",
kn:" edu gov net org ",kr:" ac busan chungbuk chungnam co daegu daejeon es gangwon go gwangju gyeongbuk gyeonggi gyeongnam hs incheon jeju jeonbuk jeonnam k kg mil ms ne or pe re sc seoul ulsan ",kw:" com edu gov net org ",ky:" com edu gov net org ",kz:" com edu gov mil net org ",lb:" com edu gov net org ",lk:" assn com edu gov grp hotel int ltd net ngo org sch soc web ",lr:" com edu gov net org ",lv:" asn com conf edu gov id mil net org ",ly:" com edu gov id med net org plc sch ",ma:" ac co gov m net org press ",
mc:" asso tm ",me:" ac co edu gov its net org priv ",mg:" com edu gov mil nom org prd tm ",mk:" com edu gov inf name net org pro ",ml:" com edu gov net org presse ",mn:" edu gov org ",mo:" com edu gov net org ",mt:" com edu gov net org ",mv:" aero biz com coop edu gov info int mil museum name net org pro ",mw:" ac co com coop edu gov int museum net org ",mx:" com edu gob net org ",my:" com edu gov mil name net org sch ",nf:" arts com firm info net other per rec store web ",ng:" biz com edu gov mil mobi name net org sch ",
ni:" ac co com edu gob mil net nom org ",np:" com edu gov mil net org ",nr:" biz com edu gov info net org ",om:" ac biz co com edu gov med mil museum net org pro sch ",pe:" com edu gob mil net nom org sld ",ph:" com edu gov i mil net ngo org ",pk:" biz com edu fam gob gok gon gop gos gov net org web ",pl:" art bialystok biz com edu gda gdansk gorzow gov info katowice krakow lodz lublin mil net ngo olsztyn org poznan pwr radom slupsk szczecin torun warszawa waw wroc wroclaw zgora ",pr:" ac biz com edu est gov info isla name net org pro prof ",
ps:" com edu gov net org plo sec ",pw:" belau co ed go ne or ",ro:" arts com firm info nom nt org rec store tm www ",rs:" ac co edu gov in org ",sb:" com edu gov net org ",sc:" com edu gov net org ",sh:" co com edu gov net nom org ",sl:" com edu gov net org ",st:" co com consulado edu embaixada gov mil net org principe saotome store ",sv:" com edu gob org red ",sz:" ac co org ",tr:" av bbs bel biz com dr edu gen gov info k12 name net org pol tel tsk tv web ",tt:" aero biz cat co com coop edu gov info int jobs mil mobi museum name net org pro tel travel ",
tw:" club com ebiz edu game gov idv mil net org ",mu:" ac co com gov net or org ",mz:" ac co edu gov org ",na:" co com ",nz:" ac co cri geek gen govt health iwi maori mil net org parliament school ",pa:" abo ac com edu gob ing med net nom org sld ",pt:" com edu gov int net nome org publ ",py:" com edu gov mil net org ",qa:" com edu gov mil net org ",re:" asso com nom ",ru:" ac adygeya altai amur arkhangelsk astrakhan bashkiria belgorod bir bryansk buryatia cbg chel chelyabinsk chita chukotka chuvashia com dagestan e-burg edu gov grozny int irkutsk ivanovo izhevsk jar joshkar-ola kalmykia kaluga kamchatka karelia kazan kchr kemerovo khabarovsk khakassia khv kirov koenig komi kostroma kranoyarsk kuban kurgan kursk lipetsk magadan mari mari-el marine mil mordovia mosreg msk murmansk nalchik net nnov nov novosibirsk nsk omsk orenburg org oryol penza perm pp pskov ptz rnd ryazan sakhalin samara saratov simbirsk smolensk spb stavropol stv surgut tambov tatarstan tom tomsk tsaritsyn tsk tula tuva tver tyumen udm udmurtia ulan-ude vladikavkaz vladimir vladivostok volgograd vologda voronezh vrn vyatka yakutia yamal yekaterinburg yuzhno-sakhalinsk ",
rw:" ac co com edu gouv gov int mil net ",sa:" com edu gov med net org pub sch ",sd:" com edu gov info med net org tv ",se:" a ac b bd c d e f g h i k l m n o org p parti pp press r s t tm u w x y z ",sg:" com edu gov idn net org per ",sn:" art com edu gouv org perso univ ",sy:" com edu gov mil net news org ",th:" ac co go in mi net or ",tj:" ac biz co com edu go gov info int mil name net nic org test web ",tn:" agrinet com defense edunet ens fin gov ind info intl mincom nat net org perso rnrt rns rnu tourism ",
tz:" ac co go ne or ",ua:" biz cherkassy chernigov chernovtsy ck cn co com crimea cv dn dnepropetrovsk donetsk dp edu gov if in ivano-frankivsk kh kharkov kherson khmelnitskiy kiev kirovograd km kr ks kv lg lugansk lutsk lviv me mk net nikolaev od odessa org pl poltava pp rovno rv sebastopol sumy te ternopil uzhgorod vinnica vn zaporizhzhe zhitomir zp zt ",ug:" ac co go ne or org sc ",uk:" ac bl british-library co cym gov govt icnet jet lea ltd me mil mod national-library-scotland nel net nhs nic nls org orgn parliament plc police sch scot soc ",
us:" dni fed isa kids nsn ",uy:" com edu gub mil net org ",ve:" co com edu gob info mil net org web ",vi:" co com k12 net org ",vn:" ac biz com edu gov health info int name net org pro ",ye:" co com gov ltd me net org plc ",yu:" ac co edu gov org ",za:" ac agric alt bourse city co cybernet db edu gov grondar iaccess imt inca landesign law mil net ngo nis nom olivetti org pix school tm web ",zm:" ac co com edu gov net org sch "},has:function(f){var b=f.lastIndexOf(".");if(0>=b||b>=f.length-1)return!1;
var k=f.lastIndexOf(".",b-1);if(0>=k||k>=b-1)return!1;var l=g.list[f.slice(b+1)];return l?0<=l.indexOf(" "+f.slice(k+1,b)+" "):!1},is:function(f){var b=f.lastIndexOf(".");if(0>=b||b>=f.length-1||0<=f.lastIndexOf(".",b-1))return!1;var k=g.list[f.slice(b+1)];return k?0<=k.indexOf(" "+f.slice(0,b)+" "):!1},get:function(f){var b=f.lastIndexOf(".");if(0>=b||b>=f.length-1)return null;var k=f.lastIndexOf(".",b-1);if(0>=k||k>=b-1)return null;var l=g.list[f.slice(b+1)];return!l||0>l.indexOf(" "+f.slice(k+
1,b)+" ")?null:f.slice(k+1)},noConflict:function(){f.SecondLevelDomains===this&&(f.SecondLevelDomains=l);return this}};return g});(function(f,l){"object"===typeof exports?module.exports=l(require("./punycode"),require("./IPv6"),require("./SecondLevelDomains")):"function"===typeof define&&define.amd?define(["./punycode","./IPv6","./SecondLevelDomains"],l):f.URI=l(f.punycode,f.IPv6,f.SecondLevelDomains,f)})(this,function(f,l,g,m){function b(a,c){if(!(this instanceof b))return new b(a,c);void 0===a&&(a="undefined"!==typeof location?location.href+"":"");this.href(a);return void 0!==c?this.absoluteTo(c):this}function k(a){return a.replace(/([.*+?^=!:${}()|[\]\/\\])/g,
"\\$1")}function z(a){return void 0===a?"Undefined":String(Object.prototype.toString.call(a)).slice(8,-1)}function p(a){return"Array"===z(a)}function h(a,c){var d,b;if(p(c)){d=0;for(b=c.length;d<b;d++)if(!h(a,c[d]))return!1;return!0}var e=z(c);d=0;for(b=a.length;d<b;d++)if("RegExp"===e){if("string"===typeof a[d]&&a[d].match(c))return!0}else if(a[d]===c)return!0;return!1}function r(a,c){if(!p(a)||!p(c)||a.length!==c.length)return!1;a.sort();c.sort();for(var d=0,b=a.length;d<b;d++)if(a[d]!==c[d])return!1;
return!0}function C(a){return escape(a)}function D(a){return encodeURIComponent(a).replace(/[!'()*]/g,C).replace(/\*/g,"%2A")}function A(a){return function(c,d){if(void 0===c)return this._parts[a]||"";this._parts[a]=c||null;this.build(!d);return this}}function t(a,c){return function(d,b){if(void 0===d)return this._parts[a]||"";null!==d&&(d+="",d.charAt(0)===c&&(d=d.substring(1)));this._parts[a]=d;this.build(!b);return this}}var n=m&&m.URI;b.version="1.14.1";var e=b.prototype,v=Object.prototype.hasOwnProperty;
b._parts=function(){return{protocol:null,username:null,password:null,hostname:null,urn:null,port:null,path:null,query:null,fragment:null,duplicateQueryParameters:b.duplicateQueryParameters,escapeQuerySpace:b.escapeQuerySpace}};b.duplicateQueryParameters=!1;b.escapeQuerySpace=!0;b.protocol_expression=/^[a-z][a-z0-9.+-]*$/i;b.idn_expression=/[^a-z0-9\.-]/i;b.punycode_expression=/(xn--)/i;b.ip4_expression=/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/;b.ip6_expression=/^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$/;
b.find_uri_expression=/\b((?:[a-z][\w-]+:(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?\u00ab\u00bb\u201c\u201d\u2018\u2019]))/ig;b.findUri={start:/\b(?:([a-z][a-z0-9.+-]*:\/\/)|www\.)/gi,end:/[\s\r\n]|$/,trim:/[`!()\[\]{};:'".,<>?\u00ab\u00bb\u201c\u201d\u201e\u2018\u2019]+$/};b.defaultPorts={http:"80",https:"443",ftp:"21",gopher:"70",ws:"80",wss:"443"};b.invalid_hostname_characters=
/[^a-zA-Z0-9\.-]/;b.domAttributes={a:"href",blockquote:"cite",link:"href",base:"href",script:"src",form:"action",img:"src",area:"href",iframe:"src",embed:"src",source:"src",track:"src",input:"src",audio:"src",video:"src"};b.getDomAttribute=function(a){if(a&&a.nodeName){var c=a.nodeName.toLowerCase();return"input"===c&&"image"!==a.type?void 0:b.domAttributes[c]}};b.encode=D;b.decode=decodeURIComponent;b.iso8859=function(){b.encode=escape;b.decode=unescape};b.unicode=function(){b.encode=D;b.decode=
decodeURIComponent};b.characters={pathname:{encode:{expression:/%(24|26|2B|2C|3B|3D|3A|40)/ig,map:{"%24":"$","%26":"&","%2B":"+","%2C":",","%3B":";","%3D":"=","%3A":":","%40":"@"}},decode:{expression:/[\/\?#]/g,map:{"/":"%2F","?":"%3F","#":"%23"}}},reserved:{encode:{expression:/%(21|23|24|26|27|28|29|2A|2B|2C|2F|3A|3B|3D|3F|40|5B|5D)/ig,map:{"%3A":":","%2F":"/","%3F":"?","%23":"#","%5B":"[","%5D":"]","%40":"@","%21":"!","%24":"$","%26":"&","%27":"'","%28":"(","%29":")","%2A":"*","%2B":"+","%2C":",",
"%3B":";","%3D":"="}}}};b.encodeQuery=function(a,c){var d=b.encode(a+"");void 0===c&&(c=b.escapeQuerySpace);return c?d.replace(/%20/g,"+"):d};b.decodeQuery=function(a,c){a+="";void 0===c&&(c=b.escapeQuerySpace);try{return b.decode(c?a.replace(/\+/g,"%20"):a)}catch(d){return a}};b.recodePath=function(a){a=(a+"").split("/");for(var c=0,d=a.length;c<d;c++)a[c]=b.encodePathSegment(b.decode(a[c]));return a.join("/")};b.decodePath=function(a){a=(a+"").split("/");for(var c=0,d=a.length;c<d;c++)a[c]=b.decodePathSegment(a[c]);
return a.join("/")};var w={encode:"encode",decode:"decode"},q,B=function(a,c){return function(d){try{return b[c](d+"").replace(b.characters[a][c].expression,function(d){return b.characters[a][c].map[d]})}catch(u){return d}}};for(q in w)b[q+"PathSegment"]=B("pathname",w[q]);b.encodeReserved=B("reserved","encode");b.parse=function(a,c){var d;c||(c={});d=a.indexOf("#");-1<d&&(c.fragment=a.substring(d+1)||null,a=a.substring(0,d));d=a.indexOf("?");-1<d&&(c.query=a.substring(d+1)||null,a=a.substring(0,
d));"//"===a.substring(0,2)?(c.protocol=null,a=a.substring(2),a=b.parseAuthority(a,c)):(d=a.indexOf(":"),-1<d&&(c.protocol=a.substring(0,d)||null,c.protocol&&!c.protocol.match(b.protocol_expression)?c.protocol=void 0:"//"===a.substring(d+1,d+3)?(a=a.substring(d+3),a=b.parseAuthority(a,c)):(a=a.substring(d+1),c.urn=!0)));c.path=a;return c};b.parseHost=function(a,c){var d=a.indexOf("/"),b;-1===d&&(d=a.length);"["===a.charAt(0)?(b=a.indexOf("]"),c.hostname=a.substring(1,b)||null,c.port=a.substring(b+
2,d)||null,"/"===c.port&&(c.port=null)):a.indexOf(":")!==a.lastIndexOf(":")?(c.hostname=a.substring(0,d)||null,c.port=null):(b=a.substring(0,d).split(":"),c.hostname=b[0]||null,c.port=b[1]||null);c.hostname&&"/"!==a.substring(d).charAt(0)&&(d++,a="/"+a);return a.substring(d)||"/"};b.parseAuthority=function(a,c){a=b.parseUserinfo(a,c);return b.parseHost(a,c)};b.parseUserinfo=function(a,c){var d=a.indexOf("/"),u=a.lastIndexOf("@",-1<d?d:a.length-1);-1<u&&(-1===d||u<d)?(d=a.substring(0,u).split(":"),
c.username=d[0]?b.decode(d[0]):null,d.shift(),c.password=d[0]?b.decode(d.join(":")):null,a=a.substring(u+1)):(c.username=null,c.password=null);return a};b.parseQuery=function(a,c){if(!a)return{};a=a.replace(/&+/g,"&").replace(/^\?*&*|&+$/g,"");if(!a)return{};for(var d={},u=a.split("&"),e=u.length,f,h,g=0;g<e;g++)f=u[g].split("="),h=b.decodeQuery(f.shift(),c),f=f.length?b.decodeQuery(f.join("="),c):null,v.call(d,h)?("string"===typeof d[h]&&(d[h]=[d[h]]),d[h].push(f)):d[h]=f;return d};b.build=function(a){var c=
"";a.protocol&&(c+=a.protocol+":");a.urn||!c&&!a.hostname||(c+="//");c+=b.buildAuthority(a)||"";"string"===typeof a.path&&("/"!==a.path.charAt(0)&&"string"===typeof a.hostname&&(c+="/"),c+=a.path);"string"===typeof a.query&&a.query&&(c+="?"+a.query);"string"===typeof a.fragment&&a.fragment&&(c+="#"+a.fragment);return c};b.buildHost=function(a){var c="";if(a.hostname)c=b.ip6_expression.test(a.hostname)?c+("["+a.hostname+"]"):c+a.hostname;else return"";a.port&&(c+=":"+a.port);return c};b.buildAuthority=
function(a){return b.buildUserinfo(a)+b.buildHost(a)};b.buildUserinfo=function(a){var c="";a.username&&(c+=b.encode(a.username),a.password&&(c+=":"+b.encode(a.password)),c+="@");return c};b.buildQuery=function(a,c,d){var u="",e,f,h,g;for(f in a)if(v.call(a,f)&&f)if(p(a[f]))for(e={},h=0,g=a[f].length;h<g;h++)void 0!==a[f][h]&&void 0===e[a[f][h]+""]&&(u+="&"+b.buildQueryParameter(f,a[f][h],d),!0!==c&&(e[a[f][h]+""]=!0));else void 0!==a[f]&&(u+="&"+b.buildQueryParameter(f,a[f],d));return u.substring(1)};
b.buildQueryParameter=function(a,c,d){return b.encodeQuery(a,d)+(null!==c?"="+b.encodeQuery(c,d):"")};b.addQuery=function(a,c,d){if("object"===typeof c)for(var e in c)v.call(c,e)&&b.addQuery(a,e,c[e]);else if("string"===typeof c)void 0===a[c]?a[c]=d:("string"===typeof a[c]&&(a[c]=[a[c]]),p(d)||(d=[d]),a[c]=(a[c]||[]).concat(d));else throw new TypeError("URI.addQuery() accepts an object, string as the name parameter");};b.removeQuery=function(a,c,d){var e;if(p(c))for(d=0,e=c.length;d<e;d++)a[c[d]]=
void 0;else if("object"===typeof c)for(e in c)v.call(c,e)&&b.removeQuery(a,e,c[e]);else if("string"===typeof c)if(void 0!==d)if(a[c]===d)a[c]=void 0;else{if(p(a[c])){e=a[c];var f={},h,g;if(p(d))for(h=0,g=d.length;h<g;h++)f[d[h]]=!0;else f[d]=!0;h=0;for(g=e.length;h<g;h++)void 0!==f[e[h]]&&(e.splice(h,1),g--,h--);a[c]=e}}else a[c]=void 0;else throw new TypeError("URI.addQuery() accepts an object, string as the first parameter");};b.hasQuery=function(a,c,d,e){if("object"===typeof c){for(var f in c)if(v.call(c,
f)&&!b.hasQuery(a,f,c[f]))return!1;return!0}if("string"!==typeof c)throw new TypeError("URI.hasQuery() accepts an object, string as the name parameter");switch(z(d)){case "Undefined":return c in a;case "Boolean":return a=Boolean(p(a[c])?a[c].length:a[c]),d===a;case "Function":return!!d(a[c],c,a);case "Array":return p(a[c])?(e?h:r)(a[c],d):!1;case "RegExp":return p(a[c])?e?h(a[c],d):!1:Boolean(a[c]&&a[c].match(d));case "Number":d=String(d);case "String":return p(a[c])?e?h(a[c],d):!1:a[c]===d;default:throw new TypeError("URI.hasQuery() accepts undefined, boolean, string, number, RegExp, Function as the value parameter");
}};b.commonPath=function(a,c){var d=Math.min(a.length,c.length),b;for(b=0;b<d;b++)if(a.charAt(b)!==c.charAt(b)){b--;break}if(1>b)return a.charAt(0)===c.charAt(0)&&"/"===a.charAt(0)?"/":"";if("/"!==a.charAt(b)||"/"!==c.charAt(b))b=a.substring(0,b).lastIndexOf("/");return a.substring(0,b+1)};b.withinString=function(a,c,d){d||(d={});var e=d.start||b.findUri.start,f=d.end||b.findUri.end,h=d.trim||b.findUri.trim,g=/[a-z0-9-]=["']?$/i;for(e.lastIndex=0;;){var r=e.exec(a);if(!r)break;r=r.index;if(d.ignoreHtml){var k=
a.slice(Math.max(r-3,0),r);if(k&&g.test(k))continue}var k=r+a.slice(r).search(f),m=a.slice(r,k).replace(h,"");d.ignore&&d.ignore.test(m)||(k=r+m.length,m=c(m,r,k,a),a=a.slice(0,r)+m+a.slice(k),e.lastIndex=r+m.length)}e.lastIndex=0;return a};b.ensureValidHostname=function(a){if(a.match(b.invalid_hostname_characters)){if(!f)throw new TypeError('Hostname "'+a+'" contains characters other than [A-Z0-9.-] and Punycode.js is not available');if(f.toASCII(a).match(b.invalid_hostname_characters))throw new TypeError('Hostname "'+
a+'" contains characters other than [A-Z0-9.-]');}};b.noConflict=function(a){if(a)return a={URI:this.noConflict()},m.URITemplate&&"function"===typeof m.URITemplate.noConflict&&(a.URITemplate=m.URITemplate.noConflict()),m.IPv6&&"function"===typeof m.IPv6.noConflict&&(a.IPv6=m.IPv6.noConflict()),m.SecondLevelDomains&&"function"===typeof m.SecondLevelDomains.noConflict&&(a.SecondLevelDomains=m.SecondLevelDomains.noConflict()),a;m.URI===this&&(m.URI=n);return this};e.build=function(a){if(!0===a)this._deferred_build=
!0;else if(void 0===a||this._deferred_build)this._string=b.build(this._parts),this._deferred_build=!1;return this};e.clone=function(){return new b(this)};e.valueOf=e.toString=function(){return this.build(!1)._string};e.protocol=A("protocol");e.username=A("username");e.password=A("password");e.hostname=A("hostname");e.port=A("port");e.query=t("query","?");e.fragment=t("fragment","#");e.search=function(a,c){var d=this.query(a,c);return"string"===typeof d&&d.length?"?"+d:d};e.hash=function(a,c){var d=
this.fragment(a,c);return"string"===typeof d&&d.length?"#"+d:d};e.pathname=function(a,c){if(void 0===a||!0===a){var d=this._parts.path||(this._parts.hostname?"/":"");return a?b.decodePath(d):d}this._parts.path=a?b.recodePath(a):"/";this.build(!c);return this};e.path=e.pathname;e.href=function(a,c){var d;if(void 0===a)return this.toString();this._string="";this._parts=b._parts();var e=a instanceof b,f="object"===typeof a&&(a.hostname||a.path||a.pathname);a.nodeName&&(f=b.getDomAttribute(a),a=a[f]||
"",f=!1);!e&&f&&void 0!==a.pathname&&(a=a.toString());if("string"===typeof a||a instanceof String)this._parts=b.parse(String(a),this._parts);else if(e||f)for(d in e=e?a._parts:a,e)v.call(this._parts,d)&&(this._parts[d]=e[d]);else throw new TypeError("invalid input");this.build(!c);return this};e.is=function(a){var c=!1,d=!1,e=!1,f=!1,h=!1,r=!1,k=!1,m=!this._parts.urn;this._parts.hostname&&(m=!1,d=b.ip4_expression.test(this._parts.hostname),e=b.ip6_expression.test(this._parts.hostname),c=d||e,h=(f=
!c)&&g&&g.has(this._parts.hostname),r=f&&b.idn_expression.test(this._parts.hostname),k=f&&b.punycode_expression.test(this._parts.hostname));switch(a.toLowerCase()){case "relative":return m;case "absolute":return!m;case "domain":case "name":return f;case "sld":return h;case "ip":return c;case "ip4":case "ipv4":case "inet4":return d;case "ip6":case "ipv6":case "inet6":return e;case "idn":return r;case "url":return!this._parts.urn;case "urn":return!!this._parts.urn;case "punycode":return k}return null};
var E=e.protocol,F=e.port,G=e.hostname;e.protocol=function(a,c){if(void 0!==a&&a&&(a=a.replace(/:(\/\/)?$/,""),!a.match(b.protocol_expression)))throw new TypeError('Protocol "'+a+"\" contains characters other than [A-Z0-9.+-] or doesn't start with [A-Z]");return E.call(this,a,c)};e.scheme=e.protocol;e.port=function(a,c){if(this._parts.urn)return void 0===a?"":this;if(void 0!==a&&(0===a&&(a=null),a&&(a+="",":"===a.charAt(0)&&(a=a.substring(1)),a.match(/[^0-9]/))))throw new TypeError('Port "'+a+'" contains characters other than [0-9]');
return F.call(this,a,c)};e.hostname=function(a,c){if(this._parts.urn)return void 0===a?"":this;if(void 0!==a){var d={};b.parseHost(a,d);a=d.hostname}return G.call(this,a,c)};e.host=function(a,c){if(this._parts.urn)return void 0===a?"":this;if(void 0===a)return this._parts.hostname?b.buildHost(this._parts):"";b.parseHost(a,this._parts);this.build(!c);return this};e.authority=function(a,c){if(this._parts.urn)return void 0===a?"":this;if(void 0===a)return this._parts.hostname?b.buildAuthority(this._parts):
"";b.parseAuthority(a,this._parts);this.build(!c);return this};e.userinfo=function(a,c){if(this._parts.urn)return void 0===a?"":this;if(void 0===a){if(!this._parts.username)return"";var d=b.buildUserinfo(this._parts);return d.substring(0,d.length-1)}"@"!==a[a.length-1]&&(a+="@");b.parseUserinfo(a,this._parts);this.build(!c);return this};e.resource=function(a,c){var d;if(void 0===a)return this.path()+this.search()+this.hash();d=b.parse(a);this._parts.path=d.path;this._parts.query=d.query;this._parts.fragment=
d.fragment;this.build(!c);return this};e.subdomain=function(a,c){if(this._parts.urn)return void 0===a?"":this;if(void 0===a){if(!this._parts.hostname||this.is("IP"))return"";var d=this._parts.hostname.length-this.domain().length-1;return this._parts.hostname.substring(0,d)||""}d=this._parts.hostname.length-this.domain().length;d=this._parts.hostname.substring(0,d);d=new RegExp("^"+k(d));a&&"."!==a.charAt(a.length-1)&&(a+=".");a&&b.ensureValidHostname(a);this._parts.hostname=this._parts.hostname.replace(d,
a);this.build(!c);return this};e.domain=function(a,c){if(this._parts.urn)return void 0===a?"":this;"boolean"===typeof a&&(c=a,a=void 0);if(void 0===a){if(!this._parts.hostname||this.is("IP"))return"";var d=this._parts.hostname.match(/\./g);if(d&&2>d.length)return this._parts.hostname;d=this._parts.hostname.length-this.tld(c).length-1;d=this._parts.hostname.lastIndexOf(".",d-1)+1;return this._parts.hostname.substring(d)||""}if(!a)throw new TypeError("cannot set domain empty");b.ensureValidHostname(a);
!this._parts.hostname||this.is("IP")?this._parts.hostname=a:(d=new RegExp(k(this.domain())+"$"),this._parts.hostname=this._parts.hostname.replace(d,a));this.build(!c);return this};e.tld=function(a,c){if(this._parts.urn)return void 0===a?"":this;"boolean"===typeof a&&(c=a,a=void 0);if(void 0===a){if(!this._parts.hostname||this.is("IP"))return"";var d=this._parts.hostname.lastIndexOf("."),d=this._parts.hostname.substring(d+1);return!0!==c&&g&&g.list[d.toLowerCase()]?g.get(this._parts.hostname)||d:d}if(a)if(a.match(/[^a-zA-Z0-9-]/))if(g&&
g.is(a))d=new RegExp(k(this.tld())+"$"),this._parts.hostname=this._parts.hostname.replace(d,a);else throw new TypeError('TLD "'+a+'" contains characters other than [A-Z0-9]');else{if(!this._parts.hostname||this.is("IP"))throw new ReferenceError("cannot set TLD on non-domain host");d=new RegExp(k(this.tld())+"$");this._parts.hostname=this._parts.hostname.replace(d,a)}else throw new TypeError("cannot set TLD empty");this.build(!c);return this};e.directory=function(a,c){if(this._parts.urn)return void 0===
a?"":this;if(void 0===a||!0===a){if(!this._parts.path&&!this._parts.hostname)return"";if("/"===this._parts.path)return"/";var d=this._parts.path.length-this.filename().length-1,d=this._parts.path.substring(0,d)||(this._parts.hostname?"/":"");return a?b.decodePath(d):d}d=this._parts.path.length-this.filename().length;d=this._parts.path.substring(0,d);d=new RegExp("^"+k(d));this.is("relative")||(a||(a="/"),"/"!==a.charAt(0)&&(a="/"+a));a&&"/"!==a.charAt(a.length-1)&&(a+="/");a=b.recodePath(a);this._parts.path=
this._parts.path.replace(d,a);this.build(!c);return this};e.filename=function(a,c){if(this._parts.urn)return void 0===a?"":this;if(void 0===a||!0===a){if(!this._parts.path||"/"===this._parts.path)return"";var d=this._parts.path.lastIndexOf("/"),d=this._parts.path.substring(d+1);return a?b.decodePathSegment(d):d}d=!1;"/"===a.charAt(0)&&(a=a.substring(1));a.match(/\.?\//)&&(d=!0);var e=new RegExp(k(this.filename())+"$");a=b.recodePath(a);this._parts.path=this._parts.path.replace(e,a);d?this.normalizePath(c):
this.build(!c);return this};e.suffix=function(a,c){if(this._parts.urn)return void 0===a?"":this;if(void 0===a||!0===a){if(!this._parts.path||"/"===this._parts.path)return"";var d=this.filename(),e=d.lastIndexOf(".");if(-1===e)return"";d=d.substring(e+1);d=/^[a-z0-9%]+$/i.test(d)?d:"";return a?b.decodePathSegment(d):d}"."===a.charAt(0)&&(a=a.substring(1));if(d=this.suffix())e=a?new RegExp(k(d)+"$"):new RegExp(k("."+d)+"$");else{if(!a)return this;this._parts.path+="."+b.recodePath(a)}e&&(a=b.recodePath(a),
this._parts.path=this._parts.path.replace(e,a));this.build(!c);return this};e.segment=function(a,c,d){var b=this._parts.urn?":":"/",e=this.path(),f="/"===e.substring(0,1),e=e.split(b);void 0!==a&&"number"!==typeof a&&(d=c,c=a,a=void 0);if(void 0!==a&&"number"!==typeof a)throw Error('Bad segment "'+a+'", must be 0-based integer');f&&e.shift();0>a&&(a=Math.max(e.length+a,0));if(void 0===c)return void 0===a?e:e[a];if(null===a||void 0===e[a])if(p(c)){e=[];a=0;for(var h=c.length;a<h;a++)if(c[a].length||
e.length&&e[e.length-1].length)e.length&&!e[e.length-1].length&&e.pop(),e.push(c[a])}else{if(c||"string"===typeof c)""===e[e.length-1]?e[e.length-1]=c:e.push(c)}else c?e[a]=c:e.splice(a,1);f&&e.unshift("");return this.path(e.join(b),d)};e.segmentCoded=function(a,c,d){var e,f;"number"!==typeof a&&(d=c,c=a,a=void 0);if(void 0===c){a=this.segment(a,c,d);if(p(a))for(e=0,f=a.length;e<f;e++)a[e]=b.decode(a[e]);else a=void 0!==a?b.decode(a):void 0;return a}if(p(c))for(e=0,f=c.length;e<f;e++)c[e]=b.decode(c[e]);
else c="string"===typeof c||c instanceof String?b.encode(c):c;return this.segment(a,c,d)};var H=e.query;e.query=function(a,c){if(!0===a)return b.parseQuery(this._parts.query,this._parts.escapeQuerySpace);if("function"===typeof a){var d=b.parseQuery(this._parts.query,this._parts.escapeQuerySpace),e=a.call(this,d);this._parts.query=b.buildQuery(e||d,this._parts.duplicateQueryParameters,this._parts.escapeQuerySpace);this.build(!c);return this}return void 0!==a&&"string"!==typeof a?(this._parts.query=
b.buildQuery(a,this._parts.duplicateQueryParameters,this._parts.escapeQuerySpace),this.build(!c),this):H.call(this,a,c)};e.setQuery=function(a,c,d){var e=b.parseQuery(this._parts.query,this._parts.escapeQuerySpace);if("string"===typeof a||a instanceof String)e[a]=void 0!==c?c:null;else if("object"===typeof a)for(var f in a)v.call(a,f)&&(e[f]=a[f]);else throw new TypeError("URI.addQuery() accepts an object, string as the name parameter");this._parts.query=b.buildQuery(e,this._parts.duplicateQueryParameters,
this._parts.escapeQuerySpace);"string"!==typeof a&&(d=c);this.build(!d);return this};e.addQuery=function(a,c,d){var e=b.parseQuery(this._parts.query,this._parts.escapeQuerySpace);b.addQuery(e,a,void 0===c?null:c);this._parts.query=b.buildQuery(e,this._parts.duplicateQueryParameters,this._parts.escapeQuerySpace);"string"!==typeof a&&(d=c);this.build(!d);return this};e.removeQuery=function(a,c,d){var e=b.parseQuery(this._parts.query,this._parts.escapeQuerySpace);b.removeQuery(e,a,c);this._parts.query=
b.buildQuery(e,this._parts.duplicateQueryParameters,this._parts.escapeQuerySpace);"string"!==typeof a&&(d=c);this.build(!d);return this};e.hasQuery=function(a,c,d){var e=b.parseQuery(this._parts.query,this._parts.escapeQuerySpace);return b.hasQuery(e,a,c,d)};e.setSearch=e.setQuery;e.addSearch=e.addQuery;e.removeSearch=e.removeQuery;e.hasSearch=e.hasQuery;e.normalize=function(){return this._parts.urn?this.normalizeProtocol(!1).normalizeQuery(!1).normalizeFragment(!1).build():this.normalizeProtocol(!1).normalizeHostname(!1).normalizePort(!1).normalizePath(!1).normalizeQuery(!1).normalizeFragment(!1).build()};
e.normalizeProtocol=function(a){"string"===typeof this._parts.protocol&&(this._parts.protocol=this._parts.protocol.toLowerCase(),this.build(!a));return this};e.normalizeHostname=function(a){this._parts.hostname&&(this.is("IDN")&&f?this._parts.hostname=f.toASCII(this._parts.hostname):this.is("IPv6")&&l&&(this._parts.hostname=l.best(this._parts.hostname)),this._parts.hostname=this._parts.hostname.toLowerCase(),this.build(!a));return this};e.normalizePort=function(a){"string"===typeof this._parts.protocol&&
this._parts.port===b.defaultPorts[this._parts.protocol]&&(this._parts.port=null,this.build(!a));return this};e.normalizePath=function(a){if(this._parts.urn||!this._parts.path||"/"===this._parts.path)return this;var c,d=this._parts.path,e="",f,h;"/"!==d.charAt(0)&&(c=!0,d="/"+d);d=d.replace(/(\/(\.\/)+)|(\/\.$)/g,"/").replace(/\/{2,}/g,"/");c&&(e=d.substring(1).match(/^(\.\.\/)+/)||"")&&(e=e[0]);for(;;){f=d.indexOf("/..");if(-1===f)break;else if(0===f){d=d.substring(3);continue}h=d.substring(0,f).lastIndexOf("/");
-1===h&&(h=f);d=d.substring(0,h)+d.substring(f+3)}c&&this.is("relative")&&(d=e+d.substring(1));d=b.recodePath(d);this._parts.path=d;this.build(!a);return this};e.normalizePathname=e.normalizePath;e.normalizeQuery=function(a){"string"===typeof this._parts.query&&(this._parts.query.length?this.query(b.parseQuery(this._parts.query,this._parts.escapeQuerySpace)):this._parts.query=null,this.build(!a));return this};e.normalizeFragment=function(a){this._parts.fragment||(this._parts.fragment=null,this.build(!a));
return this};e.normalizeSearch=e.normalizeQuery;e.normalizeHash=e.normalizeFragment;e.iso8859=function(){var a=b.encode,c=b.decode;b.encode=escape;b.decode=decodeURIComponent;this.normalize();b.encode=a;b.decode=c;return this};e.unicode=function(){var a=b.encode,c=b.decode;b.encode=D;b.decode=unescape;this.normalize();b.encode=a;b.decode=c;return this};e.readable=function(){var a=this.clone();a.username("").password("").normalize();var c="";a._parts.protocol&&(c+=a._parts.protocol+"://");a._parts.hostname&&
(a.is("punycode")&&f?(c+=f.toUnicode(a._parts.hostname),a._parts.port&&(c+=":"+a._parts.port)):c+=a.host());a._parts.hostname&&a._parts.path&&"/"!==a._parts.path.charAt(0)&&(c+="/");c+=a.path(!0);if(a._parts.query){for(var d="",e=0,h=a._parts.query.split("&"),g=h.length;e<g;e++){var r=(h[e]||"").split("="),d=d+("&"+b.decodeQuery(r[0],this._parts.escapeQuerySpace).replace(/&/g,"%26"));void 0!==r[1]&&(d+="="+b.decodeQuery(r[1],this._parts.escapeQuerySpace).replace(/&/g,"%26"))}c+="?"+d.substring(1)}return c+=
b.decodeQuery(a.hash(),!0)};e.absoluteTo=function(a){var c=this.clone(),d=["protocol","username","password","hostname","port"],e,f;if(this._parts.urn)throw Error("URNs do not have any generally defined hierarchical components");a instanceof b||(a=new b(a));c._parts.protocol||(c._parts.protocol=a._parts.protocol);if(this._parts.hostname)return c;for(e=0;f=d[e];e++)c._parts[f]=a._parts[f];c._parts.path?".."===c._parts.path.substring(-2)&&(c._parts.path+="/"):(c._parts.path=a._parts.path,c._parts.query||
(c._parts.query=a._parts.query));"/"!==c.path().charAt(0)&&(a=a.directory(),c._parts.path=(a?a+"/":"")+c._parts.path,c.normalizePath());c.build();return c};e.relativeTo=function(a){var c=this.clone().normalize(),d,e,f,h;if(c._parts.urn)throw Error("URNs do not have any generally defined hierarchical components");a=(new b(a)).normalize();d=c._parts;e=a._parts;f=c.path();h=a.path();if("/"!==f.charAt(0))throw Error("URI is already relative");if("/"!==h.charAt(0))throw Error("Cannot calculate a URI relative to another relative URI");
d.protocol===e.protocol&&(d.protocol=null);if(d.username===e.username&&d.password===e.password&&null===d.protocol&&null===d.username&&null===d.password&&d.hostname===e.hostname&&d.port===e.port)d.hostname=null,d.port=null;else return c.build();if(f===h)return d.path="",c.build();a=b.commonPath(c.path(),a.path());if(!a)return c.build();e=e.path.substring(a.length).replace(/[^\/]*$/,"").replace(/.*?\//g,"../");d.path=e+d.path.substring(a.length);return c.build()};e.equals=function(a){var c=this.clone();
a=new b(a);var d={},e={},f={},h;c.normalize();a.normalize();if(c.toString()===a.toString())return!0;d=c.query();e=a.query();c.query("");a.query("");if(c.toString()!==a.toString()||d.length!==e.length)return!1;d=b.parseQuery(d,this._parts.escapeQuerySpace);e=b.parseQuery(e,this._parts.escapeQuerySpace);for(h in d)if(v.call(d,h)){if(!p(d[h])){if(d[h]!==e[h])return!1}else if(!r(d[h],e[h]))return!1;f[h]=!0}for(h in e)if(v.call(e,h)&&!f[h])return!1;return!0};e.duplicateQueryParameters=function(a){this._parts.duplicateQueryParameters=
!!a;return this};e.escapeQuerySpace=function(a){this._parts.escapeQuerySpace=!!a;return this};return b});(function(f,l){"object"===typeof exports?module.exports=l(require("./URI")):"function"===typeof define&&define.amd?define(["./URI"],l):f.URITemplate=l(f.URI,f)})(this,function(f,l){function g(b){if(g._cache[b])return g._cache[b];if(!(this instanceof g))return new g(b);this.expression=b;g._cache[b]=this;return this}function m(b){this.data=b;this.cache={}}var b=l&&l.URITemplate,k=Object.prototype.hasOwnProperty,z=g.prototype,p={"":{prefix:"",separator:",",named:!1,empty_name_separator:!1,encode:"encode"},
"+":{prefix:"",separator:",",named:!1,empty_name_separator:!1,encode:"encodeReserved"},"#":{prefix:"#",separator:",",named:!1,empty_name_separator:!1,encode:"encodeReserved"},".":{prefix:".",separator:".",named:!1,empty_name_separator:!1,encode:"encode"},"/":{prefix:"/",separator:"/",named:!1,empty_name_separator:!1,encode:"encode"},";":{prefix:";",separator:";",named:!0,empty_name_separator:!1,encode:"encode"},"?":{prefix:"?",separator:"&",named:!0,empty_name_separator:!0,encode:"encode"},"&":{prefix:"&",
separator:"&",named:!0,empty_name_separator:!0,encode:"encode"}};g._cache={};g.EXPRESSION_PATTERN=/\{([^a-zA-Z0-9%_]?)([^\}]+)(\}|$)/g;g.VARIABLE_PATTERN=/^([^*:]+)((\*)|:(\d+))?$/;g.VARIABLE_NAME_PATTERN=/[^a-zA-Z0-9%_]/;g.expand=function(b,f){var k=p[b.operator],m=k.named?"Named":"Unnamed",l=b.variables,t=[],n,e,v;for(v=0;e=l[v];v++)n=f.get(e.name),n.val.length?t.push(g["expand"+m](n,k,e.explode,e.explode&&k.separator||",",e.maxlength,e.name)):n.type&&t.push("");return t.length?k.prefix+t.join(k.separator):
""};g.expandNamed=function(b,g,k,m,l,t){var n="",e=g.encode;g=g.empty_name_separator;var v=!b[e].length,w=2===b.type?"":f[e](t),q,p,z;p=0;for(z=b.val.length;p<z;p++)l?(q=f[e](b.val[p][1].substring(0,l)),2===b.type&&(w=f[e](b.val[p][0].substring(0,l)))):v?(q=f[e](b.val[p][1]),2===b.type?(w=f[e](b.val[p][0]),b[e].push([w,q])):b[e].push([void 0,q])):(q=b[e][p][1],2===b.type&&(w=b[e][p][0])),n&&(n+=m),k?n+=w+(g||q?"=":"")+q:(p||(n+=f[e](t)+(g||q?"=":"")),2===b.type&&(n+=w+","),n+=q);return n};g.expandUnnamed=
function(b,g,k,m,l){var t="",n=g.encode;g=g.empty_name_separator;var e=!b[n].length,p,w,q,z;q=0;for(z=b.val.length;q<z;q++)l?w=f[n](b.val[q][1].substring(0,l)):e?(w=f[n](b.val[q][1]),b[n].push([2===b.type?f[n](b.val[q][0]):void 0,w])):w=b[n][q][1],t&&(t+=m),2===b.type&&(p=l?f[n](b.val[q][0].substring(0,l)):b[n][q][0],t+=p,t=k?t+(g||w?"=":""):t+","),t+=w;return t};g.noConflict=function(){l.URITemplate===g&&(l.URITemplate=b);return g};z.expand=function(b){var f="";this.parts&&this.parts.length||this.parse();
b instanceof m||(b=new m(b));for(var k=0,l=this.parts.length;k<l;k++)f+="string"===typeof this.parts[k]?this.parts[k]:g.expand(this.parts[k],b);return f};z.parse=function(){var b=this.expression,f=g.EXPRESSION_PATTERN,k=g.VARIABLE_PATTERN,m=g.VARIABLE_NAME_PATTERN,l=[],t=0,n,e,v;for(f.lastIndex=0;;){e=f.exec(b);if(null===e){l.push(b.substring(t));break}else l.push(b.substring(t,e.index)),t=e.index+e[0].length;if(!p[e[1]])throw Error('Unknown Operator "'+e[1]+'" in "'+e[0]+'"');if(!e[3])throw Error('Unclosed Expression "'+
e[0]+'"');n=e[2].split(",");for(var w=0,q=n.length;w<q;w++){v=n[w].match(k);if(null===v)throw Error('Invalid Variable "'+n[w]+'" in "'+e[0]+'"');if(v[1].match(m))throw Error('Invalid Variable Name "'+v[1]+'" in "'+e[0]+'"');n[w]={name:v[1],explode:!!v[3],maxlength:v[4]&&parseInt(v[4],10)}}if(!n.length)throw Error('Expression Missing Variable(s) "'+e[0]+'"');l.push({expression:e[0],operator:e[1],variables:n})}l.length||l.push(b);this.parts=l;return this};m.prototype.get=function(b){var f=this.data,
g={type:0,val:[],encode:[],encodeReserved:[]},l;if(void 0!==this.cache[b])return this.cache[b];this.cache[b]=g;f="[object Function]"===String(Object.prototype.toString.call(f))?f(b):"[object Function]"===String(Object.prototype.toString.call(f[b]))?f[b](b):f[b];if(void 0!==f&&null!==f)if("[object Array]"===String(Object.prototype.toString.call(f))){l=0;for(b=f.length;l<b;l++)void 0!==f[l]&&null!==f[l]&&g.val.push([void 0,String(f[l])]);g.val.length&&(g.type=3)}else if("[object Object]"===String(Object.prototype.toString.call(f))){for(l in f)k.call(f,
l)&&void 0!==f[l]&&null!==f[l]&&g.val.push([l,String(f[l])]);g.val.length&&(g.type=2)}else g.type=1,g.val.push([void 0,String(f)]);return g};f.expand=function(b,k){var l=(new g(b)).expand(k);return new f(l)};return g});

View file

@ -0,0 +1,2 @@
/*! 1.4.0 */
window.XMLHttpRequest&&window.FormData&&(XMLHttpRequest=function(a){return function(){var b=new a;return b.setRequestHeader=function(a){return function(c,d){if("__setXHR_"===c){var e=d(b);e instanceof Function&&e(b)}else a.apply(b,arguments)}}(b.setRequestHeader),b}}(XMLHttpRequest),window.XMLHttpRequest.__isShim=!0);

2
static/lib/angular-file-upload.min.js vendored Normal file
View file

@ -0,0 +1,2 @@
/*! 1.4.0 */
!function(){var a=angular.module("angularFileUpload",[]);a.service("$upload",["$http","$timeout",function(a,b){function c(c){c.method=c.method||"POST",c.headers=c.headers||{},c.transformRequest=c.transformRequest||function(b,c){return window.ArrayBuffer&&b instanceof window.ArrayBuffer?b:a.defaults.transformRequest[0](b,c)},window.XMLHttpRequest.__isShim&&(c.headers.__setXHR_=function(){return function(a){a&&(c.__XHR=a,c.xhrFn&&c.xhrFn(a),a.upload.addEventListener("progress",function(a){c.progress&&b(function(){c.progress&&c.progress(a)})},!1),a.upload.addEventListener("load",function(a){a.lengthComputable&&c.progress&&c.progress(a)},!1))}});var d=a(c);return d.progress=function(a){return c.progress=a,d},d.abort=function(){return c.__XHR&&b(function(){c.__XHR.abort()}),d},d.xhr=function(a){return c.xhrFn=a,d},d.then=function(a,b){return function(d,e,f){c.progress=f||c.progress;var g=b.apply(a,[d,e,f]);return g.abort=a.abort,g.progress=a.progress,g.xhr=a.xhr,g.then=a.then,g}}(d,d.then),d}this.upload=function(b){b.headers=b.headers||{},b.headers["Content-Type"]=void 0,b.transformRequest=b.transformRequest||a.defaults.transformRequest;var d=new FormData,e=b.transformRequest,f=b.data;return b.transformRequest=function(a,c){if(f)if(b.formDataAppender)for(var d in f){var g=f[d];b.formDataAppender(a,d,g)}else for(var d in f){var g=f[d];if("function"==typeof e)g=e(g,c);else for(var h=0;h<e.length;h++){var i=e[h];"function"==typeof i&&(g=i(g,c))}a.append(d,g)}if(null!=b.file){var j=b.fileFormDataName||"file";if("[object Array]"===Object.prototype.toString.call(b.file))for(var k="[object String]"===Object.prototype.toString.call(j),h=0;h<b.file.length;h++)a.append(k?j+h:j[h],b.file[h],b.file[h].name);else a.append(j,b.file,b.file.name)}return a},b.data=d,c(b)},this.http=function(a){return c(a)}}]),a.directive("ngFileSelect",["$parse","$timeout",function(a,b){return function(c,d,e){var f=a(e.ngFileSelect);d.bind("change",function(a){var d,e,g=[];if(d=a.target.files,null!=d)for(e=0;e<d.length;e++)g.push(d.item(e));b(function(){f(c,{$files:g,$event:a})})}),("ontouchstart"in window||navigator.maxTouchPoints>0||navigator.msMaxTouchPoints>0)&&d.bind("touchend",function(a){a.preventDefault(),a.target.click()})}}]),a.directive("ngFileDropAvailable",["$parse","$timeout",function(a,b){return function(c,d,e){if("draggable"in document.createElement("span")){var f=a(e.ngFileDropAvailable);b(function(){f(c)})}}}]),a.directive("ngFileDrop",["$parse","$timeout",function(a,b){return function(c,d,e){function f(a,b){if(b.isDirectory){var c=b.createReader();i++,c.readEntries(function(b){for(var c=0;c<b.length;c++)f(a,b[c]);i--})}else i++,b.file(function(b){i--,a.push(b)})}if("draggable"in document.createElement("span")){var g=null,h=a(e.ngFileDrop);d[0].addEventListener("dragover",function(a){b.cancel(g),a.stopPropagation(),a.preventDefault(),d.addClass(e.ngFileDragOverClass||"dragover")},!1),d[0].addEventListener("dragleave",function(){g=b(function(){d.removeClass(e.ngFileDragOverClass||"dragover")})},!1);var i=0;d[0].addEventListener("drop",function(a){a.stopPropagation(),a.preventDefault(),d.removeClass(e.ngFileDragOverClass||"dragover");var g=[],j=a.dataTransfer.items;if(j&&j.length>0&&j[0].webkitGetAsEntry)for(var k=0;k<j.length;k++)f(g,j[k].webkitGetAsEntry());else{var l=a.dataTransfer.files;if(null!=l)for(var k=0;k<l.length;k++)g.push(l.item(k))}!function m(d){b(function(){i?m(10):h(c,{$files:g,$event:a})},d||0)}()},!1)}}}])}();

1
static/lib/rickshaw.min.css vendored Normal file

File diff suppressed because one or more lines are too long

3
static/lib/rickshaw.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -1,4 +1,4 @@
<div class="container about-us">
<div class="cor-container about-us">
<h2>
About Us
</h2>
@ -40,7 +40,7 @@
<div class="row">
<div class="col-sm-12">
<h3>Our Story</h3>
<p>Quay.io was originally created out of necessesity when we wanted to use Docker containers with our original IDE product. We were using Docker containers to host and isolate server processes invoked on behalf of our users and often running their code. We started by building the Docker image dynamically whenever we spun up a new host node. The image was monolithic. It was too large, took too long to build, and was hard to manage conflicts. It was everything that Docker wasn't supposed to be. When we decided to split it up into pre-built images and host them somewhere, we noticed that there wasn't a good place to host images securely. Determined to scratch our own itch, we built Quay.io, and officially launched it as an aside in our presentation to the <a href="http://www.meetup.com/Docker-NewYorkCity/events/142142762/">Docker New York City Meetup</a> on October 2nd, 2013.</p>
<p>Quay.io was originally created out of necessity when we wanted to use Docker containers with our original IDE product. We were using Docker containers to host and isolate server processes invoked on behalf of our users and often running their code. We started by building the Docker image dynamically whenever we spun up a new host node. The image was monolithic. It was too large, took too long to build, and was hard to manage conflicts. It was everything that Docker wasn't supposed to be. When we decided to split it up into pre-built images and host them somewhere, we noticed that there wasn't a good place to host images securely. Determined to scratch our own itch, we built Quay.io, and officially launched it as an aside in our presentation to the <a href="http://www.meetup.com/Docker-NewYorkCity/events/142142762/">Docker New York City Meetup</a> on October 2nd, 2013.</p>
<p>After launch, our customers demanded that Quay.io become our main focus. They rely on us to make sure they can store and distribute their Docker images, and we understand that solemn responsibility. Our customers have been fantastic with giving us great feedback and suggestions.</p>
<p>In August, 2014, Quay.io joined <a href="https://coreos.com">CoreOS</a> to provide registry support for the enterprise. As ever, we are working as hard as we can to deliver on the promise and execute our vision of what a top notch Docker registry should be.
</div>

View file

@ -1,8 +1,8 @@
<div class="resource-view" resource="repository_build" error-message="'No matching repository build found'"></div>
<div class="container repo repo-build" ng-show="accessDenied">
<div class="cor-container repo repo-build" ng-show="accessDenied">
You do not have permission to view this page
</div>
<div class="container repo repo-build repo-build-pack" ng-show="repobuild">
<div class="cor-container repo repo-build repo-build-pack" ng-show="repobuild">
<div class="header row">
<a href="{{ '/repository/' + repo.namespace + '/' + repo.name + '/build' }}" class="back"><i class="fa fa-chevron-left"></i></a>
<h3>

View file

@ -1,5 +1,5 @@
<div class="confirm-invite">
<div class="container signin-container">
<div class="cor-container signin-container">
<div class="row">
<div class="col-sm-6 col-sm-offset-3">
<div class="user-setup" ng-show="user.anonymous" redirect-url="redirectUrl"

View file

@ -1,4 +1,4 @@
<div class="container">
<div class="cor-container">
<h2>
Contact Us<br>
<small>We are here to help!</small>

View file

@ -1,4 +1,4 @@
<div class="container">
<div class="cor-container">
<h3>Redirecting...</h3>
<META http-equiv="refresh" content="0;URL=http://docs.quay.io/solution/getting-started.html">
If this page does not redirect, please <a href="http://docs.quay.io/solution/getting-started.html"> click here</a>.

View file

@ -1,5 +1,5 @@
<div class="resource-view" resource="image" error-message="'No image found'">
<div class="container repo repo-image-view">
<div class="cor-container repo repo-image-view">
<div class="header">
<a href="{{ '/repository/' + repo.namespace + '/' + repo.name }}" class="back"><i class="fa fa-chevron-left"></i></a>
<h3>

View file

@ -2,7 +2,7 @@
<div class="landing-background" ng-class="user.anonymous ? 'landing': 'signedin'"></div>
<div class="landing-filter" ng-class="user.anonymous ? 'landing': 'signedin'"></div>
<div class="landing-content">
<div class="container">
<div class="cor-container">
<div class="row messages">
<div class="col-md-7">
<div ng-show="user.anonymous" style="text-align: center">
@ -54,6 +54,6 @@
</div>
</div> <!-- col -->
</div> <!-- row -->
</div> <!-- container -->
</div> <!-- cor-container -->
</div>
</div> <!-- jumbotron -->

View file

@ -7,13 +7,13 @@
</span>
<span class="spacer"></span>
Quay.io is now part of CoreOS! <a href="http://blog.devtable.com/" target="_blank">Read the blog post.</a>
Quay.io is now part of CoreOS! <a href="https://coreos.com/blog/CoreOS-enterprise-docker-registry/" target="_blank">Read the blog post.</a>
</div>
<div class="landing-background" ng-class="user.anonymous ? 'landing': 'signedin'"></div>
<div class="landing-filter" ng-class="user.anonymous ? 'landing': 'signedin'"></div>
<div class="landing-content">
<div class="container">
<div class="cor-container">
<div class="row messages">
<div class="col-md-7">
<div ng-show="user.anonymous">
@ -70,7 +70,7 @@
<div class="rows" ng-show="user.anonymous">
<div class=" shoutout-row landing-section">
<div class="container">
<div class="cor-container">
<div class="row">
<div class="col-md-4 shoutout">
<i class="fa fa-lock"></i>
@ -105,7 +105,7 @@
<div class=" landing-section">
<h2>Trusted by companies who use Docker</h2>
<div class="container">
<div class="cor-container">
<div class="row">
<div class="trusted-logos">
<div class="col-md-3 trusted-logo">
@ -127,7 +127,7 @@
<div class="landing-section">
<h2>Built with our users in mind</h2>
<div class="container">
<div class="cor-container">
<div class="row">
<div class="col-lg-8 feature-shoutout">
<img id="screenshot" ng-src="{{ '/static/img/' + currentScreenshot + '.png' }}" class="img-responsive">
@ -171,7 +171,7 @@
<div class="landing-section">
<h2>Seamlessly integrate into your Docker-based infrastructure</h2>
<div class="container">
<div class="cor-container">
<div class="row testimonial">
<div class="message">
Quay.io has become an essential part of our infrastructure as we move to Docker-based deploys.
@ -192,7 +192,7 @@
<div class="landing-section">
<h2>See what other people are saying about Quay.io</h2>
<div class="container">
<div class="cor-container">
<div class="row">
<div class="jcarousel-wrapper">
<div class="jcarousel">
@ -207,7 +207,7 @@
</li>
<li>
<div class="twitter-view" avatar-url="https://pbs.twimg.com/profile_images/2578175278/ykn3l9ktfdy1hia5odij_bigger.jpeg"
<div class="twitter-view" avatar-url="https://pbs.twimg.com/profile_images/483391930147954688/pvJAHzy__bigger.jpeg"
author-name="Frank Macreery" author-user="fancyremarker" message-url="https://twitter.com/fancyremarker/statuses/448528623692025857"
message-date="March 25, 2014">
<a href="https://twitter.com/quayio">@quayio</a> releases Docker build flair! <a href="http://t.co/72ULgveLj4">pic.twitter.com/72ULgveLj4</a>
@ -263,7 +263,7 @@
<div class="landing-section">
<h2>Start pushing to Quay.io in under a minute</h2>
<div class="container">
<div class="cor-container">
<div class="row landing-action">
<a href="/plans" class="btn btn-primary">Start Free Trial</a>
</div>
@ -272,7 +272,7 @@
</div>
<div class="container" ng-if="user.anonymous">
<div class="cor-container" ng-if="user.anonymous">
<div class="row">
<div style="border-top: 1px solid #eee; padding-top: 20px;" class="col-md-12">
<a href="https://mixpanel.com/f/partner"><img src="//cdn.mxpnl.com/site_media/images/partner/badge_light.png" alt="Mobile Analytics" /></a>

View file

@ -2,7 +2,7 @@
</div>
<div ng-show="application">
<div class="container manage-application">
<div class="cor-container manage-application">
<!-- Header -->
<div class="row">
<div class="col-md-12">

View file

@ -2,7 +2,7 @@
<div class="quay-spinner"></div>
</div>
<div class="container create-org" ng-show="!creating">
<div class="cor-container create-org" ng-show="!creating">
<div class="row header-row">
<div class="col-md-12">
@ -54,14 +54,14 @@
<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="bottom" data-container="body" ng-pattern="/^[a-z0-9_]{4,30}$/">
<span class="description">This will also be the namespace for your repositories</span>
<span class="description">This will also be the namespace for your repositories. Must be alphanumeric and all lowercase.</span>
</div>
<div class="form-group nested">
<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>
<span class="description">This address must be different from your account's email.</span>
</div>
<!-- Plans Table -->

View file

@ -1,18 +1,18 @@
<div class="container" ng-show="user.anonymous">
<div class="cor-container" ng-show="user.anonymous">
<div class="col-sm-6 col-sm-offset-3">
<div class="user-setup" redirect-url="'/new/'"></div>
</div>
</div>
<div class="container" ng-show="!user.anonymous && building">
<div class="cor-container" ng-show="!user.anonymous && building">
<div class="quay-spinner"></div>
</div>
<div class="container" ng-show="!user.anonymous && creating">
<div class="cor-container" ng-show="!user.anonymous && creating">
<div class="quay-spinner"></div>
</div>
<div class="container new-repo" ng-show="!user.anonymous && !creating && !building">
<div class="cor-container new-repo" ng-show="!user.anonymous && !creating && !building">
<form method="post" name="newRepoForm" id="newRepoForm" ng-submit="createNewRepo()">
<!-- Header -->
@ -143,9 +143,9 @@
<div class="section-title">Upload <span ng-if="repo.initialize == 'dockerfile'">Dockerfile</span><span ng-if="repo.initialize == 'zipfile'">Archive</span></div>
<div style="padding-top: 20px;">
<div class="initialize-repo">
<div class="dockerfile-build-form" repository="createdForBuild" upload-failed="handleBuildFailed(message)"
<div class="dockerfile-build-form" repository="createdForBuild || repo" upload-failed="handleBuildFailed(message)"
build-started="handleBuildStarted()" build-failed="handleBuildFailed(message)" start-now="createdForBuild"
has-dockerfile="hasDockerfile" uploading="uploading" building="building"></div>
is-ready="hasDockerfile" uploading="uploading" building="building"></div>
</div>
</div>
</div>

View file

@ -1,5 +1,5 @@
<div class="resource-view" resource="orgResource" error-message="'No organization found'"></div>
<div class="org-admin container" ng-show="organization">
<div class="org-admin cor-container" ng-show="organization">
<div class="organization-header" organization="organization" clickable="true"></div>
<div class="row">

View file

@ -1,5 +1,5 @@
<div class="resource-view" resource="memberResource" error-message="'Member not found'">
<div class="org-member-logs container">
<div class="org-member-logs cor-container">
<div class="organization-header" organization="organization" clickable="true"></div>
<div class="logs-view" organization="organization" performer="memberInfo"
makevisible="organization && memberInfo && ready"></div>

View file

@ -1,5 +1,5 @@
<div class="resource-view" resource="orgResource" error-message="'No matching organization found'">
<div class="org-view container">
<div class="org-view cor-container">
<div class="organization-header" organization="organization">
<div class="header-buttons" ng-show="organization.is_admin">

View file

@ -1,4 +1,4 @@
<div class="container org-list conntent-container">
<div class="cor-container org-list conntent-container">
<div class="loading" ng-show="!user">
<div class="quay-spinner"></div>
</div>

View file

@ -1,4 +1,4 @@
<div class="container plans content-container">
<div class="cor-container plans content-container">
<div class="row plans-list">
<div class="col-sm-2">
<div class="features-bar hidden-xs">

View file

@ -1,9 +1,9 @@
<div class="container" ng-show="deleting"><div class="quay-spinner"></div></div>
<div class="cor-container" ng-show="deleting"><div class="quay-spinner"></div></div>
<div class="resource-view" resource="repository" error-message="'No repository found'"></div>
<div class="container repo repo-admin" ng-show="accessDenied">
<div class="cor-container repo repo-admin" ng-show="accessDenied">
You do not have permission to view this page
</div>
<div class="container repo repo-admin" ng-show="repo && !deleting">
<div class="cor-container repo repo-admin" ng-show="repo && !deleting">
<div class="header row">
<a href="{{ '/repository/' + repo.namespace + '/' + repo.name }}" class="back"><i class="fa fa-chevron-left"></i></a>
<h3>

View file

@ -1,8 +1,8 @@
<div class="resource-view" resource="repository" error-message="'No repository found'"></div>
<div class="container repo repo-build" ng-show="accessDenied">
<div class="cor-container repo repo-build" ng-show="accessDenied">
You do not have permission to view this page
</div>
<div class="container repo repo-build" ng-show="repo">
<div class="cor-container repo repo-build" ng-show="repo">
<div class="header row">
<a href="{{ '/repository/' + repo.namespace + '/' + repo.name }}" class="back"><i class="fa fa-chevron-left"></i></a>
<h3>
@ -94,13 +94,20 @@
</div>
</div>
<div style="margin-top: 10px">
<span class="quay-spinner" ng-show="pollChannel.working"></span>
<button class="btn btn-default"
ng-show="build.phase == 'waiting' && build.resource_key"
ng-click="askCancelBuild(build)">
<i class="fa fa-times-circle" style="margin-right: 6px; display: inline-block;"></i>
Cancel Build
</button>
<button class="btn" ng-show="(build.phase == 'error' || build.phase == 'complete') && build.resource_key"
ng-class="build.phase == 'error' ? 'btn-success' : 'btn-default'"
ng-click="askRestartBuild(build)">
<i class="fa fa-refresh"></i>
Run Build Again
</button>
<span class="quay-spinner" ng-show="pollChannel.working"></span>
<span class="build-id">{{ build.id }}</span>
</div>
</div>

View file

@ -1,4 +1,4 @@
<div class="container">
<div class="cor-container">
<div class="row">
<div class="col-md-12">
<h1>Quay.io Security</h1>

299
static/partials/setup.html Normal file
View file

@ -0,0 +1,299 @@
<div>
<div class="cor-loader" ng-show="currentStep == States.LOADING"></div>
<div class="page-content" quay-show="Features.SUPER_USERS && currentStep == States.CONFIG">
<div class="cor-title">
<span class="cor-title-link"></span>
<span class="cor-title-content">Enterprise Registry Setup</span>
</div>
<div class="cor-tab-panel" style="padding: 20px;">
<div class="co-alert alert alert-info">
<span class="cor-step-bar" progress="stepProgress">
<span class="cor-step" title="Configure Database" text="1"></span>
<span class="cor-step" title="Setup Database" icon="database"></span>
<span class="cor-step" title="Container Restart" icon="refresh"></span>
<span class="cor-step" title="Create Superuser" text="2"></span>
<span class="cor-step" title="Configure Registry" text="3"></span>
<span class="cor-step" title="Validate Configuration" text="4"></span>
<span class="cor-step" title="Container Restart" icon="refresh"></span>
<span class="cor-step" title="Setup Complete" icon="check"></span>
</span>
<div><strong>Almost done!</strong></div>
<div>Configure your Redis database and other settings below</div>
</div>
<div class="config-setup-tool" is-active="isStep(currentStep, States.CONFIG)"
configuration-saved="configurationSaved(config)"></div>
</div>
</div>
</div>
<!-- Modal message dialog -->
<div class="co-dialog modal fade initial-setup-modal" id="setupModal">
<div class="modal-dialog">
<div class="modal-content">
<!-- Header -->
<div class="modal-header">
<span class="cor-step-bar" progress="stepProgress">
<span class="cor-step" title="Configure Database" text="1"></span>
<span class="cor-step" title="Setup Database" icon="database"></span>
<span class="cor-step" title="Container Restart" icon="refresh"></span>
<span class="cor-step" title="Create Superuser" text="2"></span>
<span class="cor-step" title="Configure Registry" text="3"></span>
<span class="cor-step" title="Validate Configuration" text="4"></span>
<span class="cor-step" title="Container Restart" icon="refresh"></span>
<span class="cor-step" title="Setup Complete" icon="check"></span>
</span>
<h4 class="modal-title"><span><span class="registry-name" is-short="true"></span> Setup</h4>
</div>
<form id="superuserForm" name="superuserForm" ng-submit="createSuperUser()">
<!-- Content: CREATE_SUPERUSER or SUPERUSER_ERROR or CREATING_SUPERUSER -->
<div class="modal-body config-setup-tool-element" style="padding: 20px"
ng-show="isStep(currentStep, States.CREATE_SUPERUSER, States.SUPERUSER_ERROR, States.CREATING_SUPERUSER)">
<p>A superuser is the main administrator of your <span class="registry-name" is-short="true"></span>. Only superusers can edit configuration settings.</p>
<div class="form-group">
<label>Username</label>
<input class="form-control" type="text" ng-model="superUser.username"
ng-pattern="/^[a-z0-9_]{4,30}$/" required>
<div class="help-text">Minimum 4 characters in length</div>
</div>
<div class="form-group">
<label>Email address</label>
<input class="form-control" type="email" ng-model="superUser.email" required>
</div>
<div class="form-group">
<label>Password</label>
<input class="form-control" type="password" ng-model="superUser.password"
ng-pattern="/^[^\s]+$/"
ng-minlength="8" required>
<div class="help-text">Minimum 8 characters in length</div>
</div>
<div class="form-group">
<label>Repeat Password</label>
<input class="form-control" type="password" ng-model="superUser.repeatPassword"
match="superUser.password" required>
</div>
</div>
<!-- Footer: CREATE_SUPERUSER or SUPERUSER_ERROR -->
<div class="modal-footer"
ng-show="isStep(currentStep, States.CREATE_SUPERUSER, States.SUPERUSER_ERROR)">
<button type="submit" class="btn btn-primary" ng-disabled="!superuserForm.$valid">
Create Super User
</button>
</div>
</form>
<!-- Content: DB_RESTARTING or CONFIG_RESTARTING -->
<div class="modal-body" style="padding: 20px;"
ng-show="isStep(currentStep, States.DB_RESTARTING, States.CONFIG_RESTARTING)">
<h4 style="margin-bottom: 20px;">
<i class="fa fa-lg fa-refresh" style="margin-right: 10px;"></i>
<span class="registry-name"></span> is currently being restarted
</h4>
This can take several minutes. If the container does not restart on its own,
please re-execute the <code>docker run</code> command.
</div>
<!-- Content: READY -->
<div class="modal-body" style="padding: 20px;"
ng-show="isStep(currentStep, States.READY)">
<h4>Installation and setup of <span class="registry-name"></span> is complete</h4>
You can now invite users to join, create organizations and start pushing and pulling
repositories.
<strong ng-if="hasSSL" style="margin-top: 20px;">
Note: SSL is enabled. Please make sure to visit with
an <u>https</u> prefix
</strong>
</div>
<!-- Content: VALID_CONFIG -->
<div class="modal-body" style="padding: 20px;"
ng-show="isStep(currentStep, States.VALID_CONFIG)">
<h4>All configuration has been validated and saved</h4>
The container must be restarted to apply the configuration changes.
</div>
<!-- Content: DB_SETUP_SUCCESS -->
<div class="modal-body" style="padding: 20px;"
ng-show="isStep(currentStep, States.DB_SETUP_SUCCESS)">
<h4>The database has been setup and is ready</h4>
The container must be restarted to apply the configuration changes.
</div>
<!-- Content: DB_SETUP or DB_SETUP_ERROR -->
<div class="modal-body" style="padding: 20px;"
ng-show="isStep(currentStep, States.DB_SETUP, States.DB_SETUP_ERROR)">
<h4>
<i class="fa fa-lg fa-database" style="margin-right: 10px;"></i>
<span class="registry-name"></span> is currently setting up its database
schema
</h4>
This can take several minutes.
</div>
<!-- Content: CONFIG_DB or DB_ERROR or VALIDATING_DB or SAVING_DB -->
<div class="modal-body validate-database config-setup-tool-element"
ng-show="isStep(currentStep, States.CONFIG_DB, States.DB_ERROR, States.VALIDATING_DB, States.SAVING_DB)">
<p>
Please enter the connection details for your <strong>empty</strong> database. The schema will be created in the following step.</p>
</p>
<div class="config-parsed-field" binding="databaseUri"
parser="parseDbUri(value)"
serializer="serializeDbUri(fields)">
<table class="config-table">
<tr>
<td class="non-input">Database Type:</td>
<td>
<select ng-model="fields.kind">
<option value="mysql+pymysql">MySQL</option>
<option value="postgresql">Postgres</option>
</select>
</td>
</tr>
<tr ng-show="fields.kind">
<td>Database Server:</td>
<td>
<span class="config-string-field" binding="fields.server"
placeholder="dbserverhost"
pattern="{{ HOSTNAME_REGEX }}"
validator="validateHostname(value)">></span>
<div class="help-text">
The server (and optionally, custom port) where the database lives
</div>
</td>
</tr>
<tr ng-show="fields.kind">
<td>Username:</td>
<td>
<span class="config-string-field" binding="fields.username"
placeholder="someuser"></span>
<div class="help-text">This user must have <strong>full access</strong> to the database</div>
</td>
</tr>
<tr ng-show="fields.kind">
<td>Password:</td>
<td>
<input class="form-control" type="password" ng-model="fields.password"></span>
</td>
</tr>
<tr ng-show="fields.kind">
<td>Database Name:</td>
<td>
<span class="config-string-field" binding="fields.database"
placeholder="registry-database"></span>
</td>
</tr>
</table>
</div>
</div>
<!-- Footer: CREATING_SUPERUSER -->
<div class="modal-footer working" ng-show="isStep(currentStep, States.CREATING_SUPERUSER)">
<span class="cor-loader-inline"></span> Creating superuser...
</div>
<!-- Footer: SUPERUSER_ERROR -->
<div class="modal-footer alert alert-warning"
ng-show="isStep(currentStep, States.SUPERUSER_ERROR)">
{{ errors.SuperuserCreationError }}
</div>
<!-- Footer: DB_SETUP_ERROR -->
<div class="modal-footer alert alert-warning"
ng-show="isStep(currentStep, States.DB_SETUP_ERROR)">
Database Setup Failed. Please report this to support: {{ errors.DatabaseSetupError }}
</div>
<!-- Footer: DB_ERROR -->
<div class="modal-footer alert alert-warning" ng-show="isStep(currentStep, States.DB_ERROR)">
Database Validation Issue: {{ errors.DatabaseValidationError }}
</div>
<!-- Footer: CONFIG_DB or DB_ERROR -->
<div class="modal-footer"
ng-show="isStep(currentStep, States.CONFIG_DB, States.DB_ERROR)">
<span class="left-align" ng-show="isStep(currentStep, States.DB_ERROR)">
<i class="fa fa-warning"></i>
Problem Detected
</span>
<button type="submit" class="btn btn-primary"
ng-disabled="!databaseUri"
ng-click="validateDatabase()">
Validate Database Settings
</button>
</div>
<!-- Footer: READY -->
<div class="modal-footer"
ng-show="isStep(currentStep, States.READY)">
<span class="left-align">
<i class="fa fa-check"></i>
Installation Complete!
</span>
<a href="javascript:void(0)" ng-click="showSuperuserPanel()" class="btn btn-primary">
View Superuser Panel
</a>
</div>
<!-- Footer: VALID_CONFIG -->
<div class="modal-footer"
ng-show="isStep(currentStep, States.VALID_CONFIG)">
<span class="left-align">
<i class="fa fa-check"></i>
Configuration Validated and Saved
</span>
<button type="submit" class="btn btn-primary"
ng-click="restartContainer(States.CONFIG_RESTARTING)">
Restart Container
</button>
</div>
<!-- Footer: DB_SETUP_SUCCESS -->
<div class="modal-footer"
ng-show="isStep(currentStep, States.DB_SETUP_SUCCESS)">
<span class="left-align">
<i class="fa fa-check"></i>
Database Setup and Ready
</span>
<button type="submit" class="btn btn-primary"
ng-click="restartContainer(States.DB_RESTARTING)">
Restart Container
</button>
</div>
<!-- Footer: DB_SETUP -->
<div class="modal-footer working" ng-show="isStep(currentStep, States.DB_SETUP)">
<span class="cor-loader-inline"></span> Setting up database...
</div>
<!-- Footer: SAVING_DB -->
<div class="modal-footer working" ng-show="isStep(currentStep, States.SAVING_DB)">
<span class="cor-loader-inline"></span> Saving database configuration...
</div>
<!-- Footer: VALIDATING_DB -->
<div class="modal-footer working" ng-show="isStep(currentStep, States.VALIDATING_DB)">
<span class="cor-loader-inline"></span> Testing database settings...
</div>
<!-- Footer: DB_RESTARTING or CONFIG_RESTARTING-->
<div class="modal-footer working"
ng-show="isStep(currentStep, States.DB_RESTARTING, States.CONFIG_RESTARTING)">
<span class="cor-loader-inline"></span> Waiting for container to restart...
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->

View file

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

View file

@ -1,40 +1,92 @@
<div class="container" quay-show="Features.SUPER_USERS && showInterface">
<div class="alert alert-info">
This panel provides administrator access to <strong>super users of this installation of the registry</strong>. Super users can be managed in the configuration for this installation.
</div>
<div class="row">
<!-- Side tabs -->
<div class="col-md-2">
<ul class="nav nav-pills nav-stacked">
<li class="active">
<a href="javascript:void(0)" data-toggle="tab" data-target="#users" ng-click="loadUsers()">Users</a>
</li>
<li>
<a href="javascript:void(0)" data-toggle="tab" data-target="#create-user">Create User</a>
</li>
<li>
<a href="javascript:void(0)" data-toggle="tab" data-target="#usage-counter" ng-click="getUsage()">System Usage</a>
</li>
<li>
<a href="javascript:void(0)" data-toggle="tab" data-target="#logs" ng-click="loadLogs()">System Logs</a>
</li>
</ul>
<div>
<div class="cor-loader" ng-show="!configStatus"></div>
<div class="page-content" quay-show="Features.SUPER_USERS && configStatus == 'ready'">
<div ng-if="requiresRestart" class="alert alert-warning restart-required">
<button class="btn btn-warning" ng-click="restartContainer()">
<i class="fa fa-refresh"></i>Restart Now
</button>
<i class="fa fa-lg fa-warning"></i>
<div><strong>Container restart required!</strong></div>
Configuration changes have been made but the container hasn't been restarted yet.
</div>
<div class="cor-title">
<span class="cor-title-link"></span>
<span class="cor-title-content">Enterprise Registry Management</span>
</div>
<!-- Content -->
<div class="col-md-10">
<div class="tab-content">
<div class="cor-tab-panel">
<div class="cor-tabs">
<span class="cor-tab" tab-active="true" tab-title="Manage Users"
tab-target="#users" tab-init="loadUsers()">
<i class="fa fa-group"></i>
</span>
<span class="cor-tab" tab-title="Dashboard" tab-target="#dashboard"
tab-shown="setDashboardActive(true)" tab-hidden="setDashboardActive(false)">
<i class="fa fa-tachometer"></i>
</span>
<span class="cor-tab" tab-title="Container Usage" tab-target="#usage-counter" tab-init="getUsage()">
<i class="fa fa-pie-chart"></i>
</span>
<span class="cor-tab" tab-title="Usage Logs" tab-target="#logs" tab-init="loadUsageLogs()">
<i class="fa fa-bar-chart"></i>
</span>
<span class="cor-tab" tab-title="Internal Logs and Debugging" tab-target="#debug" tab-init="loadDebugServices()">
<i class="fa fa-bug"></i>
</span>
<span class="cor-tab" tab-title="Registry Settings" tab-target="#setup"
tab-init="loadConfig()">
<i class="fa fa-cog"></i>
</span>
</div> <!-- /cor-tabs -->
<div class="cor-tab-content">
<!-- Setup tab -->
<div id="setup" class="tab-pane">
<div class="config-setup-tool" is-active="configStatus == 'ready'"
configuration-saved="configurationSaved()"></div>
</div>
<!-- Dashboard tab -->
<div id="dashboard" class="tab-pane">
<div class="ps-usage-graph" is-enabled="dashboardActive"></div>
</div>
<!-- Debugging tab -->
<div id="debug" class="tab-pane">
<div class="cor-loader" ng-show="!debugServices"></div>
<div role="tabpanel" ng-show="debugServices">
<!-- Nav tabs -->
<ul class="nav nav-tabs" role="tablist">
<li role="presentation" ng-repeat="service in debugServices"
ng-class="debugService == service ? 'active' : ''">
<a href="javascript:void(0)" ng-click="viewSystemLogs(service)">{{ service }}</a>
</li>
</ul>
<div class="system-log-download-panel" ng-if="!debugService">
Select a service above to view its local logs
<div>
<a class="btn btn-primary" href="/systemlogsarchive?_csrf_token={{ csrf_token }}" target="_blank">
<i class="fa fa-download fa-lg" style="margin-right: 4px;"></i> Download All Local Logs (.tar.gz)
</a>
</div>
</div>
<div class="cor-log-box" logs="debugLogs" ng-show="debugService"></div>
</div>
</div>
<!-- Logs tab -->
<div id="logs" class="tab-pane">
<div class="logsView" makevisible="logsCounter" all-logs="true"></div>
</div>
</div> <!-- /logs tab-->
<!-- Usage tab -->
<div id="usage-counter" class="tab-pane">
<div class="quay-spinner" ng-show="systemUsage == null"></div>
<div class="cor-loader" ng-show="systemUsage == null"></div>
<div class="usage-chart" total="systemUsage.allowed" limit="systemUsageLimit"
current="systemUsage.usage" usage-title="Deployed Repositories"></div>
current="systemUsage.usage" usage-title="Deployed Containers"></div>
<!-- Alerts -->
<div class="alert alert-danger" ng-show="systemUsageLimit == 'over' && systemUsage">
@ -51,46 +103,13 @@
You are nearing the number of allowed deployed repositories. It might be time to think about
upgrading your subscription by contacting <a href="mailto:sales@coreos.com">CoreOS Sales</a>.
</div>
</div>
<!-- Create user tab -->
<div id="create-user" class="tab-pane">
<span class="quay-spinner" ng-show="creatingUser"></span>
<form name="createUserForm" ng-submit="createUser()" ng-show="!creatingUser">
<div class="form-group">
<label>Username</label>
<input class="form-control" type="text" ng-model="newUser.username" ng-pattern="/^[a-z0-9_]{4,30}$/" required>
</div>
<div class="form-group">
<label>Email address</label>
<input class="form-control" type="email" ng-model="newUser.email" required>
</div>
<button class="btn btn-primary" type="submit" ng-disabled="!createUserForm.$valid">Create User</button>
</form>
<div style="margin-top: 20px; padding-top: 20px; border-top: 1px solid #eee;" ng-show="createdUsers.length">
<table class="table">
<thead>
<th>Username</th>
<th>E-mail address</th>
<th>Temporary Password</th>
</thead>
<tr ng-repeat="created_user in createdUsers"
class="user-row">
<td>{{ created_user.username }}</td>
<td>{{ created_user.email }}</td>
<td>{{ created_user.password }}</td>
</tr>
</table>
</div>
</div>
For more information: <a href="https://coreos.com/products/enterprise-registry/plans/">See Here</a>.
</div> <!-- /usage-counter tab-->
<!-- Users tab -->
<div id="users" class="tab-pane active">
<div class="quay-spinner" ng-show="!users"></div>
<div class="cor-loader" ng-show="!users"></div>
<div class="alert alert-error" ng-show="usersError">
{{ usersError }}
</div>
@ -103,105 +122,186 @@
<div class="filter-input">
<input id="log-filter" class="form-control" placeholder="Filter Users" type="text" ng-model="search.$">
</div>
<button class="btn btn-primary" style="vertical-align: top; margin-left: 10px;"
ng-click="showCreateUser()">
<i class="fa fa-plus" style="margin-right: 6px;"></i>Create User
</button>
</div>
<table class="table">
<thead>
<th style="width: 24px;"></th>
<th>Username</th>
<th>E-mail address</th>
<th style="width: 24px;"></th>
</thead>
<tr ng-repeat="current_user in (users | filter:search | orderBy:'username' | limitTo:100)"
class="user-row"
ng-class="current_user.super_user ? 'super-user' : ''">
class="user-row">
<td>
<i class="fa fa-user" style="margin-right: 6px"></i>
<span class="avatar" hash="current_user.avatar" size="24"></span>
</td>
<td>
<span class="labels">
<span class="label label-default" ng-if="user.username == current_user.username">
You
</span>
<span class="label label-primary"
ng-if="current_user.super_user">
Superuser
</span>
</span>
{{ current_user.username }}
</td>
<td>
<a href="mailto:{{ current_user.email }}">{{ current_user.email }}</a>
</td>
<td style="text-align: center;">
<i class="fa fa-ge fa-lg" ng-if="current_user.super_user" data-title="Super User" bs-tooltip></i>
<div class="dropdown" style="text-align: left;" ng-if="user.username != current_user.username && !current_user.super_user">
<button class="btn btn-default dropdown-toggle" data-toggle="dropdown">
<i class="caret"></i>
</button>
<ul class="dropdown-menu pull-right">
<li>
<a href="javascript:void(0)" ng-click="showChangePassword(current_user)">
<i class="fa fa-key"></i> Change Password
</a>
<a href="javascript:void(0)" ng-click="sendRecoveryEmail(current_user)" quay-show="Features.MAILING">
<i class="fa fa-envelope"></i> Send Recovery Email
</a>
<a href="javascript:void(0)" ng-click="showDeleteUser(current_user)">
<i class="fa fa-times"></i> Delete User
</a>
</li>
</ul>
</div>
<span class="cor-options-menu"
ng-if="user.username != current_user.username && !current_user.super_user">
<span class="cor-option" option-click="showChangePassword(current_user)">
<i class="fa fa-key"></i> Change Password
</span>
<span class="cor-option" option-click="sendRecoveryEmail(current_user)"
quay-show="Features.MAILING">
<i class="fa fa-envelope"></i> Send Recovery Email
</span>
<span class="cor-option" option-click="showDeleteUser(current_user)">
<i class="fa fa-times"></i> Delete User
</span>
</span>
</td>
</tr>
</table>
</div> <!-- /show if users -->
</div> <!-- users-tab -->
</div> <!-- /cor-tab-content -->
</div> <!-- /cor-tab-panel -->
<!-- Modal message dialog -->
<div class="co-dialog modal fade" id="confirmDeleteUserModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">Delete User?</h4>
</div>
</div>
</div>
</div>
</div>
<!-- Modal message dialog -->
<div class="modal fade" id="confirmDeleteUserModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">Delete User?</h4>
</div>
<div class="modal-body">
<div class="alert alert-danger">
This operation <strong>cannot be undone</strong> and will <strong>delete any repositories owned by the user</strong>.
<div class="modal-body">
<div class="alert alert-danger">
This operation <strong>cannot be undone</strong> and will <strong>delete any repositories owned by the user</strong>.
</div>
Are you <strong>sure</strong> you want to delete user <strong>{{ userToDelete.username }}</strong>?
</div>
Are you <strong>sure</strong> you want to delete user <strong>{{ userToDelete.username }}</strong>?
</div>
<div class="modal-footer">
<button type="button" class="btn btn-danger" ng-click="deleteUser(userToDelete)">Delete User</button>
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
<!-- Modal message dialog -->
<div class="modal fade" id="changePasswordModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">Change User Password</h4>
</div>
<div class="modal-body">
<div class="alert alert-warning">
The user will no longer be able to access the registry with their current password
<div class="modal-footer">
<button type="button" class="btn btn-danger" ng-click="deleteUser(userToDelete)">Delete User</button>
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
<form class="form-change" id="changePasswordForm" name="changePasswordForm" data-trigger="manual">
<input type="password" class="form-control" placeholder="User's new password" ng-model="userToChange.password" required ng-pattern="/^.{8,}$/">
<input type="password" class="form-control" placeholder="Verify the new password" ng-model="userToChange.repeatPassword"
match="userToChange.password" required ng-pattern="/^.{8,}$/">
<!-- Modal message dialog -->
<div class="co-dialog modal fade" id="createUserModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">Create New User</h4>
</div>
<form name="createUserForm" ng-submit="createUser()">
<div class="modal-body" ng-show="createdUser">
<table class="table">
<thead>
<th>Username</th>
<th>E-mail address</th>
<th>Temporary Password</th>
</thead>
<tr class="user-row">
<td>{{ createdUser.username }}</td>
<td>{{ createdUser.email }}</td>
<td>{{ createdUser.password }}</td>
</tr>
</table>
</div>
<div class="modal-body" ng-show="creatingUser">
<div class="cor-loader"></div>
</div>
<div class="modal-body" ng-show="!creatingUser && !createdUser">
<div class="form-group">
<label>Username</label>
<input class="form-control" type="text" ng-model="newUser.username" ng-pattern="/^[a-z0-9_]{4,30}$/" required>
</div>
<div class="form-group">
<label>Email address</label>
<input class="form-control" type="email" ng-model="newUser.email" required>
</div>
</div>
<div class="modal-footer" ng-show="createdUser">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
</div>
<div class="modal-footer" ng-show="!creatingUser && !createdUser">
<button class="btn btn-primary" type="submit" ng-disabled="!createUserForm.$valid">
Create User
</button>
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" ng-click="changeUserPassword(userToChange)"
ng-disabled="changePasswordForm.$invalid">Change User Password</button>
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
<!-- Modal message dialog -->
<div class="co-dialog modal fade" id="restartingContainerModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">Container Currently Restarting</h4>
</div>
<div class="modal-body" style="padding: 20px;">
<i class="fa fa-lg fa-refresh" style="margin-right: 10px;"></i>
<span class="registry-name"></span> is currently being restarted.
<br><br>
This can take several minutes. If the container does not restart on its own,
please reexecute the <code>docker run</code> command.
</div>
<div class="modal-footer working">
<span class="cor-loader-inline"></span> Waiting for container to restart...
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
<!-- Modal message dialog -->
<div class="co-dialog modal fade" id="changePasswordModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">Change User Password</h4>
</div>
<div class="modal-body">
<div class="alert alert-warning">
The user will no longer be able to access the registry with their current password
</div>
<form class="form-change" id="changePasswordForm" name="changePasswordForm" data-trigger="manual">
<input type="password" class="form-control" placeholder="User's new password" ng-model="userToChange.password" required ng-pattern="/^.{8,}$/">
<input type="password" class="form-control" placeholder="Verify the new password" ng-model="userToChange.repeatPassword"
match="userToChange.password" required ng-pattern="/^.{8,}$/">
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" ng-click="changeUserPassword(userToChange)"
ng-disabled="changePasswordForm.$invalid">Change User Password</button>
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
</div> <!-- /page-content -->
</div>

View file

@ -1,8 +1,8 @@
<div class="resource-view" resource="orgResource" error-message="'No matching organization'">
<div class="team-view container">
<div class="team-view cor-container">
<div class="organization-header" organization="organization" team-name="teamname">
<div ng-show="canEditMembers" class="side-controls">
<div class="hidden-sm hidden-xs">
<div class="hidden-xs">
<button class="btn btn-success"
id="showAddMember"
data-title="Add Team Member"
@ -82,7 +82,7 @@
</table>
<div ng-show="canEditMembers">
<div ng-if-media="'(max-width: 560px)'">
<div ng-if-media="'(max-width: 767px)'">
<div ng-include="'/static/directives/team-view-add.html'"></div>
</div>
</div>

View file

@ -6,6 +6,6 @@
</ul>
</div>
<div class="container">
<div class="cor-container">
<div class="tour-content" kind="kind"></div>
</div>

View file

@ -1,3 +1,3 @@
<div class="container">
<div class="cor-container">
<div class="angular-tour-ui" tour="tour" inline="true"></div>
</div>

View file

@ -6,7 +6,7 @@
No matching user found
</div>
<div class="user-admin container" ng-show="!user.anonymous">
<div class="user-admin cor-container" ng-show="!user.anonymous">
<div class="row">
<div class="organization-header-element">
<span class="avatar" size="24" hash="user.avatar"></span>

View file

@ -1,4 +1,4 @@
<div class="container">
<div class="cor-container">
<div ng-show="!user.anonymous && user.verified">
<h3>Welcome <b>{{ user.username }}</b>. Your account is fully activated!</h3>

View file

@ -1,8 +1,8 @@
<div class="container">
<div class="alert alert-info" style="padding: 4px;">
<div class="checkbox">
<label>
<input name="showSudo" type="checkbox" ng-model="tour.tourScope.showSudo" style="display: inline-block; margin-left: 0px; margin-right: 10px;">
<input name="showSudo" type="checkbox" ng-model="tour.tourScope.showSudo" style="display: inline-block; margin-left: 10px; margin-right: 10px;">
<label for="showSudo" style="padding-left: 30px;">
My OS requires me to run all <code>docker</code> commands with <code>sudo</code>
</label>
</div>