Merge branch 'master' into no-signing-whitelist
This commit is contained in:
commit
45bf7efc84
434 changed files with 10877 additions and 11061 deletions
|
@ -973,13 +973,18 @@ a:focus {
|
|||
table-layout: fixed;
|
||||
}
|
||||
|
||||
.co-fixed-table .co-flowing-col{
|
||||
.co-fixed-table .co-flowing-col {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
padding-left: 16px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.co-fixed-table .nowrap-col {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.co-table td {
|
||||
border-bottom: 1px solid #eee;
|
||||
padding: 10px;
|
||||
|
@ -1239,6 +1244,7 @@ a:focus {
|
|||
|
||||
.co-top-bar {
|
||||
height: 50px;
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
|
||||
.co-check-bar .co-checked-actions .btn {
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
/*
|
||||
A list of useful mixins
|
||||
*/
|
||||
|
||||
@mixin box-shadow($args...) {
|
||||
-webkit-box-shadow: $args;
|
||||
-moz-box-shadow: $args;
|
||||
box-shadow: $args;
|
||||
-o-box-shadow: $args;
|
||||
}
|
|
@ -1,50 +0,0 @@
|
|||
.rp-description {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.rp-throbber {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.rp-panelBody {
|
||||
padding: 15px 30px;
|
||||
}
|
||||
|
||||
.rp-tabs {
|
||||
border-bottom: 1px solid #DDD;
|
||||
}
|
||||
.rp-tabs > li.active > a,
|
||||
.rp-tabs > li.active > a:focus,
|
||||
.rp-tabs > li.active > a:hover {
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
.rp-tabs {
|
||||
padding: 0 15px;
|
||||
font-size: 20px;
|
||||
|
||||
li.active a {
|
||||
color: #51a3d9;
|
||||
border-bottom: 1px solid #51a3d9;
|
||||
|
||||
&:hover {
|
||||
color: #51a3d9;
|
||||
border-bottom: 1px solid #51a3d9;
|
||||
}
|
||||
}
|
||||
|
||||
li a {
|
||||
color: #333;
|
||||
border-bottom: 1px solid #DDD;
|
||||
&:focus,
|
||||
&:hover {
|
||||
border: 1px solid #fff;
|
||||
border-bottom: 1px solid #ddd;
|
||||
background-color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.rp-tagSpan {
|
||||
margin: 0 2px;
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
.rp-button {
|
||||
float: right;
|
||||
margin-right: 30px;
|
||||
}
|
||||
|
||||
.rp-button__dropdown {
|
||||
background-color: #fff;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.25), 0 0 1px 0 rgba(0, 0, 0, 0.5), inset 0 1px 0 0 rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.rp-button__text {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.rp-button__text--bold {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
|
||||
.rp-header__row {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.rp-title {
|
||||
font-size: 24px;
|
||||
color: #333;
|
||||
float: left;
|
||||
}
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
// Repo Page specific styles here
|
||||
@import "../mixins";
|
||||
@import "body";
|
||||
@import "header";
|
||||
@import "sidebar";
|
||||
|
||||
.rp-header {
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
.rp-mainPanel {
|
||||
margin-bottom: 20px;
|
||||
background-color: #fff;
|
||||
@include box-shadow(0px 2px 2px rgba(0, 0, 0, 0.4));
|
||||
overflow: hidden;
|
||||
display: table;
|
||||
|
||||
[class*="col-"] {
|
||||
float: none;
|
||||
display: table-cell;
|
||||
vertical-align: top;
|
||||
}
|
||||
}
|
||||
|
||||
.rp-main {
|
||||
padding: 0;
|
||||
border-right: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.rp-sidebar {
|
||||
padding: 30px 30px 0 30px;
|
||||
}
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
.rp-badge {
|
||||
float: left;
|
||||
width: 100%;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.rp-badge__icon {
|
||||
float: left;
|
||||
height: 25px;
|
||||
font-size: 16px;
|
||||
padding: 0 12px;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.rp-badge__icon--private {
|
||||
@extend .rp-badge__icon;
|
||||
background-color: #d64456;
|
||||
}
|
||||
|
||||
.rp-badge__icon--public {
|
||||
@extend .rp-badge__icon;
|
||||
background-color: #2fc98e;
|
||||
}
|
||||
|
||||
.rp-imagesHeader {
|
||||
font-size: 18px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.rp-imagesTable {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.rp-imagesTable__headerCell {
|
||||
font-size: 13px;
|
||||
font-weight: 300;
|
||||
font-style: normal;
|
||||
color: #999;
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.rp-imagesTable__tagIcon {
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.rp-sharing {
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
|
@ -43,7 +43,7 @@
|
|||
|
||||
.repo-panel-tags-element .image-track-line.start {
|
||||
top: 18px;
|
||||
height: 25px;
|
||||
height: 28px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
@ -144,6 +144,10 @@
|
|||
padding-top: 0px;
|
||||
}
|
||||
|
||||
.repo-panel-tags-element .signing-delegations-list {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
@media (max-width: 1000px) {
|
||||
.repo-panel-tags-element .image-track {
|
||||
display: none;
|
||||
|
|
|
@ -72,4 +72,10 @@
|
|||
|
||||
.app-public-view-element .co-panel .co-panel-heading i.fa {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.app-public-view-element .co-tab-panel {
|
||||
margin: 0px;
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
cor-tabs {
|
||||
.vertical cor-tabs {
|
||||
display: table-cell;
|
||||
float: none;
|
||||
vertical-align: top;
|
||||
|
@ -6,6 +6,37 @@ cor-tabs {
|
|||
border-right: 1px solid #DDE7ED;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.horizontal-label {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.vertical .horizontal-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.horizontal .horizontal-label {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.horizontal .horizontal-label {
|
||||
margin-left: 10px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.horizontal .cor-tab-itself {
|
||||
font-size: 18px;
|
||||
padding: 6px;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.horizontal .cor-tab-itself i.fa {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.co-tab-container {
|
||||
padding: 0px;
|
||||
}
|
||||
|
@ -18,7 +49,14 @@ cor-tabs {
|
|||
vertical-align: top;
|
||||
}
|
||||
|
||||
.co-tab-content {
|
||||
.horizontal .co-tab-content {
|
||||
width: 100%;
|
||||
display: block;
|
||||
float: none;
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
.vertical .co-tab-content {
|
||||
width: 100%;
|
||||
display: table-cell;
|
||||
float: none;
|
||||
|
@ -68,6 +106,10 @@ cor-tabs {
|
|||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.vertical cor-tabs {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.co-tabs {
|
||||
display: block;
|
||||
width: auto;
|
||||
|
@ -78,6 +120,16 @@ cor-tabs {
|
|||
cor-tabs {
|
||||
position: relative;
|
||||
display: block;
|
||||
float: none;
|
||||
vertical-align: top;
|
||||
background-color: #e8f1f6;
|
||||
border-right: 1px solid #DDE7ED;
|
||||
}
|
||||
|
||||
.co-tab-element li.active {
|
||||
background-color: white;
|
||||
border-right: 1px solid white;
|
||||
margin-right: -1px;
|
||||
}
|
||||
|
||||
.co-tab-element .xs-toggle {
|
||||
|
@ -106,24 +158,46 @@ cor-tabs {
|
|||
font-family: FontAwesome;
|
||||
}
|
||||
|
||||
.co-tab-element .xs-label {
|
||||
line-height: 60px;
|
||||
.co-tab-element .horizontal-label {
|
||||
font-size: 16px;
|
||||
margin-left: 16px;
|
||||
display: inline-block !important;
|
||||
color: gray;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.co-tabs li a {
|
||||
.cor-tab-itself {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.cor-tab-icon {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.co-top-tab-bar {
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.co-top-tab-bar li {
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.co-tabs li a, .co-top-tab-bar li a {
|
||||
display: inline-block;
|
||||
height: 60px;
|
||||
line-height: 60px;
|
||||
line-height: 54px;
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
padding-left: 20px;
|
||||
text-decoration: none !important;
|
||||
font-size: 28px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.co-top-tab {
|
||||
display: block !important;
|
||||
border-bottom: 0px !important;
|
||||
}
|
||||
|
||||
.co-tabs li a i {
|
||||
|
@ -131,7 +205,7 @@ cor-tabs {
|
|||
font-size: 28px;
|
||||
}
|
||||
|
||||
.co-tabs li.active a .xs-label {
|
||||
.co-tabs li.active a .horizontal-label {
|
||||
color: black;
|
||||
}
|
||||
|
||||
|
@ -145,6 +219,7 @@ cor-tabs {
|
|||
|
||||
.co-tab-element.closed li {
|
||||
height: 0px;
|
||||
padding: 0px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
|
|
@ -66,4 +66,10 @@
|
|||
.create-external-notification-element .help-text {
|
||||
margin-top: 10px;
|
||||
color: #aaa;
|
||||
}
|
||||
}
|
||||
|
||||
#authorizeEmailModal .loading-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: 20px;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,17 @@
|
|||
.manifest-label-list-element {
|
||||
padding-left: 6px;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.manifest-label-list-element:before {
|
||||
content: "\f02c";
|
||||
font-family: FontAwesome;
|
||||
position: absolute;
|
||||
left: -22px;
|
||||
top: 0px;
|
||||
font-size: 15px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.manifest-label-list-element .none {
|
||||
|
|
|
@ -1,31 +0,0 @@
|
|||
.markdown-editor-element .wmd-panel .btn {
|
||||
background-color: #ddd;
|
||||
}
|
||||
|
||||
.markdown-editor-element .wmd-panel .btn:hover {
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
.markdown-editor-element .wmd-panel .btn:active {
|
||||
background-color: #ccc;
|
||||
}
|
||||
|
||||
.markdown-editor-element .preview-btn {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.markdown-editor-element .preview-btn.active {
|
||||
box-shadow: inset 0 3px 5px rgba(0,0,0,.125);
|
||||
}
|
||||
|
||||
.markdown-editor-element .preview-panel .markdown-view {
|
||||
border: 1px solid #eee;
|
||||
padding: 4px;
|
||||
min-height: 150px;
|
||||
}
|
||||
|
||||
.markdown-editor-element .preview-top-bar {
|
||||
height: 43px;
|
||||
line-height: 43px;
|
||||
color: #ddd;
|
||||
}
|
|
@ -1,3 +1,8 @@
|
|||
.quay-service-status-element {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.quay-service-status-indicator {
|
||||
display: inline-block;
|
||||
border-radius: 50%;
|
||||
|
@ -9,7 +14,9 @@
|
|||
}
|
||||
|
||||
.quay-service-status-description {
|
||||
vertical-align: middle;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.quay-service-status-indicator.none {
|
||||
|
|
|
@ -2,10 +2,11 @@
|
|||
text-align: center;
|
||||
display: inline-block;
|
||||
cursor: default;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tag-signing-display-element .fa {
|
||||
font-size: 18px;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.tag-signing-display-element .fa.fa-question-circle {
|
||||
|
@ -22,34 +23,153 @@
|
|||
color: #9B9B9B;
|
||||
}
|
||||
|
||||
.tag-signing-display-element .signing-valid .okay,
|
||||
.tag-signing-display-element .signing-valid .expires-soon {
|
||||
.tag-signing-display-element .signing-valid.okay-release {
|
||||
color: #2FC98E;
|
||||
}
|
||||
|
||||
|
||||
.tag-signing-display-element .signing-valid .expires-soon {
|
||||
position: relative;
|
||||
.tag-signing-display-element .signing-valid.okay {
|
||||
color: #5f9dd0;
|
||||
}
|
||||
|
||||
.tag-signing-display-element .signing-valid .expires-soon:after {
|
||||
border-radius: 50%;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
right: 0px;
|
||||
z-index: 1;
|
||||
display: inline-block;
|
||||
content: " ";
|
||||
background-color: #FCA657;
|
||||
}
|
||||
|
||||
|
||||
.tag-signing-display-element .signing-valid .expired {
|
||||
.tag-signing-display-element .signing-valid.partial-okay {
|
||||
color: #FCA657;
|
||||
}
|
||||
|
||||
.tag-signing-display-element .signing-invalid {
|
||||
color: #D64456;
|
||||
}
|
||||
}
|
||||
|
||||
.tag-signing-display-element .indicator {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tag-signing-display-element .expiring-soon {
|
||||
border-radius: 100%;
|
||||
background-color: #fbab62;
|
||||
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.tag-signing-display-element .expired {
|
||||
border-radius: 100%;
|
||||
background-color: #ec5266;
|
||||
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.tag-signing-display-element .invalid {
|
||||
border-radius: 100%;
|
||||
background-color: #ec5266;
|
||||
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
.tag-signing-display-element.extended {
|
||||
display: block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tag-signing-display-element.extended .fa {
|
||||
color: #888 !important;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.tag-signing-display-element.extended .indicator {
|
||||
font-size: 16px;
|
||||
position: absolute;
|
||||
left: -22px;
|
||||
top: 4px;
|
||||
}
|
||||
|
||||
.tag-signing-display-element.extended .delegations {
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
text-align: left;
|
||||
padding-left: 6px;
|
||||
padding-top: 6px;
|
||||
}
|
||||
|
||||
.tag-signing-display-element.extended .delegations td {
|
||||
padding: 4px;
|
||||
border: 0px;
|
||||
}
|
||||
|
||||
.tag-signing-display-element.extended .delegations .delegation {
|
||||
padding: 4px;
|
||||
background-color: #eee;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
padding-right: 6px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.tag-signing-display-element.extended .delegations .delegation:before {
|
||||
content: "\f00c";
|
||||
font-family: FontAwesome;
|
||||
margin-right: 2px;
|
||||
margin-left: 2px;
|
||||
display: inline-block;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.tag-signing-display-element.extended .delegations .delegation.okay {
|
||||
background-color: #d0deea;
|
||||
}
|
||||
|
||||
.tag-signing-display-element.extended .delegations .delegation.okay:before {
|
||||
color: #5f9dd0;
|
||||
}
|
||||
|
||||
.tag-signing-display-element.extended .delegations .delegation.default {
|
||||
background-color: #bdf1dd;
|
||||
}
|
||||
|
||||
.tag-signing-display-element.extended .delegations .delegation.default:before {
|
||||
color: #2FC98E;
|
||||
}
|
||||
|
||||
.tag-signing-display-element.extended .delegations .delegation.warning {
|
||||
background-color: #ffe0c4;
|
||||
}
|
||||
|
||||
.tag-signing-display-element.extended .delegations .delegation.warning:before {
|
||||
content: "\f12a";
|
||||
color: #FCA657;
|
||||
}
|
||||
|
||||
.tag-signing-display-element.extended .delegations .delegation.error {
|
||||
background-color: #ffcad1;
|
||||
}
|
||||
|
||||
.tag-signing-display-element.extended .delegations .delegation.error:before {
|
||||
content: "\f00d";
|
||||
color: #D64456;
|
||||
}
|
||||
|
||||
.tag-signing-display-element.extended .delegations .delegation-name {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.tag-signing-display-element.extended .delegations .delegation-info {
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
vertical-align: middle;
|
||||
margin-left: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
|
|
|
@ -7,6 +7,10 @@
|
|||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.error-view-element h2 .repo-circle {
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.error-view-element h3 {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
|
|
@ -1569,7 +1569,7 @@ p.editable:hover i {
|
|||
transition: color 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
.copy-box-element .copy-container .copy-icon.zeroclipboard-is-hover {
|
||||
.copy-box-element .copy-container .copy-icon:hover {
|
||||
color: #444;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<div class="build-logs-view-element" ng-class="useTimestamps ? 'with-timestamps' : ''">
|
||||
<span ng-show="logEntries">
|
||||
<button id="copyButton" class="btn btn-primary copy-button" data-clipboard-text="{{ buildLogsText }}">
|
||||
<button id="copyButton" class="btn btn-primary copy-button"
|
||||
clipboard-copy="#{{ ::buildLogsText }}">
|
||||
<i class="fa fa-clipboard"></i>Copy Logs
|
||||
</button>
|
||||
</span>
|
||||
|
|
|
@ -53,59 +53,6 @@
|
|||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="non-input">Anonymous Access:</td>
|
||||
<td colspan="2">
|
||||
<div class="config-bool-field" binding="config.FEATURE_ANONYMOUS_ACCESS">
|
||||
Enable Anonymous Access
|
||||
</div>
|
||||
<div class="help-text">
|
||||
If enabled, public repositories and search can be accessed by anyone that can
|
||||
reach the registry, even if they are not authenticated. Disable to only allow
|
||||
authenticated users to view and pull "public" resources.
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="non-input">User Creation:</td>
|
||||
<td colspan="2">
|
||||
<div class="config-bool-field" binding="config.FEATURE_USER_CREATION">
|
||||
Enable Open User Creation
|
||||
</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>
|
||||
<tr>
|
||||
<td class="non-input">Encrypted Client Password:</td>
|
||||
<td colspan="2">
|
||||
<div class="config-bool-field" binding="config.FEATURE_REQUIRE_ENCRYPTED_BASIC_AUTH">
|
||||
Require Encrypted Client Passwords
|
||||
</div>
|
||||
<div class="help-text">
|
||||
If enabled, users will not be able to login from the Docker command
|
||||
line with a non-encrypted password and must generate an encrypted
|
||||
password to use.
|
||||
</div>
|
||||
<div class="help-text" ng-if="config.AUTHENTICATION_TYPE != 'Database'">
|
||||
This feature is <strong>highly recommended</strong> for setups with external authentication, as Docker currently stores passwords in <strong>plaintext</strong> on user's machines.
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-show="config.FEATURE_MAILING">
|
||||
<td class="non-input">Team Invitations:</td>
|
||||
<td colspan="2">
|
||||
<div class="config-bool-field" binding="config.FEATURE_REQUIRE_TEAM_INVITE">
|
||||
Require Team Invitations
|
||||
</div>
|
||||
<div class="help-text">
|
||||
If enabled, when adding a new user to a team, they will receive an invitation to join the team, with the option to decline.
|
||||
Otherwise, users will be immediately part of a team when added by a team administrator.
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -404,6 +351,47 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action log archiving -->
|
||||
<div class="co-panel">
|
||||
<div class="co-panel-heading">
|
||||
<i class="fa fa-archive"></i> Action Log Rotation and Archiving
|
||||
</div>
|
||||
<div class="co-panel-body">
|
||||
<div class="description">
|
||||
<p>
|
||||
All actions performed in <span class="registry-name"></span> are automatically logged. These logs are stored in a database table, which can become quite large.
|
||||
Enabling log rotation and archiving will move all logs older than 30 days into storage.
|
||||
</p>
|
||||
</div>
|
||||
<div class="config-bool-field" binding="config.FEATURE_ACTION_LOG_ROTATION">
|
||||
Enable Action Log Rotation
|
||||
</div>
|
||||
|
||||
<table class="config-table" ng-if="config.FEATURE_ACTION_LOG_ROTATION">
|
||||
<tr>
|
||||
<td>Storage location:</td>
|
||||
<td>
|
||||
<select class="form-control" ng-model="config.ACTION_LOG_ARCHIVE_LOCATION">
|
||||
<option ng-repeat="sc in storageConfig" value="{{ sc['location'] }}">{{ sc['location'] }}</option>
|
||||
</select>
|
||||
<div class="help-text">
|
||||
The storage location in which to place archived action logs. Logs will only be archived to this single location.
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Storage path:</td>
|
||||
<td>
|
||||
<span class="config-string-field" binding="config.ACTION_LOG_ARCHIVE_PATH"
|
||||
placeholder="Path under storage to place archived logs"></span>
|
||||
<div class="help-text">
|
||||
The path under the configured storage engine in which to place the archived logs in JSON form.
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Security Scanner -->
|
||||
<div class="co-panel">
|
||||
<div class="co-panel-heading">
|
||||
|
@ -452,6 +440,22 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- App Registry -->
|
||||
<div class="co-panel">
|
||||
<div class="co-panel-heading">
|
||||
<i class="fa ci-appcube"></i> Application Registry
|
||||
</div>
|
||||
<div class="co-panel-body">
|
||||
<div class="description">
|
||||
<p>If enabled, an additional registry API will be available for managing applications (Kubernetes manifests, Helm charts) via the <a href="https://github.com/app-registry">App Registry specification</a>. A great place to get started is to install the <a href="https://github.com/app-registry/appr-helm-plugin">Helm Registry Plugin</a>.
|
||||
</div>
|
||||
|
||||
<div class="config-bool-field" binding="config.FEATURE_APP_REGISTRY">
|
||||
Enable App Registry
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- BitTorrent pull -->
|
||||
<div class="co-panel">
|
||||
<div class="co-panel-heading">
|
||||
|
@ -1149,6 +1153,95 @@
|
|||
</div>
|
||||
</div> <!-- /External Authentication -->
|
||||
|
||||
<!-- Access settings -->
|
||||
<div class="co-panel">
|
||||
<div class="co-panel-heading">
|
||||
<i class="fa fa-user-circle"></i> Access Settings
|
||||
</div>
|
||||
<div class="co-panel-body">
|
||||
<div class="description">
|
||||
<p>Various settings around access and authentication to the registry.</p>
|
||||
</div>
|
||||
|
||||
<table class="config-table">
|
||||
<tr>
|
||||
<td class="non-input">Basic Credentials Login:</td>
|
||||
<td colspan="2">
|
||||
<div class="config-bool-field" binding="config.FEATURE_DIRECT_LOGIN" ng-if="getOIDCProviders(config).length || config.FEATURE_GITHUB_LOGIN || config.FEATURE_GOOGLE_LOGIN">
|
||||
Login to User Interface via credentials
|
||||
</div>
|
||||
<div ng-if="!getOIDCProviders(config).length && !config.FEATURE_GITHUB_LOGIN && !config.FEATURE_GOOGLE_LOGIN">
|
||||
<div ng-if="!config.FEATURE_DIRECT_LOGIN" class="co-alert co-alert-danger">
|
||||
Login to User Interface via credentials must be enabled. <a ng-click="enableFeature(config, 'FEATURE_DIRECT_LOGIN')">Click here to enable</a>.
|
||||
</div>
|
||||
<div ng-if="config.FEATURE_DIRECT_LOGIN">
|
||||
Login to User Interface via credentials is <strong>enabled</strong> (requires at least one OIDC provider to disable)
|
||||
</div>
|
||||
</div>
|
||||
<div class="help-text">
|
||||
If enabled, users will be able to login to the <strong>user interface</strong> via their username and password credentials.
|
||||
</div>
|
||||
<div class="help-text">
|
||||
If <strong>disabled</strong>, users will only be able to login to the <strong>user interface</strong> via one of the configured External Authentication providers.
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="non-input">Anonymous Access:</td>
|
||||
<td colspan="2">
|
||||
<div class="config-bool-field" binding="config.FEATURE_ANONYMOUS_ACCESS">
|
||||
Enable Anonymous Access
|
||||
</div>
|
||||
<div class="help-text">
|
||||
If enabled, public repositories and search can be accessed by anyone that can
|
||||
reach the registry, even if they are not authenticated. Disable to only allow
|
||||
authenticated users to view and pull "public" resources.
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="non-input">User Creation:</td>
|
||||
<td colspan="2">
|
||||
<div class="config-bool-field" binding="config.FEATURE_USER_CREATION">
|
||||
Enable Open User Creation
|
||||
</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>
|
||||
<tr>
|
||||
<td class="non-input">Encrypted Client Password:</td>
|
||||
<td colspan="2">
|
||||
<div class="config-bool-field" binding="config.FEATURE_REQUIRE_ENCRYPTED_BASIC_AUTH">
|
||||
Require Encrypted Client Passwords
|
||||
</div>
|
||||
<div class="help-text">
|
||||
If enabled, users will not be able to login from the Docker command
|
||||
line with a non-encrypted password and must generate an encrypted
|
||||
password to use.
|
||||
</div>
|
||||
<div class="help-text" ng-if="config.AUTHENTICATION_TYPE != 'Database'">
|
||||
This feature is <strong>highly recommended</strong> for setups with external authentication, as Docker currently stores passwords in <strong>plaintext</strong> on user's machines.
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-show="config.FEATURE_MAILING">
|
||||
<td class="non-input">Team Invitations:</td>
|
||||
<td colspan="2">
|
||||
<div class="config-bool-field" binding="config.FEATURE_REQUIRE_TEAM_INVITE">
|
||||
Require Team Invitations
|
||||
</div>
|
||||
<div class="help-text">
|
||||
If enabled, when adding a new user to a team, they will receive an invitation to join the team, with the option to decline.
|
||||
Otherwise, users will be immediately part of a team when added by a team administrator.
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div> <!-- /Access settings -->
|
||||
|
||||
<!-- Build Support -->
|
||||
<div class="co-panel">
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
<div class="copy-box-element" ng-class="disabled ? 'disabled' : ''">
|
||||
<div class="id-container">
|
||||
<div class="copy-container">
|
||||
<input type="text" class="form-control" value="{{ value }}" readonly>
|
||||
<span class="copy-icon" data-title="Copy to Clipboard"
|
||||
data-container="body"
|
||||
data-placement="bottom"
|
||||
bs-tooltip>
|
||||
<input id="{{ ::inputId }}" type="text" class="form-control"
|
||||
value="{{ value }}"
|
||||
readonly>
|
||||
<span class="copy-icon"
|
||||
clipboard-copy="#{{ ::inputId }}"
|
||||
data-title="Copy to Clipboard" data-container="body" data-placement="bottom" bs-tooltip>
|
||||
<i class="fa fa-clipboard"></i>
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
ng-if="entity"></div>
|
||||
</div>
|
||||
<div class="modal-body" ng-show="view == 'enterName'">
|
||||
<form name="enterNameForm" ng-submit="createEntity()">
|
||||
<form name="enterNameForm" ng-submit="enterNameForm.$valid && createEntity()">
|
||||
<label>Provide a name for your new {{ entityTitle }}:</label>
|
||||
<input type="text" class="form-control" ng-model="entityName" ng-pattern="entityNameRegexObj" required>
|
||||
<div class="help-text">
|
||||
|
|
|
@ -191,43 +191,47 @@
|
|||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Authorize email dialog -->
|
||||
<div class="modal" tabindex="-1" role="dialog" id="authorizeEmailModal">
|
||||
<div class="modal-dialog">
|
||||
<!-- Authorize email dialog -->
|
||||
<div class="modal" tabindex="-1" role="dialog" id="authorizeEmailModal">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" ng-click="cancelEmailAuth()">
|
||||
×
|
||||
</button>
|
||||
<h4 class="modal-title">E-mail authorization</h4>
|
||||
</div>
|
||||
<div class="modal-content">
|
||||
<div class="modal-body" style="padding: 4px; padding-left: 20px;">
|
||||
<button type="button" class="close" ng-click="cancelEmailAuth()" style="padding: 4px;">
|
||||
×
|
||||
</button>
|
||||
|
||||
<!-- Authorize e-mail view -->
|
||||
<div ng-show="status == 'authorizing-email-sent'">
|
||||
An e-mail has been sent to <code>{{ currentConfig.email }}</code>. Please click the link contained
|
||||
in the e-mail.
|
||||
<br><br>
|
||||
<span class="cor-loader-inline"></span>
|
||||
</div>
|
||||
|
||||
<!-- Authorize e-mail view -->
|
||||
<div ng-show="status == 'unauthorized-email'">
|
||||
The e-mail address <code>{{ currentConfig.email }}</code> has not been authorized to receive
|
||||
notifications from this repository. Please click "Send Authorization E-mail" below to start
|
||||
the authorization process.
|
||||
</div>
|
||||
<div class="modal-body" style="padding: 4px; padding-left: 20px;">
|
||||
<!-- Authorize e-mail view -->
|
||||
<div ng-if="status == 'authorizing-email-sent'">
|
||||
An e-mail has been sent to <code>{{ ::currentConfig.email }}</code>. Please click the link contained
|
||||
in the e-mail.
|
||||
</div>
|
||||
|
||||
<!-- Auth e-mail button bar -->
|
||||
<div class="modal-footer" ng-if="status == 'unauthorized-email'">
|
||||
<button type="button" class="btn btn-success" ng-click="sendAuthEmail()">
|
||||
Send Authorization E-mail
|
||||
</button>
|
||||
<button type="button" class="btn btn-default" ng-click="cancelEmailAuth()" ng-disabled="creating">Cancel</button>
|
||||
<!-- Authorize e-mail view -->
|
||||
<div ng-if="status == 'unauthorized-email'">
|
||||
The e-mail address <code>{{ ::currentConfig.email }}</code> has not been authorized to receive
|
||||
notifications from this repository. Please click "Send Authorization E-mail" below to start
|
||||
the authorization process.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Auth e-mail button bar -->
|
||||
<div class="modal-footer" ng-if="status == 'unauthorized-email'">
|
||||
<button type="button" class="btn btn-success" ng-click="sendAuthEmail()">
|
||||
Send Authorization E-mail
|
||||
</button>
|
||||
<button type="button" class="btn btn-default" ng-click="cancelEmailAuth()" ng-disabled="creating">Cancel</button>
|
||||
</div>
|
||||
|
||||
<!-- Loading -->
|
||||
<div ng-if="status != 'unauthorized-email'"
|
||||
class="loading-container">
|
||||
<span class="cor-loader-inline"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<div class="cor-loader"></div>
|
||||
</div>
|
||||
<div class="co-tab-modal-body" ng-show="!credentials.loading">
|
||||
<cor-tab-panel remember-cookie="quay.credentialsTab">
|
||||
<cor-tab-panel orientation="vertical" cor-cookie-tabs="quay.credentialsTab">
|
||||
<!-- Tabs -->
|
||||
<cor-tabs>
|
||||
<cor-tab tab-active="true" tab-id="cred-secret-{{ ::dialogID }}">
|
||||
|
|
|
@ -66,17 +66,15 @@
|
|||
|
||||
<div ng-if="getCommand(currentFormat, currentRobot)">
|
||||
Command:
|
||||
<pre class="command">{{ getCommand(currentFormat, currentRobot) }}</pre>
|
||||
<pre id="command-data" class="command">{{ getCommand(currentFormat, currentRobot) }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="clipboard-copied-message" style="display: none">
|
||||
Copied
|
||||
</div>
|
||||
<input type="hidden" name="command-data" id="command-data"
|
||||
value="{{ getCommand(currentFormat, currentRobot) }}">
|
||||
<button id="copyClipboard" type="button" class="btn btn-primary"
|
||||
data-clipboard-target="command-data"
|
||||
<button type="button" class="btn btn-primary"
|
||||
clipboard-copy="#command-data"
|
||||
ng-show="getCommand(currentFormat, currentRobot)">Copy Command</button>
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||
</div>
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
<td class="message-content">
|
||||
<span ng-switch on="message.media_type">
|
||||
<span ng-switch-when="text/markdown">
|
||||
<span class="markdown-view" content="message.content"></span>
|
||||
<markdown-view content="message.content"></markdown-view>
|
||||
</span>
|
||||
<span ng-switch-default>{{ message.content }}</span>
|
||||
</span>
|
||||
|
@ -81,7 +81,10 @@
|
|||
</select>
|
||||
|
||||
<label>Message</label>
|
||||
<div class="markdown-editor" content="newMessage.content"></div>
|
||||
<markdown-input content="newMessage.content"
|
||||
can-write="true"
|
||||
(content-changed)="updateMessage($event.content)"
|
||||
field-title="message"></markdown-input>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer" ng-show="createdMessage">
|
||||
|
|
|
@ -89,14 +89,14 @@
|
|||
<i class="fa ci-robot"></i> New Robot Account
|
||||
</a>
|
||||
</li>
|
||||
<li role="presentation" class="divider" ng-if="currentPageContext.repository && currentPageContext.repository.can_write && !currentPageContext.repository.trust_enabled"></li>
|
||||
<li role="presentation" class="divider" ng-if="currentPageContext.repository && currentPageContext.repository.can_write && !currentPageContext.repository.tag_operations_disabled"></li>
|
||||
<li role="presentation" class="dropdown-header"
|
||||
ng-if="currentPageContext.repository && currentPageContext.repository.can_write &&
|
||||
!currentPageContext.repository.trust_enabled">
|
||||
!currentPageContext.repository.tag_operations_disabled">
|
||||
Repository {{ currentPageContext.repository.namespace }}/{{ currentPageContext.repository.name }}
|
||||
</li>
|
||||
<li ng-if="currentPageContext.repository && currentPageContext.repository.can_write &&
|
||||
!currentPageContext.repository.trust_enabled">
|
||||
!currentPageContext.repository.tag_operations_disabled">
|
||||
<a ng-click="startBuild()">
|
||||
<i class="fa fa-tasks"></i> New Dockerfile Build
|
||||
</a>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<!-- Comment -->
|
||||
<div class="image-comment" ng-if="imageData.comment">
|
||||
<blockquote style="margin-top: 10px;">
|
||||
<span class="markdown-view" content="imageData.comment"></span>
|
||||
<markdown-view content="imageData.comment"></markdown-view>
|
||||
</blockquote>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -45,7 +45,7 @@
|
|||
<!-- Regex -->
|
||||
<div ng-switch-when="regex">
|
||||
<input type="text" class="form-control" ng-model="parameters[field.name]"
|
||||
ng-pattern="getPattern(field)"
|
||||
ng-pattern="field.regex"
|
||||
placeholder="{{ field.placeholder }}"
|
||||
ng-name="field.name"
|
||||
id="{{ field.name }}"
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
<div class="markdown-editor-element">
|
||||
<a class="btn btn-default preview-btn" ng-click="togglePreview()" ng-class="{'active': previewing}">Preview</a>
|
||||
<div class="wmd-panel" ng-show="!previewing">
|
||||
<div id="wmd-button-bar-{{id}}"></div>
|
||||
<textarea class="wmd-input form-control" id="wmd-input-{{id}}" ng-model="content"></textarea>
|
||||
</div>
|
||||
<div class="preview-panel" ng-show="previewing">
|
||||
<div class="preview-top-bar">Viewing preview</div>
|
||||
<div class="markdown-view" content="content || '(Nothing entered)'"></div>
|
||||
</div>
|
||||
</div>
|
|
@ -1,31 +0,0 @@
|
|||
<div class="markdown-input-container">
|
||||
<p ng-class="'lead ' + (canWrite ? 'editable' : 'noteditable')" ng-click="editContent()">
|
||||
<span class="markdown-view" content="content"></span>
|
||||
<span class="empty" ng-show="!content && canWrite">Click to set {{ fieldTitle }}</span>
|
||||
<i class="fa fa-edit"></i>
|
||||
</p>
|
||||
|
||||
<!-- Modal editor -->
|
||||
<div class="modal fade">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
<h4 class="modal-title">Edit {{ fieldTitle }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="wmd-panel">
|
||||
<div id="wmd-button-bar-description-{{id}}"></div>
|
||||
<textarea class="wmd-input" id="wmd-input-description-{{id}}" placeholder="Enter {{ fieldTitle }}">{{ content }}</textarea>
|
||||
</div>
|
||||
|
||||
<div id="wmd-preview-description-{{id}}" class="wmd-panel wmd-preview"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-primary" ng-click="saveContent()">Save changes</button>
|
||||
</div>
|
||||
</div><!-- /.modal-content -->
|
||||
</div><!-- /.modal-dialog -->
|
||||
</div><!-- /.modal -->
|
||||
</div>
|
|
@ -1 +0,0 @@
|
|||
<span class="markdown-view-content" ng-bind-html="getMarkedDown(content, firstLineOnly)"></span>
|
|
@ -3,7 +3,7 @@
|
|||
<div class="quay-service-status-description" ng-class="message.severity">
|
||||
<span ng-switch on="message.media_type">
|
||||
<span ng-switch-when="text/markdown">
|
||||
<span class="markdown-view" content="message.content"></span>
|
||||
<markdown-view content="message.content"></markdown-view>
|
||||
</span>
|
||||
<span ng-switch-default>{{ message.content }}</span>
|
||||
</span>
|
||||
|
|
|
@ -41,7 +41,10 @@
|
|||
star-toggled="starToggled({'repository': repository})"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="description markdown-view" content="repository.description" first-line-only="true" placeholder-needed="true"></div>
|
||||
<markdown-view class="description"
|
||||
content="repository.description"
|
||||
first-line-only="true"
|
||||
placeholder-needed="true"></markdown-view>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
<div class="repo-panel-builds-element">
|
||||
<div class="feedback-bar" feedback="feedback"></div>
|
||||
<div class="tab-header-controls">
|
||||
<button class="btn btn-primary" ng-click="showNewBuildDialog()" ng-if="!repository.trust_enabled">
|
||||
<button class="btn btn-primary" ng-click="showNewBuildDialog()" ng-if="!repository.tag_operations_disabled">
|
||||
<i class="fa fa-play"></i> Start New Build
|
||||
</button>
|
||||
</div>
|
||||
<h3 class="tab-header">Repository Builds</h3>
|
||||
|
||||
<div class="co-alert co-alert-info" ng-if="repository.trust_enabled">
|
||||
Builds cannot be performed on this repository because Quay Content Trust is
|
||||
<div class="co-alert co-alert-info" ng-if="repository.tag_operations_disabled">
|
||||
Builds cannot be performed on this repository because Quay Trust is
|
||||
enabled, which requires that all operations be signed by a user.
|
||||
</div>
|
||||
|
||||
|
@ -83,7 +83,7 @@
|
|||
</div> <!-- /Builds -->
|
||||
|
||||
<!-- Build Triggers -->
|
||||
<div class="co-panel" ng-if="repository.can_admin && TriggerService.getTypes().length && !repository.trust_enabled" id="repoBuildTriggers">
|
||||
<div class="co-panel" ng-if="repository.can_admin && TriggerService.getTypes().length && !repository.tag_operations_disabled" id="repoBuildTriggers">
|
||||
<!-- Builds header controls -->
|
||||
<div class="co-panel-heading">
|
||||
<i class="fa fa-flash"></i>
|
||||
|
|
|
@ -32,7 +32,7 @@
|
|||
<!-- No Builds -->
|
||||
<div class="empty" ng-if="builds && !builds.length">
|
||||
<div class="empty-primary-msg">No builds have been run for this repository.</div>
|
||||
<div class="empty-secondary-msg" ng-if="repository.can_write && !repository.trust_enabled">
|
||||
<div class="empty-secondary-msg" ng-if="repository.can_write && !repository.tag_operations_disabled">
|
||||
Click on the <i class="fa fa-tasks" style="margin-left: 6px"></i> Builds tab to start a new build.
|
||||
</div>
|
||||
</div>
|
||||
|
@ -57,11 +57,11 @@
|
|||
<tr>
|
||||
<td>
|
||||
<h4 style="font-size:20px;">Description</h4>
|
||||
<div class="description markdown-input"
|
||||
content="repository.description"
|
||||
can-write="repository.can_write"
|
||||
content-changed="updateDescription"
|
||||
field-title="'repository description'">
|
||||
<div class="description">
|
||||
<markdown-input content="repository.description"
|
||||
can-write="repository.can_write"
|
||||
(content-changed)="updateDescription($event.content)"
|
||||
field-title="repository description"></markdown-input>
|
||||
</div>
|
||||
</td>
|
||||
<td style="width: 400px;" class="hidden-xs hidden-sm">
|
||||
|
|
|
@ -53,7 +53,7 @@
|
|||
</li>
|
||||
<li ng-if="repository.can_write">
|
||||
<a ng-click="askDeleteMultipleTags(checkedTags.checked)"
|
||||
ng-class="repository.trust_enabled ? 'disabled-option' : ''">
|
||||
ng-class="repository.tag_operations_disabled ? 'disabled-option' : ''">
|
||||
<i class="fa fa-times"></i><span class="text">Delete Tags</span>
|
||||
</a>
|
||||
</li>
|
||||
|
@ -131,7 +131,7 @@
|
|||
<td class="signing-col hidden-xs"
|
||||
quay-require="['SIGNING']"
|
||||
ng-if="repository.trust_enabled">
|
||||
<tag-signing-display tag="tag" signatures="repoSignatureInfo"></tag-signing-display>
|
||||
<tag-signing-display tag="tag" delegations="repoDelegationsInfo" compact="true"></tag-signing-display>
|
||||
</td>
|
||||
<td class="hidden-xs">
|
||||
<span bo-if="tag.last_modified" data-title="{{ tag.last_modified | amDateFormat:'dddd, MMMM Do YYYY, h:mm:ss a' }}" bs-tooltip>
|
||||
|
@ -243,7 +243,7 @@
|
|||
<span bo-if="repository.can_write">
|
||||
<span class="cor-options-menu">
|
||||
<span class="cor-option" option-click="askAddTag(tag)"
|
||||
ng-class="repository.trust_enabled ? 'disabled-option' : ''">
|
||||
ng-class="repository.tag_operations_disabled ? 'disabled-option' : ''">
|
||||
<i class="fa fa-plus"></i> Add New Tag
|
||||
</span>
|
||||
<span class="cor-option" option-click="showLabelEditor(tag)"
|
||||
|
@ -251,7 +251,7 @@
|
|||
<i class="fa fa-tags"></i> Edit Labels
|
||||
</span>
|
||||
<span class="cor-option" option-click="askDeleteTag(tag.name)"
|
||||
ng-class="repository.trust_enabled ? 'disabled-option' : ''">
|
||||
ng-class="repository.tag_operations_disabled ? 'disabled-option' : ''">
|
||||
<i class="fa fa-times"></i> Delete Tag
|
||||
</span>
|
||||
</span>
|
||||
|
@ -261,9 +261,15 @@
|
|||
</tr>
|
||||
<tr ng-if="expandedView">
|
||||
<td class="checkbox-col"></td>
|
||||
<td class="labels-col" colspan="{{5 + (Features.SECURITY_SCANNER ? 1 : 0)}}">
|
||||
<td class="labels-col" colspan="{{5 + (Features.SECURITY_SCANNER ? 1 : 0) + (repository.trust_enabled ? 1 : 0) }}">
|
||||
<!-- Labels -->
|
||||
<div class="manifest-label-list" repository="repository"
|
||||
manifest-digest="tag.manifest_digest" cache="labelCache"></div>
|
||||
|
||||
<!-- Delegations -->
|
||||
<div class="signing-delegations-list" ng-if="repository.trust_enabled">
|
||||
<tag-signing-display compact="false" tag="tag" delegations="repoDelegationsInfo"></tag-signing-display>
|
||||
</div>
|
||||
</td>
|
||||
<td class="hidden-xs hidden-sm image-track" ng-repeat="it in imageTracks"
|
||||
ng-if="imageTracks.length <= maxTrackCount" bindonce>
|
||||
|
|
|
@ -29,6 +29,7 @@
|
|||
<td>Title</td>
|
||||
<td>Event</td>
|
||||
<td>Notification</td>
|
||||
<td>Enabled</td>
|
||||
<td class="options-col"></td>
|
||||
</tr>
|
||||
</thead>
|
||||
|
@ -70,6 +71,11 @@
|
|||
</span>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<span ng-if="notification.number_of_failures >= 3">Disabled due to 3 failed attempts in a row</span>
|
||||
<span ng-if="notification.number_of_failures < 3">Enabled</span>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<span class="cor-options-menu">
|
||||
<span class="cor-option" option-click="testNotification(notification)">
|
||||
|
@ -93,6 +99,9 @@
|
|||
<span class="cor-option" option-click="deleteNotification(notification)">
|
||||
<i class="fa fa-times"></i> Delete Notification
|
||||
</span>
|
||||
<span ng-if="notification.number_of_failures >= 3" class="cor-option" option-click="reenableNotification(notification)">
|
||||
<i class="fa fa-adjust"></i> Re-enable Notification
|
||||
</span>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
@ -76,7 +76,10 @@
|
|||
<tr>
|
||||
<td><label for="create-key-notes">Approval Notes (optional):</label></td>
|
||||
<td>
|
||||
<div class="markdown-editor" content="preshared.notes"></div>
|
||||
<markdown-input content="preshared.notes"
|
||||
can-write="true"
|
||||
(content-changed)="updateNotes($event.content)"
|
||||
field-title="notes"></markdown-input>
|
||||
<span class="co-help-text">
|
||||
Optional notes for additional human-readable information about why the key was created.
|
||||
</span>
|
||||
|
|
|
@ -167,7 +167,7 @@
|
|||
|
||||
<div bo-if="key.approval.notes">
|
||||
<div class="subtitle">Approval notes</div>
|
||||
<div class="markdown-view" content="key.approval.notes"></div>
|
||||
<markdown-view content="key.approval.notes"></markdown-view>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
@ -251,7 +251,10 @@
|
|||
<li ng-repeat="key in approveKeysInfo.keys">{{ getKeyTitle(key) }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="markdown-editor" content="approveKeysInfo.notes"></div>
|
||||
<markdown-input content="approveKeysInfo.notes"
|
||||
can-write="true"
|
||||
(content-changed)="updateApproveKeysInfoNotes($event.content)"
|
||||
field-title="notes"></markdown-input>
|
||||
<span class="co-help-text">
|
||||
Enter optional notes for additional human-readable information about why the keys were approved.
|
||||
</span>
|
||||
|
@ -268,7 +271,10 @@
|
|||
<div style="margin-bottom: 10px;">
|
||||
Approve service key <strong>{{ getKeyTitle(approvalKeyInfo.key) }}</strong>?
|
||||
</div>
|
||||
<div class="markdown-editor" content="approvalKeyInfo.notes"></div>
|
||||
<markdown-input content="approvalKeysInfo.notes"
|
||||
can-write="true"
|
||||
(content-changed)="updateApprovalKeyInfoNotes($event.content)"
|
||||
field-title="notes"></markdown-input>
|
||||
<span class="co-help-text">
|
||||
Enter optional notes for additional human-readable information about why the key was approved.
|
||||
</span>
|
||||
|
@ -344,7 +350,10 @@
|
|||
<tr>
|
||||
<td><label for="create-key-notes">Approval Notes:</label></td>
|
||||
<td>
|
||||
<div class="markdown-editor" content="newKey.notes"></div>
|
||||
<markdown-input content="newKey.notes"
|
||||
can-write="true"
|
||||
(content-changed)="updateNewKeyNotes($event.content)"
|
||||
field-title="notes"></markdown-input>
|
||||
<span class="co-help-text">
|
||||
Optional notes for additional human-readable information about why the key was added.
|
||||
</span>
|
||||
|
|
|
@ -144,16 +144,17 @@
|
|||
manifest-digest="restoreTagInfo.manifest_digest"></span>?
|
||||
</div>
|
||||
|
||||
<!-- Trust Enabled Dialog -->
|
||||
<div class="modal fade" id="trustEnabledModal">
|
||||
<!-- Tag Operations Disabled Dialog -->
|
||||
<div class="modal fade" id="tagOperationsDisabledModal">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
Cannot execute with trust enabled
|
||||
Tag operations have been disabled.
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
The selected operation cannot be performed on this repository because Quay Content Trust is
|
||||
enabled, which requires that all operations be signed by a user.
|
||||
The selected operation cannot be performed on this repository because tag operations have been disabled
|
||||
by an administrator. <span ng-if="repository.trust_enabled">Trust is enabled for this repo, so any tag changes
|
||||
should be performed by users with signing keys.</span>
|
||||
</div>
|
||||
</div><!-- /.modal-content -->
|
||||
</div><!-- /.modal-dialog -->
|
||||
|
|
19
static/js/constants/platform.constant.ts
Normal file
19
static/js/constants/platform.constant.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
/**
|
||||
* Type representing current browser platform.
|
||||
* TODO: Add more browser platforms.
|
||||
*/
|
||||
export type BrowserPlatform = "firefox"
|
||||
| "chrome";
|
||||
|
||||
/**
|
||||
* Constant representing current browser platform. Used for determining available features.
|
||||
* TODO Only rudimentary implementation, should prefer specific feature detection strategies instead.
|
||||
*/
|
||||
export const browserPlatform: BrowserPlatform = (() => {
|
||||
if (navigator.userAgent.toLowerCase().indexOf('firefox') != -1) {
|
||||
return 'firefox';
|
||||
}
|
||||
else {
|
||||
return 'chrome';
|
||||
}
|
||||
})();
|
|
@ -23,6 +23,8 @@ angular.module("core-config-setup", ['angularFileUpload'])
|
|||
|
||||
{'id': 'time-machine', 'title': 'Time Machine'},
|
||||
|
||||
{'id': 'access', 'title': 'Access Settings'},
|
||||
|
||||
{'id': 'ssl', 'title': 'SSL certificate and key', 'condition': function(config) {
|
||||
return config.PREFERRED_URL_SCHEME == 'https';
|
||||
}},
|
||||
|
@ -78,6 +80,10 @@ angular.module("core-config-setup", ['angularFileUpload'])
|
|||
{'id': 'oidc-login', 'title': 'OIDC Login(s)', 'condition': function(config) {
|
||||
return $scope.getOIDCProviders(config).length > 0;
|
||||
}},
|
||||
|
||||
{'id': 'actionlogarchiving', 'title': 'Action Log Rotation', 'condition': function(config) {
|
||||
return config.FEATURE_ACTION_LOG_ROTATION;
|
||||
}},
|
||||
];
|
||||
|
||||
$scope.STORAGE_CONFIG_FIELDS = {
|
||||
|
@ -136,6 +142,10 @@ angular.module("core-config-setup", ['angularFileUpload'])
|
|||
]
|
||||
};
|
||||
|
||||
$scope.enableFeature = function(config, feature) {
|
||||
config[feature] = true;
|
||||
};
|
||||
|
||||
$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.'
|
||||
|
|
|
@ -7,5 +7,5 @@ export function Inject(value: string) {
|
|||
return (target: any, propertyKey: string | symbol, parameterIndex: number): void => {
|
||||
target.$inject = target.$inject = [];
|
||||
target.$inject[parameterIndex] = value;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,109 +0,0 @@
|
|||
import * as React from "react";
|
||||
|
||||
import Build from "./build";
|
||||
import Throbber from "./throbber";
|
||||
|
||||
interface IBody {
|
||||
description: string;
|
||||
api: Object;
|
||||
repository: Object;
|
||||
}
|
||||
|
||||
interface IBodyState {
|
||||
currentBuild: any;
|
||||
intervalId: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* The Component for the main body of the repo page
|
||||
* @param {string} description - The description of the repository
|
||||
* @param {object} api - The ApiService injected from Angular
|
||||
* @param {object} repository - The list of properties for the repository
|
||||
*/
|
||||
class body extends React.Component<IBody, IBodyState> {
|
||||
static propTypes = {
|
||||
description: React.PropTypes.string.isRequired,
|
||||
api: React.PropTypes.object.isRequired,
|
||||
repository: React.PropTypes.object.isRequired,
|
||||
}
|
||||
constructor(props){
|
||||
super(props)
|
||||
this.state = {
|
||||
currentBuild: [],
|
||||
intervalId: null
|
||||
};
|
||||
}
|
||||
componentDidMount() {
|
||||
let intervalId: number = window.setInterval(() => this.getBuilds(), 1000);
|
||||
this.setState({
|
||||
currentBuild: this.state.currentBuild,
|
||||
intervalId: intervalId
|
||||
});
|
||||
}
|
||||
comoponentDidUnmount() {
|
||||
clearInterval(this.state.intervalId);
|
||||
}
|
||||
getBuilds() {
|
||||
let api: any = this.props.api;
|
||||
let repository: any = this.props.repository;
|
||||
let params: Object = {
|
||||
'repository': repository.namespace + '/' + repository.name,
|
||||
'limit': 8
|
||||
};
|
||||
|
||||
api.getRepoBuildsAsResource(params, true).get((data) => {
|
||||
let builds: Array<Object> = [];
|
||||
data.builds.forEach((element, i) => {
|
||||
builds.push({
|
||||
user: element.manual_user,
|
||||
id: element.id,
|
||||
display_name: element.display_name,
|
||||
started: element.started,
|
||||
tags: element.tags,
|
||||
phase: element.phase,
|
||||
trigger: element.trigger,
|
||||
trigger_metadata: element.trigger_metadata
|
||||
});
|
||||
});
|
||||
|
||||
this.setState({
|
||||
currentBuild: builds,
|
||||
intervalId: this.state.intervalId
|
||||
});
|
||||
});
|
||||
}
|
||||
render () {
|
||||
let description: string = this.props.description;
|
||||
if (description === null || description === "") {
|
||||
description = "No Description";
|
||||
}
|
||||
return(
|
||||
<div>
|
||||
<ul className="nav nav-tabs rp-tabs">
|
||||
<li className="active">
|
||||
<a data-target="#tab1" data-toggle="tab">Description</a>
|
||||
</li>
|
||||
<li>
|
||||
<a data-target="#tab2" data-toggle="tab">Automated Builds</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div className="panel-body rp-panelBody">
|
||||
<div className="tab-content">
|
||||
<div className="tab-pane in active" id="tab1">
|
||||
<div className="rp-description">{description}</div>
|
||||
</div>
|
||||
<div className="tab-pane" id="tab2">
|
||||
<div className="panel-body">
|
||||
<h3 className="tab-header">Repository Builds</h3>
|
||||
<Build data={this.state.currentBuild}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default body;
|
||||
|
|
@ -1,73 +0,0 @@
|
|||
import * as React from 'react';
|
||||
import * as moment from "moment";
|
||||
|
||||
import Throbber from "./throbber";
|
||||
|
||||
export default class Build extends React.Component<any, any> {
|
||||
render () {
|
||||
let builds: any = this.props.data;
|
||||
let buildsTable: any = [];
|
||||
let table: any;
|
||||
if (Object.keys(builds).length === 0) {
|
||||
buildsTable.push('Loading')
|
||||
table = <Throbber />
|
||||
}
|
||||
else {
|
||||
// Get Builds
|
||||
builds.forEach((element, i) => {
|
||||
let tags: Array<any> = []
|
||||
element.tags.forEach(tag => {
|
||||
tags.push(
|
||||
<span className="building-tag">
|
||||
<span className="tag-span rp-tagSpan">
|
||||
<i className="fa fa-tag"></i> {tag}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
});
|
||||
let buildId: string = element.id.split('-')[0];
|
||||
let phase: string = element.phase ? element.phase : 'Cannot retrieve phase';
|
||||
let started: string = element.started ? element.started : 'Cannot retrieve start date';
|
||||
let message: string;
|
||||
if (element.trigger_metadata && element.trigger_metadata.commit_info && element.trigger_metadata.commit_info.message){
|
||||
message = element.trigger_metadata.commit_info.message;
|
||||
}
|
||||
else {
|
||||
message = 'Cannot retrieve message';
|
||||
}
|
||||
buildsTable.push(
|
||||
<tr key={buildId}>
|
||||
<td>{phase}</td>
|
||||
<td>{buildId}</td>
|
||||
<td>{message}</td>
|
||||
<td>{moment(started).format('l')}</td>
|
||||
<td>{tags}</td>
|
||||
</tr>
|
||||
)
|
||||
});
|
||||
// Build the table
|
||||
table = (
|
||||
<table className="co-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>BUILD ID</td>
|
||||
<td>TRIGGERED BY</td>
|
||||
<td>DATE STARTED</td>
|
||||
<td>TAGS</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{buildsTable}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
return(
|
||||
<div className="row">
|
||||
{table}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
import * as React from "react";
|
||||
|
||||
interface IHeader {
|
||||
name: string;
|
||||
namespace: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The Component for the header of the repo page
|
||||
* @param {string} name - The name of the repository
|
||||
* @param {string} namespace - The namespace of the repository
|
||||
*/
|
||||
class repoHeader extends React.Component<IHeader, {}> {
|
||||
static propTypes = {
|
||||
name: React.PropTypes.string.isRequired,
|
||||
namespace: React.PropTypes.string.isRequired
|
||||
}
|
||||
render () {
|
||||
return(
|
||||
<div className="row rp-header__row">
|
||||
<div className="rp-title">{this.props.namespace}/{this.props.name}</div>
|
||||
<div className="rp-button">
|
||||
<div className="dropdown">
|
||||
<button className="btn rp-button__dropdown dropdown-toggle" type="button" data-toggle="dropdown">
|
||||
<span className="rp-button__text">
|
||||
Run with <span className="rp-button__text--bold">Docker</span>
|
||||
</span>
|
||||
<span className="caret"></span>
|
||||
</button>
|
||||
<ul className="dropdown-menu">
|
||||
<li><a href="#">Squashed Docker Image</a></li>
|
||||
<li><a href="#">Rocket Fetch</a></li>
|
||||
<li><a href="#">Basic Docker Pull</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default repoHeader;
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
import "sass/repo-page/repo-page.scss";
|
||||
import repoHeader from "./header";
|
||||
import repoSidebar from "./sidebar";
|
||||
import repoBody from "./body";
|
||||
|
||||
rpHeaderDirective.$inject = [
|
||||
'reactDirective',
|
||||
];
|
||||
|
||||
export function rpHeaderDirective(reactDirective) {
|
||||
return reactDirective(repoHeader);
|
||||
}
|
||||
|
||||
|
||||
rpSidebarDirective.$inject = [
|
||||
'reactDirective',
|
||||
];
|
||||
|
||||
export function rpSidebarDirective(reactDirective) {
|
||||
return reactDirective(repoSidebar);
|
||||
}
|
||||
|
||||
|
||||
rpBodyDirective.$inject = [
|
||||
'reactDirective',
|
||||
'ApiService',
|
||||
];
|
||||
|
||||
export function rpBodyDirective(reactDirective, ApiService) {
|
||||
return reactDirective(repoBody, undefined, {}, {api: ApiService});
|
||||
}
|
|
@ -1,107 +0,0 @@
|
|||
import * as React from "react";
|
||||
import * as moment from "moment";
|
||||
|
||||
interface tag {
|
||||
image_id: string;
|
||||
last_modified: string;
|
||||
name: string;
|
||||
size: number;
|
||||
}
|
||||
|
||||
interface ISidebar {
|
||||
isPublic: string;
|
||||
tags: Array<tag>;
|
||||
repository: Object
|
||||
}
|
||||
|
||||
/**
|
||||
* The Component for the sidebar of the repo page
|
||||
* @param {string} isPublic - A string that states whether the repository is private or public
|
||||
* @param {tag} tags - The list of tags for the repository
|
||||
* @param {object} repository - The list of properties for the repository
|
||||
*/
|
||||
class repoSidebar extends React.Component<ISidebar, {}> {
|
||||
static propTypes = {
|
||||
isPublic: React.PropTypes.string.isRequired,
|
||||
tags: React.PropTypes.array.isRequired,
|
||||
repository: React.PropTypes.object.isRequired
|
||||
}
|
||||
render () {
|
||||
let isPublic: string = (this.props.isPublic) ? "Public" : "Private";
|
||||
let sortedTags: Array<any> = [];
|
||||
let tagRows: Array<any> = [];
|
||||
let badgeIcon: string = (this.props.isPublic) ? "rp-badge__icon--public" : "rp-badge__icon--private";
|
||||
let repository: any = this.props.repository;
|
||||
let sharing: string = repository.company || repository.namespace;
|
||||
|
||||
if (Object.keys(this.props.tags).length > 0) {
|
||||
for (let tagObject in this.props.tags) {
|
||||
sortedTags.push({
|
||||
name: this.props.tags[tagObject].name,
|
||||
lastModified: Date.parse(this.props.tags[tagObject].last_modified)
|
||||
});
|
||||
}
|
||||
|
||||
sortedTags = sortedTags.sort(function(a, b) {
|
||||
return b.lastModified - a.lastModified;
|
||||
});
|
||||
|
||||
sortedTags.slice(0,5).forEach(function(el, i){
|
||||
tagRows.push(
|
||||
<tr>
|
||||
<td>
|
||||
<i className="fa fa-tag rp-imagesTable__tagIcon" aria-hidden="true"></i>
|
||||
{el.name}
|
||||
</td>
|
||||
<td>
|
||||
{moment(el.lastModified).fromNow()}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
});
|
||||
}
|
||||
else {
|
||||
tagRows.push(
|
||||
<tr>
|
||||
<td>
|
||||
No Tags Available
|
||||
</td>
|
||||
<td>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
||||
return(
|
||||
<div>
|
||||
<div className="rp-badge">
|
||||
<div className={badgeIcon}>
|
||||
{isPublic}
|
||||
</div>
|
||||
</div>
|
||||
<div className="rp-sharing">
|
||||
{sharing} is sharing this container {this.props.isPublic ? "publically" : "privately"}
|
||||
</div>
|
||||
<div className="rp-imagesHeader">
|
||||
Latest Images
|
||||
</div>
|
||||
<div>
|
||||
<table className="co-table co-fixed-table rp-imagesTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="rp-imagesTable__headerCell">NAME</th>
|
||||
<th className="rp-imagesTable__headerCell">LAST MODIFIED</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{tagRows}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default repoSidebar;
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
import * as React from 'react';
|
||||
import * as moment from "moment";
|
||||
|
||||
export default class Throbber extends React.Component<any, any> {
|
||||
render () {
|
||||
return(
|
||||
<div className="co-m-loader co-an-fade-in-out rp-throbber">
|
||||
<div className="co-m-loader-dot__one"></div>
|
||||
<div className="co-m-loader-dot__two"></div>
|
||||
<div className="co-m-loader-dot__three"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -50,4 +50,3 @@ angular.module('quay').directive('repoPanelInfo', function () {
|
|||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
||||
|
||||
|
|
|
@ -39,7 +39,7 @@ angular.module('quay').directive('repoPanelTags', function () {
|
|||
$scope.labelCache = {};
|
||||
|
||||
$scope.imageVulnerabilities = {};
|
||||
$scope.repoSignatureInfo = null;
|
||||
$scope.repoDelegationsInfo = null;
|
||||
|
||||
$scope.defcon1 = {};
|
||||
$scope.hasDefcon1 = false;
|
||||
|
@ -50,16 +50,16 @@ angular.module('quay').directive('repoPanelTags', function () {
|
|||
}
|
||||
|
||||
$scope.repoSignatureError = false;
|
||||
$scope.repoSignatureInfo = null;
|
||||
$scope.repoDelegationsInfo = null;
|
||||
|
||||
var params = {
|
||||
'repository': $scope.repository.namespace + '/' + $scope.repository.name
|
||||
};
|
||||
|
||||
ApiService.getRepoSignatures(null, params).then(function(resp) {
|
||||
$scope.repoSignatureInfo = resp;
|
||||
$scope.repoDelegationsInfo = resp;
|
||||
}, function() {
|
||||
$scope.repoSignatureInfo = {'error': true};
|
||||
$scope.repoDelegationsInfo = {'error': true};
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -38,4 +38,4 @@ export class QuayRequireDirective implements AfterContentInit {
|
|||
this.$transclude
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<div class="co-main-content-panel">
|
||||
<div class="app-row">
|
||||
<!-- Main panel -->
|
||||
<div class="col-md-9 main-content">
|
||||
<div class="col-md-9 col-sm-12 main-content">
|
||||
<!-- App Header -->
|
||||
<div class="app-header">
|
||||
<a href="https://coreos.com/blog/quay-application-registry-for-kubernetes.html" class="hidden-xs hidden-sm" style="float: right; padding: 6px;" ng-safenewtab><i class="fa fa-info-circle" style="margin-right: 6px;"></i>Learn more about applications</a>
|
||||
|
@ -10,89 +10,101 @@
|
|||
</div>
|
||||
|
||||
<!-- Tabs -->
|
||||
<ul class="co-top-tab-bar">
|
||||
<li class="co-top-tab" ng-class="$ctrl.currentTab == 'description' ? 'active': ''" ng-click="$ctrl.showTab('description')">
|
||||
Description
|
||||
</li>
|
||||
<li class="co-top-tab" ng-class="$ctrl.currentTab == 'channels' ? 'active': ''" ng-click="$ctrl.showTab('channels')">
|
||||
Channels
|
||||
</li>
|
||||
<li class="co-top-tab" ng-class="$ctrl.currentTab == 'releases' ? 'active': ''" ng-click="$ctrl.showTab('releases')">
|
||||
Releases
|
||||
</li>
|
||||
<li class="co-top-tab" ng-class="$ctrl.currentTab == 'settings' ? 'active': ''" ng-click="$ctrl.showTab('settings')"
|
||||
ng-if="$ctrl.repository.can_admin">
|
||||
Settings
|
||||
</li>
|
||||
</ul>
|
||||
<cor-tab-panel cor-nav-tabs>
|
||||
<cor-tabs>
|
||||
<cor-tab tab-title="Description" tab-id="description">
|
||||
<i class="fa fa-info-circle"></i>
|
||||
</cor-tab>
|
||||
<cor-tab tab-title="Channels" tab-id="channels">
|
||||
<i class="fa fa-tags"></i>
|
||||
</cor-tab>
|
||||
<cor-tab tab-title="Releases" tab-id="releases">
|
||||
<i class="fa ci-package"></i>
|
||||
</cor-tab>
|
||||
<cor-tab tab-title="Usage Logs" tab-id="logs" tab-init="$ctrl.showLogs()" ng-if="$ctrl.repository.can_admin">
|
||||
<i class="fa fa-bar-chart"></i>
|
||||
</cor-tab>
|
||||
<cor-tab tab-title="Settings" tab-id="settings" tab-init="$ctrl.showSettings()" ng-if="$ctrl.repository.can_admin">
|
||||
<i class="fa fa-gear"></i>
|
||||
</cor-tab>
|
||||
</cor-tabs>
|
||||
|
||||
<div class="tab-content">
|
||||
<div ng-show="$ctrl.currentTab == 'description'">
|
||||
<div class="description markdown-input"
|
||||
content="$ctrl.repository.description"
|
||||
can-write="$ctrl.repository.can_write"
|
||||
content-changed="$ctrl.updateDescription"
|
||||
field-title="'application description'">
|
||||
</div>
|
||||
</div>
|
||||
<cor-tab-content>
|
||||
<!-- Description -->
|
||||
<cor-tab-pane id="description">
|
||||
<div class="description">
|
||||
<markdown-input content="$ctrl.repository.description"
|
||||
can-write="$ctrl.repository.can_write"
|
||||
(content-changed)="$ctrl.updateDescription($event.content)"
|
||||
field-title="repository description"></markdown-input>
|
||||
</div>
|
||||
</cor-tab-pane>
|
||||
|
||||
<div ng-show="$ctrl.currentTab == 'channels'">
|
||||
<div ng-show="!$ctrl.repository.channels.length && $ctrl.repository.can_write">
|
||||
<h3>No channels found for this application</h3>
|
||||
<br>
|
||||
<p>
|
||||
To push a new channel (from within the Helm package directory and with the <a href="https://coreos.com/apps" ng-safenewtab>Helm registry plugin</a> installed):
|
||||
<pre class="command">
|
||||
helm registry push --namespace {{ $ctrl.repository.namespace }} --channel {channelName} {{ $ctrl.Config.SERVER_HOSTNAME }}
|
||||
</pre>
|
||||
</p>
|
||||
</div>
|
||||
<!-- Channels -->
|
||||
<cor-tab-pane id="channels">
|
||||
<div ng-show="!$ctrl.repository.channels.length && $ctrl.repository.can_write">
|
||||
<h3>No channels found for this application</h3>
|
||||
<br>
|
||||
<p class="hidden-xs">
|
||||
To push a new channel (from within the Helm package directory and with the <a href="https://github.com/app-registry/appr-helm-plugin" ng-safenewtab>Helm registry plugin</a> installed):
|
||||
<pre class="command hidden-xs">helm registry push --namespace {{ $ctrl.repository.namespace }} --channel {channelName} {{ $ctrl.Config.SERVER_HOSTNAME }}</pre>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div ng-show="$ctrl.repository.channels.length || !$ctrl.repository.can_write">
|
||||
<cor-table table-data="$ctrl.repository.channels" table-item-title="channels" filter-fields="['name']">
|
||||
<cor-table-col datafield="name" sortfield="name" title="Name"
|
||||
templateurl="/static/js/directives/ui/app-public-view/channel-name.html"></cor-table-col>
|
||||
<cor-table-col datafield="release" sortfield="release" title="Current Release"></cor-table-col>
|
||||
<cor-table-col datafield="last_modified" sortfield="last_modified" title="Last Modified"
|
||||
selected="true" kindof="datetime"
|
||||
templateurl="/static/js/directives/ui/app-public-view/last-modified.html"></cor-table-col>
|
||||
</cor-table>
|
||||
</div>
|
||||
</div> <!-- /channels -->
|
||||
<div ng-show="$ctrl.repository.channels.length || !$ctrl.repository.can_write">
|
||||
<cor-table table-data="$ctrl.repository.channels" table-item-title="channels" filter-fields="['name']">
|
||||
<cor-table-col datafield="name" sortfield="name" title="Name"
|
||||
templateurl="/static/js/directives/ui/app-public-view/channel-name.html"></cor-table-col>
|
||||
<cor-table-col datafield="release" sortfield="release" title="Current Release"></cor-table-col>
|
||||
<cor-table-col datafield="last_modified" sortfield="last_modified" title="Last Modified"
|
||||
selected="true" kindof="datetime"
|
||||
templateurl="/static/js/directives/ui/app-public-view/last-modified.html"></cor-table-col>
|
||||
</cor-table>
|
||||
</div>
|
||||
</cor-tab-pane>
|
||||
|
||||
<div ng-show="$ctrl.currentTab == 'releases'">
|
||||
<div ng-show="!$ctrl.repository.releases.length && $ctrl.repository.can_write">
|
||||
<h3>No releases found for this application</h3>
|
||||
<br>
|
||||
<p>
|
||||
To push a new release (from within the Helm package directory and with the <a href="https://coreos.com/apps" ng-safenewtab>Helm registry plugin</a> installed):
|
||||
<pre class="command">
|
||||
helm registry push --namespace {{ $ctrl.repository.namespace }} {{ $ctrl.Config.SERVER_HOSTNAME }}
|
||||
</pre>
|
||||
</p>
|
||||
</div>
|
||||
<!-- Releases -->
|
||||
<cor-tab-pane id="releases">
|
||||
<div ng-show="!$ctrl.repository.releases.length && $ctrl.repository.can_write">
|
||||
<h3>No releases found for this application</h3>
|
||||
<br>
|
||||
<p class="hidden-xs">
|
||||
To push a new release (from within the Helm package directory and with the <a href="https://coreos.com/apps" ng-safenewtab>Helm registry plugin</a> installed):
|
||||
<pre class="command hidden-xs">helm registry push --namespace {{ $ctrl.repository.namespace }} {{ $ctrl.Config.SERVER_HOSTNAME }}</pre>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div ng-show="$ctrl.repository.releases.length || !$ctrl.repository.can_write">
|
||||
<cor-table table-data="$ctrl.repository.releases" table-item-title="releases" filter-fields="['name']">
|
||||
<cor-table-col datafield="name" sortfield="name" title="Name"></cor-table-col>
|
||||
<cor-table-col datafield="last_modified" sortfield="last_modified"
|
||||
title="Created"
|
||||
selected="true" kindof="datetime"
|
||||
templateurl="/static/js/directives/ui/app-public-view/last-modified.html"></cor-table-col>
|
||||
<cor-table-col datafield="channels" title="Channels"
|
||||
templateurl="/static/js/directives/ui/app-public-view/channels-list.html"></cor-table-col>
|
||||
</cor-table>
|
||||
</div>
|
||||
</div> <!-- /releases -->
|
||||
<div ng-show="$ctrl.repository.releases.length || !$ctrl.repository.can_write">
|
||||
<cor-table table-data="$ctrl.repository.releases"
|
||||
table-item-title="releases"
|
||||
filter-fields="['name']"
|
||||
can-expand="true">
|
||||
<cor-table-col datafield="name" sortfield="name" title="Name"></cor-table-col>
|
||||
<cor-table-col datafield="last_modified" sortfield="last_modified"
|
||||
title="Created"
|
||||
selected="true" kindof="datetime"
|
||||
templateurl="/static/js/directives/ui/app-public-view/last-modified.html"></cor-table-col>
|
||||
<cor-table-col datafield="channels" title="Channels" item-limit="6"
|
||||
templateurl="/static/js/directives/ui/app-public-view/channels-list.html"></cor-table-col>
|
||||
</cor-table>
|
||||
</div>
|
||||
</cor-tab-pane>
|
||||
|
||||
<div ng-show="$ctrl.currentTab == 'settings'" ng-if="$ctrl.repository.can_admin">
|
||||
<div class="repo-panel-settings" repository="$ctrl.repository" is-enabled="$ctrl.settingsShown"></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Usage Logs-->
|
||||
<cor-tab-pane id="logs" ng-if="$ctrl.repository.can_admin">
|
||||
<div class="logs-view" repository="$ctrl.repository" makevisible="$ctrl.logsShown"></div>
|
||||
</cor-tab-pane>
|
||||
|
||||
<!-- Settings -->
|
||||
<cor-tab-pane id="settings" ng-if="$ctrl.repository.can_admin">
|
||||
<div class="repo-panel-settings" repository="$ctrl.repository" is-enabled="$ctrl.settingsShown"></div>
|
||||
</cor-tab-pane>
|
||||
</cor-tab-content>
|
||||
</cor-tab-panel>
|
||||
</div>
|
||||
|
||||
<!-- Side bar -->
|
||||
<div class="col-md-3 side-bar">
|
||||
<div class="col-md-3 hidden-xs hidden-sm side-bar">
|
||||
<div>
|
||||
<visibility-indicator repository="$ctrl.repository"></visibility-indicator>
|
||||
</div>
|
||||
|
@ -121,4 +133,4 @@ helm registry push --namespace {{ $ctrl.repository.namespace }} {{ $ctrl.Config.
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -9,23 +9,26 @@ import { Input, Component, Inject } from 'ng-metadata/core';
|
|||
templateUrl: '/static/js/directives/ui/app-public-view/app-public-view.component.html'
|
||||
})
|
||||
export class AppPublicViewComponent {
|
||||
|
||||
@Input('<') public repository: any;
|
||||
private currentTab: string = 'description';
|
||||
|
||||
private settingsShown: number = 0;
|
||||
private logsShown: number = 0;
|
||||
|
||||
constructor(@Inject('Config') private Config: any) {
|
||||
this.updateDescription = this.updateDescription.bind(this);
|
||||
}
|
||||
|
||||
public showSettings(): void {
|
||||
this.settingsShown++;
|
||||
}
|
||||
|
||||
public showLogs(): void {
|
||||
this.logsShown++;
|
||||
}
|
||||
|
||||
private updateDescription(content: string) {
|
||||
this.repository.description = content;
|
||||
this.repository.put();
|
||||
}
|
||||
|
||||
public showTab(tab: string): void {
|
||||
this.currentTab = tab;
|
||||
if (tab == 'settings') {
|
||||
this.settingsShown++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,18 @@
|
|||
<span ng-repeat="channel_name in item.channels">
|
||||
<channel-icon name="channel_name"></channel-icon>
|
||||
</span>
|
||||
<span ng-if="!item.channels.length" class="empty">(None)</span>
|
||||
<div style="display: flex; align-items: center;">
|
||||
<div style="display: flex; flex-wrap: wrap; width: 70%;">
|
||||
<!-- TODO(alecmerdler): Move repeat logic to separate component -->
|
||||
<span ng-repeat="channel_name in item.channels | limitTo : ($ctrl.rows[rowIndex].expanded ? item.channels.length : col.itemLimit)"
|
||||
ng-style="{
|
||||
'width': (100 / col.itemLimit) + '%',
|
||||
'margin-bottom': $ctrl.rows[rowIndex].expanded && $index < (item.channels.length - col.itemLimit) ? '5px' : ''
|
||||
}">
|
||||
<channel-icon name="channel_name"></channel-icon>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<a ng-if="item.channels.length > col.itemLimit"
|
||||
ng-click="$ctrl.rows[rowIndex].expanded = !$ctrl.rows[rowIndex].expanded">
|
||||
{{ $ctrl.rows[rowIndex].expanded ? 'show less...' : item.channels.length - col.itemLimit + ' more...' }}
|
||||
</a>
|
||||
<span ng-if="!item.channels.length" class="empty">(None)</span>
|
||||
</div>
|
||||
|
|
|
@ -23,10 +23,6 @@ angular.module('quay').directive('buildLogsView', function () {
|
|||
repoStatusApiCall = ApiService.getRepoBuildStatusSuperUser;
|
||||
repoLogApiCall = ApiService.getRepoBuildLogsSuperUserAsResource;
|
||||
}
|
||||
var result = $element.find('#copyButton').clipboardCopy();
|
||||
if (!result) {
|
||||
$element.find('#copyButton').hide();
|
||||
}
|
||||
|
||||
$scope.logEntries = null;
|
||||
$scope.currentParentEntry = null;
|
||||
|
|
|
@ -44,4 +44,4 @@ export class ChannelIconComponent {
|
|||
var num: number = parseInt(hash.substr(0, 4));
|
||||
return this.colors[num % this.colors.length];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
import { ClipboardCopyDirective } from './clipboard-copy.directive';
|
||||
import * as Clipboard from 'clipboard';
|
||||
import { Mock } from 'ts-mocks';
|
||||
import Spy = jasmine.Spy;
|
||||
|
||||
|
||||
describe("ClipboardCopyDirective", () => {
|
||||
var directive: ClipboardCopyDirective;
|
||||
var $elementMock: any;
|
||||
var $timeoutMock: any;
|
||||
var $documentMock: any;
|
||||
var clipboardFactory: any;
|
||||
var clipboardMock: Mock<Clipboard>;
|
||||
|
||||
beforeEach(() => {
|
||||
$elementMock = new Mock<ng.IAugmentedJQuery>();
|
||||
$timeoutMock = jasmine.createSpy('$timeoutSpy').and.callFake((fn: () => void, delay) => fn());
|
||||
$documentMock = new Mock<ng.IDocumentService>();
|
||||
clipboardMock = new Mock<Clipboard>();
|
||||
clipboardMock.setup(mock => mock.on).is((eventName: string, callback: (event) => void) => {});
|
||||
clipboardFactory = jasmine.createSpy('clipboardFactory').and.returnValue(clipboardMock.Object);
|
||||
directive = new ClipboardCopyDirective(<any>[$elementMock.Object],
|
||||
$timeoutMock,
|
||||
<any>[$documentMock.Object],
|
||||
clipboardFactory);
|
||||
directive.copyTargetSelector = "#copy-input-box-0";
|
||||
});
|
||||
|
||||
describe("ngAfterContentInit", () => {
|
||||
|
||||
it("initializes new Clipboard instance", () => {
|
||||
const target = new Mock<ng.IAugmentedJQuery>();
|
||||
$documentMock.setup(mock => mock.querySelector).is(selector => target.Object);
|
||||
directive.ngAfterContentInit();
|
||||
|
||||
expect(clipboardFactory).toHaveBeenCalled();
|
||||
expect((<Spy>clipboardFactory.calls.argsFor(0)[0])).toEqual($elementMock.Object);
|
||||
expect((<Spy>clipboardFactory.calls.argsFor(0)[1]['target']())).toEqual(target.Object);
|
||||
});
|
||||
|
||||
it("sets error callback for Clipboard instance", () => {
|
||||
directive.ngAfterContentInit();
|
||||
|
||||
expect((<Spy>clipboardMock.Object.on.calls.argsFor(0)[0])).toEqual('error');
|
||||
expect((<Spy>clipboardMock.Object.on.calls.argsFor(0)[1])).toBeDefined();
|
||||
});
|
||||
|
||||
it("sets success callback for Clipboard instance", (done) => {
|
||||
directive.ngAfterContentInit();
|
||||
|
||||
expect((<Spy>clipboardMock.Object.on.calls.argsFor(1)[0])).toEqual('success');
|
||||
expect((<Spy>clipboardMock.Object.on.calls.argsFor(1)[1])).toBeDefined();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe("ngOnDestroy", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
clipboardMock.setup(mock => mock.destroy).is(() => null);
|
||||
});
|
||||
|
||||
it("calls method to destroy Clipboard instance if set", (done) => {
|
||||
directive.ngAfterContentInit();
|
||||
directive.ngOnDestroy();
|
||||
|
||||
expect((<Spy>clipboardMock.Object.destroy)).toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
|
||||
it("does not call method to destroy Clipboard instance if not set", () => {
|
||||
directive.ngOnDestroy();
|
||||
|
||||
expect((<Spy>clipboardMock.Object.destroy)).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,63 @@
|
|||
import { Directive, Inject, Input, AfterContentInit, OnDestroy } from 'ng-metadata/core';
|
||||
import * as Clipboard from 'clipboard';
|
||||
|
||||
|
||||
@Directive({
|
||||
selector: '[clipboardCopy]'
|
||||
})
|
||||
export class ClipboardCopyDirective implements AfterContentInit, OnDestroy {
|
||||
|
||||
@Input('@clipboardCopy') public copyTargetSelector: string;
|
||||
|
||||
private clipboard: Clipboard;
|
||||
|
||||
constructor(@Inject('$element') private $element: ng.IAugmentedJQuery,
|
||||
@Inject('$timeout') private $timeout: ng.ITimeoutService,
|
||||
@Inject('$document') private $document: ng.IDocumentService,
|
||||
@Inject('clipboardFactory') private clipboardFactory: (elem, options) => Clipboard) {
|
||||
|
||||
}
|
||||
|
||||
public ngAfterContentInit(): void {
|
||||
// FIXME: Need to wait for DOM to render to find target element
|
||||
this.$timeout(() => {
|
||||
this.clipboard = this.clipboardFactory(this.$element[0], {target: (trigger) => {
|
||||
return this.$document[0].querySelector(this.copyTargetSelector);
|
||||
}});
|
||||
|
||||
this.clipboard.on("error", (e) => {
|
||||
console.error(e);
|
||||
});
|
||||
|
||||
this.clipboard.on('success', (e) => {
|
||||
const container = e.trigger.parentNode.parentNode.parentNode;
|
||||
const messageElem = container.querySelector('.clipboard-copied-message');
|
||||
if (!messageElem) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Resets the animation.
|
||||
var elem = messageElem;
|
||||
elem.style.display = 'none';
|
||||
elem.classList.remove('animated');
|
||||
|
||||
// Show the notification.
|
||||
setTimeout(() => {
|
||||
elem.style.display = 'inline-block';
|
||||
elem.classList.add('animated');
|
||||
}, 10);
|
||||
|
||||
// Reset the notification.
|
||||
setTimeout(() => {
|
||||
elem.style.display = 'none';
|
||||
}, 5000);
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
|
||||
public ngOnDestroy(): void {
|
||||
if (this.clipboard) {
|
||||
this.clipboard.destroy();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import { ContextPathSelectComponent } from './context-path-select.component';
|
||||
import { ContextPathSelectComponent, ContextChangeEvent } from './context-path-select.component';
|
||||
|
||||
|
||||
describe("ContextPathSelectComponent", () => {
|
||||
|
@ -57,23 +57,33 @@ describe("ContextPathSelectComponent", () => {
|
|||
|
||||
expect(component.isValidContext).toBe(false);
|
||||
});
|
||||
|
||||
it("emits output event indicating build context changed", (done) => {
|
||||
component.contextChanged.subscribe((event: ContextChangeEvent) => {
|
||||
expect(event.contextDir).toEqual(newContext);
|
||||
expect(event.isValid).toEqual(component.isValidContext);
|
||||
done();
|
||||
});
|
||||
|
||||
component.setContext(newContext);
|
||||
});
|
||||
});
|
||||
|
||||
describe("setSelectedContext", () => {
|
||||
var context: string;
|
||||
var newContext: string;
|
||||
|
||||
beforeEach(() => {
|
||||
context = '/conf';
|
||||
newContext = '/conf';
|
||||
});
|
||||
|
||||
it("sets current context to given context", () => {
|
||||
component.setSelectedContext(context);
|
||||
component.setSelectedContext(newContext);
|
||||
|
||||
expect(component.currentContext).toEqual(context);
|
||||
expect(component.currentContext).toEqual(newContext);
|
||||
});
|
||||
|
||||
it("sets valid context flag to true if given context is valid", () => {
|
||||
component.setSelectedContext(context);
|
||||
component.setSelectedContext(newContext);
|
||||
|
||||
expect(component.isValidContext).toBe(true);
|
||||
});
|
||||
|
@ -83,5 +93,15 @@ describe("ContextPathSelectComponent", () => {
|
|||
|
||||
expect(component.isValidContext).toBe(false);
|
||||
});
|
||||
|
||||
it("emits output event indicating build context changed", (done) => {
|
||||
component.contextChanged.subscribe((event: ContextChangeEvent) => {
|
||||
expect(event.contextDir).toEqual(newContext);
|
||||
expect(event.isValid).toEqual(component.isValidContext);
|
||||
done();
|
||||
});
|
||||
|
||||
component.setSelectedContext(newContext);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,4 +1,4 @@
|
|||
import { Input, Component, OnChanges, SimpleChanges } from 'ng-metadata/core';
|
||||
import { Input, Component, OnChanges, SimpleChanges, Output, EventEmitter } from 'ng-metadata/core';
|
||||
|
||||
|
||||
/**
|
||||
|
@ -10,10 +10,10 @@ import { Input, Component, OnChanges, SimpleChanges } from 'ng-metadata/core';
|
|||
})
|
||||
export class ContextPathSelectComponent implements OnChanges {
|
||||
|
||||
// FIXME: Use one-way data binding
|
||||
@Input('=') public currentContext: string = '';
|
||||
@Input('=') public isValidContext: boolean;
|
||||
@Input('=') public contexts: string[];
|
||||
@Input('<') public currentContext: string = '';
|
||||
@Input('<') public contexts: string[];
|
||||
@Output() public contextChanged: EventEmitter<ContextChangeEvent> = new EventEmitter();
|
||||
public isValidContext: boolean;
|
||||
private isUnknownContext: boolean = true;
|
||||
private selectedContext: string | null = null;
|
||||
|
||||
|
@ -25,12 +25,16 @@ export class ContextPathSelectComponent implements OnChanges {
|
|||
this.currentContext = context;
|
||||
this.selectedContext = null;
|
||||
this.isValidContext = this.checkContext(context, this.contexts);
|
||||
|
||||
this.contextChanged.emit({contextDir: context, isValid: this.isValidContext});
|
||||
}
|
||||
|
||||
public setSelectedContext(context: string): void {
|
||||
this.currentContext = context;
|
||||
this.selectedContext = context;
|
||||
this.isValidContext = this.checkContext(context, this.contexts);
|
||||
|
||||
this.contextChanged.emit({contextDir: context, isValid: this.isValidContext});
|
||||
}
|
||||
|
||||
private checkContext(context: string = '', contexts: string[] = []): boolean {
|
||||
|
@ -39,8 +43,17 @@ export class ContextPathSelectComponent implements OnChanges {
|
|||
|
||||
if (context.length > 0 && context[0] === '/') {
|
||||
isValidContext = true;
|
||||
this.isUnknownContext = true;
|
||||
this.isUnknownContext = contexts.indexOf(context) != -1;
|
||||
}
|
||||
return isValidContext;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Build context changed event.
|
||||
*/
|
||||
export type ContextChangeEvent = {
|
||||
contextDir: string;
|
||||
isValid: boolean;
|
||||
};
|
||||
|
|
|
@ -1,53 +1,5 @@
|
|||
$.fn.clipboardCopy = function() {
|
||||
if (__zeroClipboardSupported) {
|
||||
(new ZeroClipboard($(this)));
|
||||
return true;
|
||||
}
|
||||
|
||||
this.hide();
|
||||
return false;
|
||||
};
|
||||
|
||||
// Initialize the clipboard system.
|
||||
(function () {
|
||||
__zeroClipboardSupported = true;
|
||||
|
||||
ZeroClipboard.config({
|
||||
'swfPath': 'static/lib/ZeroClipboard.swf'
|
||||
});
|
||||
|
||||
ZeroClipboard.on("error", function(e) {
|
||||
__zeroClipboardSupported = false;
|
||||
});
|
||||
|
||||
ZeroClipboard.on('aftercopy', function(e) {
|
||||
var container = e.target.parentNode.parentNode.parentNode;
|
||||
var message = $(container).find('.clipboard-copied-message')[0];
|
||||
if (!message) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Resets the animation.
|
||||
var elem = message;
|
||||
elem.style.display = 'none';
|
||||
elem.classList.remove('animated');
|
||||
|
||||
// Show the notification.
|
||||
setTimeout(function() {
|
||||
elem.style.display = 'inline-block';
|
||||
elem.classList.add('animated');
|
||||
}, 10);
|
||||
|
||||
// Reset the notification.
|
||||
setTimeout(function() {
|
||||
elem.style.display = 'none';
|
||||
}, 5000);
|
||||
});
|
||||
})();
|
||||
|
||||
/**
|
||||
* An element which displays a textfield with a "Copy to Clipboard" icon next to it. Note
|
||||
* that this method depends on the clipboard copying library in the lib/ folder.
|
||||
* An element which displays a textfield with a "Copy to Clipboard" icon next to it.
|
||||
*/
|
||||
angular.module('quay').directive('copyBox', function () {
|
||||
var directiveDefinitionObject = {
|
||||
|
@ -66,13 +18,6 @@ angular.module('quay').directive('copyBox', function () {
|
|||
var number = $rootScope.__copyBoxIdCounter || 0;
|
||||
$rootScope.__copyBoxIdCounter = number + 1;
|
||||
$scope.inputId = "copy-box-input-" + number;
|
||||
|
||||
var button = $($element).find('.copy-icon');
|
||||
var input = $($element).find('input');
|
||||
|
||||
input.attr('id', $scope.inputId);
|
||||
button.attr('data-clipboard-target', $scope.inputId);
|
||||
$scope.disabled = !button.clipboardCopy();
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
|
|
|
@ -10,14 +10,20 @@ import { CorTableComponent } from './cor-table.component';
|
|||
template: '',
|
||||
})
|
||||
export class CorTableColumn implements OnInit {
|
||||
|
||||
@Input('@') public title: string;
|
||||
@Input('@') public templateurl: string;
|
||||
@Input('@') public datafield: string;
|
||||
@Input('@') public sortfield: string;
|
||||
@Input('@') public selected: string;
|
||||
@Input('=') public bindModel: any;
|
||||
@Input('@') public style: string;
|
||||
@Input('@') public class: string;
|
||||
@Input('@') public kindof: string;
|
||||
@Input('<') public itemLimit: number = 5;
|
||||
|
||||
constructor(@Host() @Inject(CorTableComponent) private parent: CorTableComponent) {
|
||||
constructor(@Host() @Inject(CorTableComponent) private parent: CorTableComponent,
|
||||
@Inject('TableService') private tableService: any) {
|
||||
|
||||
}
|
||||
|
||||
|
@ -29,9 +35,9 @@ export class CorTableColumn implements OnInit {
|
|||
return this.kindof == 'datetime';
|
||||
}
|
||||
|
||||
public processColumnForOrdered(tableService: any, value: any): any {
|
||||
public processColumnForOrdered(value: any): any {
|
||||
if (this.kindof == 'datetime') {
|
||||
return tableService.getReversedTimestamp(value);
|
||||
return this.tableService.getReversedTimestamp(value);
|
||||
}
|
||||
|
||||
return value;
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
.cor-table-element .co-top-bar {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: baseline;
|
||||
}
|
|
@ -1,33 +1,57 @@
|
|||
<div class="cor-table-element">
|
||||
<span ng-transclude/>
|
||||
<span ng-transclude></span>
|
||||
|
||||
<!-- Filter -->
|
||||
<div class="co-top-bar" ng-if="$ctrl.compact != 'true'">
|
||||
<div class="co-top-bar" ng-if="!$ctrl.compact">
|
||||
<span class="co-filter-box with-options" ng-if="$ctrl.tableData.length && $ctrl.filterFields.length">
|
||||
<span class="page-controls"
|
||||
total-count="$ctrl.orderedData.entries.length"
|
||||
current-page="$ctrl.options.page"
|
||||
page-size="$ctrl.maxDisplayCount"></span>
|
||||
<span class="filter-message" ng-if="$ctrl.options.filter">
|
||||
Showing {{ $ctrl.orderedData.entries.length }} of {{ $ctrl.tableData.length }} {{ $ctrl.tableItemTitle }}
|
||||
Showing {{ $ctrl.orderedData.entries.length }} of {{ $ctrl.tableData.length }} {{ ::$ctrl.tableItemTitle }}
|
||||
</span>
|
||||
<input class="form-control" type="text" ng-model="$ctrl.options.filter"
|
||||
placeholder="Filter {{ $ctrl.tableItemTitle }}..." ng-change="$ctrl.refreshOrder()">
|
||||
<input class="form-control" type="text"
|
||||
placeholder="Filter {{ ::$ctrl.tableItemTitle }}..."
|
||||
ng-model="$ctrl.options.filter"
|
||||
ng-change="$ctrl.refreshOrder()">
|
||||
</span>
|
||||
|
||||
<!-- Compact/expand rows toggle -->
|
||||
<div ng-if="!$ctrl.compact && $ctrl.canExpand" class="tab-header-controls">
|
||||
<div class="btn-group btn-group-sm">
|
||||
<button class="btn" ng-class="!$ctrl.expandRows ? 'btn-primary active' : 'btn-default'"
|
||||
ng-click="$ctrl.setExpanded(false)">
|
||||
Compact
|
||||
</button>
|
||||
<button class="btn" ng-class="$ctrl.expandRows ? 'btn-info active' : 'btn-default'"
|
||||
ng-click="$ctrl.setExpanded(true)">
|
||||
Expanded
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty -->
|
||||
<div class="empty" ng-if="!$ctrl.tableData.length && $ctrl.compact != 'true'">
|
||||
<div class="empty-primary-msg">No {{ $ctrl.tableItemTitle }} found.</div>
|
||||
<div class="empty-primary-msg">No {{ ::$ctrl.tableItemTitle }} found.</div>
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<table class="co-table" ng-show="$ctrl.tableData.length">
|
||||
<table class="co-table co-fixed-table" ng-show="$ctrl.tableData.length">
|
||||
<thead>
|
||||
<td ng-repeat="col in $ctrl.columns" ng-class="$ctrl.tablePredicateClass(col, $ctrl.options)">
|
||||
<a ng-click="$ctrl.setOrder(col)">{{ col.title }}</a>
|
||||
<td ng-repeat="col in $ctrl.columns"
|
||||
ng-class="$ctrl.tablePredicateClass(col)" style="{{ ::col.style }}">
|
||||
<a ng-click="$ctrl.setOrder(col)">{{ ::col.title }}</a>
|
||||
</td>
|
||||
</thead>
|
||||
<tbody ng-repeat="item in $ctrl.orderedData.visibleEntries | limitTo:$ctrl.maxDisplayCount">
|
||||
<tbody ng-repeat="item in $ctrl.orderedData.visibleEntries" ng-init="rowIndex = $index"
|
||||
ng-if="($index >= $ctrl.options.page * $ctrl.maxDisplayCount &&
|
||||
$index < ($ctrl.options.page + 1) * $ctrl.maxDisplayCount)">
|
||||
<tr>
|
||||
<td ng-repeat="col in $ctrl.columns">
|
||||
<div ng-include="col.templateurl" ng-if="col.templateurl"></div>
|
||||
<td ng-repeat="col in $ctrl.columns"
|
||||
style="{{ ::col.style }}" class="{{ ::col.class }}">
|
||||
<div ng-if="col.templateurl" ng-include="col.templateurl"></div>
|
||||
<div ng-if="!col.templateurl">{{ item[col.datafield] }}</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
@ -36,7 +60,7 @@
|
|||
|
||||
<div class="empty" ng-if="!$ctrl.orderedData.entries.length && $ctrl.tableData.length"
|
||||
style="margin-top: 20px;">
|
||||
<div class="empty-primary-msg">No matching {{ $ctrl.tableItemTitle }} found.</div>
|
||||
<div class="empty-primary-msg">No matching {{ ::$ctrl.tableItemTitle }} found.</div>
|
||||
<div class="empty-secondary-msg">Try adjusting your filter above.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
126
static/js/directives/ui/cor-table/cor-table.component.spec.ts
Normal file
126
static/js/directives/ui/cor-table/cor-table.component.spec.ts
Normal file
|
@ -0,0 +1,126 @@
|
|||
import { Mock } from 'ts-mocks';
|
||||
import { CorTableComponent, CorTableOptions } from './cor-table.component';
|
||||
import { CorTableColumn } from './cor-table-col.component';
|
||||
import { SimpleChanges } from 'ng-metadata/core';
|
||||
import { ViewArray } from '../../../services/view-array/view-array';
|
||||
import Spy = jasmine.Spy;
|
||||
|
||||
|
||||
describe("CorTableComponent", () => {
|
||||
var component: CorTableComponent;
|
||||
var tableServiceMock: Mock<any>;
|
||||
var tableData: any[];
|
||||
var columnMocks: Mock<CorTableColumn>[];
|
||||
var orderedDataMock: Mock<ViewArray>;
|
||||
|
||||
beforeEach(() => {
|
||||
orderedDataMock = new Mock<ViewArray>();
|
||||
orderedDataMock.setup(mock => mock.visibleEntries).is([]);
|
||||
tableServiceMock = new Mock<any>();
|
||||
tableServiceMock.setup(mock => mock.buildOrderedItems)
|
||||
.is((items, options, filterFields, numericFields, extrafilter?) => orderedDataMock.Object);
|
||||
|
||||
tableData = [
|
||||
{name: "apple", last_modified: 1496068383000, version: "1.0.0"},
|
||||
{name: "pear", last_modified: 1496068383001, version: "1.1.0"},
|
||||
{name: "orange", last_modified: 1496068383002, version: "1.0.0"},
|
||||
{name: "banana", last_modified: 1496068383000, version: "2.0.0"},
|
||||
];
|
||||
|
||||
columnMocks = Object.keys(tableData[0])
|
||||
.map((key, index) => {
|
||||
const col = new Mock<CorTableColumn>();
|
||||
col.setup(mock => mock.isNumeric).is(() => index == 1 ? true : false);
|
||||
col.setup(mock => mock.processColumnForOrdered).is((value) => "dummy");
|
||||
col.setup(mock => mock.datafield).is(key);
|
||||
|
||||
return col;
|
||||
});
|
||||
|
||||
component = new CorTableComponent(tableServiceMock.Object);
|
||||
component.tableData = tableData;
|
||||
component.filterFields = ['name', 'version'];
|
||||
component.compact = false;
|
||||
component.tableItemTitle = "fruits";
|
||||
component.maxDisplayCount = 10;
|
||||
// Add columns
|
||||
columnMocks.forEach(colMock => component.addColumn(colMock.Object));
|
||||
(<Spy>tableServiceMock.Object.buildOrderedItems).calls.reset();
|
||||
});
|
||||
|
||||
describe("constructor", () => {
|
||||
|
||||
it("sets table options", () => {
|
||||
expect(component.options.filter).toEqual('');
|
||||
expect(component.options.reverse).toBe(false);
|
||||
expect(component.options.predicate).toEqual('');
|
||||
expect(component.options.page).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ngOnChanges", () => {
|
||||
var changes: SimpleChanges;
|
||||
|
||||
it("calls table service to build ordered items if table data is changed", () => {
|
||||
changes = {tableData: {currentValue: [], previousValue: [], isFirstChange: () => false}};
|
||||
component.ngOnChanges(changes);
|
||||
|
||||
expect((<Spy>tableServiceMock.Object.buildOrderedItems)).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("passes processed table data to table service", () => {
|
||||
changes = {tableData: {currentValue: [], previousValue: [], isFirstChange: () => false}};
|
||||
component.tableData = changes['tableData'].currentValue;
|
||||
component.ngOnChanges(changes);
|
||||
|
||||
expect((<Spy>tableServiceMock.Object.buildOrderedItems).calls.argsFor(0)[0]).not.toEqual(tableData);
|
||||
});
|
||||
|
||||
it("passes options to table service", () => {
|
||||
changes = {tableData: {currentValue: [], previousValue: [], isFirstChange: () => false}};
|
||||
component.ngOnChanges(changes);
|
||||
|
||||
expect((<Spy>tableServiceMock.Object.buildOrderedItems).calls.argsFor(0)[1]).toEqual(component.options);
|
||||
});
|
||||
|
||||
it("passes filter fields to table service", () => {
|
||||
changes = {tableData: {currentValue: [], previousValue: [], isFirstChange: () => false}};
|
||||
component.ngOnChanges(changes);
|
||||
|
||||
expect((<Spy>tableServiceMock.Object.buildOrderedItems).calls.argsFor(0)[2]).toEqual(component.filterFields);
|
||||
});
|
||||
|
||||
it("passes numeric fields to table service", () => {
|
||||
changes = {tableData: {currentValue: [], previousValue: [], isFirstChange: () => false}};
|
||||
component.ngOnChanges(changes);
|
||||
|
||||
const expectedNumericCols: string[] = columnMocks.filter(colMock => colMock.Object.isNumeric())
|
||||
.map(colMock => colMock.Object.datafield);
|
||||
|
||||
expect((<Spy>tableServiceMock.Object.buildOrderedItems).calls.argsFor(0)[3]).toEqual(expectedNumericCols);
|
||||
});
|
||||
|
||||
it("resets to first page if table data is changed", () => {
|
||||
component.options.page = 1;
|
||||
changes = {tableData: {currentValue: [], previousValue: [], isFirstChange: () => false}};
|
||||
component.ngOnChanges(changes);
|
||||
|
||||
expect(component.options.page).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("addColumn", () => {
|
||||
var columnMock: Mock<CorTableColumn>;
|
||||
|
||||
beforeEach(() => {
|
||||
columnMock = new Mock<CorTableColumn>();
|
||||
columnMock.setup(mock => mock.isNumeric).is(() => false);
|
||||
});
|
||||
|
||||
it("calls table service to build ordered items with new column", () => {
|
||||
component.addColumn(columnMock.Object);
|
||||
|
||||
expect((<Spy>tableServiceMock.Object.buildOrderedItems)).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,5 +1,7 @@
|
|||
import { Input, Component, OnChanges, SimpleChanges, Inject } from 'ng-metadata/core';
|
||||
import { CorTableColumn } from './cor-table-col.component';
|
||||
import { ViewArray } from '../../../services/view-array/view-array';
|
||||
import './cor-table.component.css';
|
||||
|
||||
|
||||
/**
|
||||
|
@ -13,23 +15,28 @@ import { CorTableColumn } from './cor-table-col.component';
|
|||
}
|
||||
})
|
||||
export class CorTableComponent implements OnChanges {
|
||||
@Input('<') public tableData: any[];
|
||||
|
||||
@Input('<') public tableData: any[] = [];
|
||||
@Input('@') public tableItemTitle: string;
|
||||
@Input('<') public filterFields: string[];
|
||||
@Input('@') public compact: string;
|
||||
@Input('<') public maxDisplayCount: number;
|
||||
private columns: CorTableColumn[];
|
||||
private orderedData: any;
|
||||
private options: any;
|
||||
@Input('<') public compact: boolean = false;
|
||||
@Input('<') public maxDisplayCount: number = 10;
|
||||
@Input('<') public canExpand: boolean = false;
|
||||
@Input('<') public expandRows: boolean = false;
|
||||
|
||||
public orderedData: ViewArray;
|
||||
public options: CorTableOptions = {
|
||||
filter: '',
|
||||
reverse: false,
|
||||
predicate: '',
|
||||
page: 0,
|
||||
};
|
||||
|
||||
private rows: CorTableRow[] = [];
|
||||
private columns: CorTableColumn[] = [];
|
||||
|
||||
constructor(@Inject('TableService') private tableService: any) {
|
||||
|
||||
constructor(@Inject('TableService') private TableService: any) {
|
||||
this.columns = [];
|
||||
this.options = {
|
||||
'filter': '',
|
||||
'reverse': false,
|
||||
'predicate': '',
|
||||
'page': 0,
|
||||
};
|
||||
}
|
||||
|
||||
public ngOnChanges(changes: SimpleChanges): void {
|
||||
|
@ -49,38 +56,55 @@ export class CorTableComponent implements OnChanges {
|
|||
}
|
||||
|
||||
private setOrder(col: CorTableColumn): void {
|
||||
this.TableService.orderBy(col.datafield, this.options);
|
||||
this.tableService.orderBy(col.datafield, this.options);
|
||||
this.refreshOrder();
|
||||
}
|
||||
|
||||
private setExpanded(isExpanded: boolean): void {
|
||||
this.expandRows = isExpanded;
|
||||
this.rows.forEach((row) => row.expanded = isExpanded);
|
||||
}
|
||||
|
||||
private tablePredicateClass(col: CorTableColumn, options: any) {
|
||||
return this.TableService.tablePredicateClass(col.datafield, this.options.predicate, this.options.reverse);
|
||||
return this.tableService.tablePredicateClass(col.datafield, this.options.predicate, this.options.reverse);
|
||||
}
|
||||
|
||||
private refreshOrder(): void {
|
||||
var columnMap = {};
|
||||
this.options.page = 0;
|
||||
|
||||
var columnMap: {[name: string]: CorTableColumn} = {};
|
||||
this.columns.forEach(function(col) {
|
||||
columnMap[col.datafield] = col;
|
||||
});
|
||||
|
||||
const filterCols = this.columns.filter(col => !!col.sortfield)
|
||||
const numericCols: string[] = this.columns.filter(col => col.isNumeric())
|
||||
.map(col => col.datafield);
|
||||
|
||||
const numericCols = this.columns.filter(col => col.isNumeric())
|
||||
.map(col => col.datafield);
|
||||
|
||||
const tableData = this.tableData || [];
|
||||
const processed = tableData.map((item) => {
|
||||
var newObj = Object.assign({}, item);
|
||||
const processed: any[] = this.tableData.map((item) => {
|
||||
Object.keys(item).forEach((key) => {
|
||||
if (columnMap[key]) {
|
||||
newObj[key] = columnMap[key].processColumnForOrdered(this.TableService, item[key]);
|
||||
item[key] = columnMap[key].processColumnForOrdered(item[key]);
|
||||
}
|
||||
});
|
||||
|
||||
return newObj;
|
||||
return item;
|
||||
});
|
||||
|
||||
this.orderedData = this.TableService.buildOrderedItems(processed, this.options, filterCols, numericCols);
|
||||
this.orderedData = this.tableService.buildOrderedItems(processed, this.options, this.filterFields, numericCols);
|
||||
this.rows = this.orderedData.visibleEntries.map((item) => Object.assign({}, {expanded: false, rowData: item}));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export type CorTableOptions = {
|
||||
filter: string;
|
||||
reverse: boolean;
|
||||
predicate: string;
|
||||
page: number;
|
||||
};
|
||||
|
||||
|
||||
export type CorTableRow = {
|
||||
expanded: boolean;
|
||||
rowData: any;
|
||||
};
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
import { CorCookieTabsDirective } from './cor-cookie-tabs.directive';
|
||||
import { CorTabPanelComponent } from '../cor-tab-panel/cor-tab-panel.component';
|
||||
import { Mock } from 'ts-mocks';
|
||||
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
|
||||
import Spy = jasmine.Spy;
|
||||
|
||||
|
||||
describe("CorCookieTabsDirective", () => {
|
||||
var directive: CorCookieTabsDirective;
|
||||
var panelMock: Mock<CorTabPanelComponent>;
|
||||
var cookieServiceMock: Mock<any>;
|
||||
var activeTab: BehaviorSubject<string>;
|
||||
|
||||
beforeEach(() => {
|
||||
activeTab = new BehaviorSubject<string>(null);
|
||||
spyOn(activeTab, "subscribe").and.returnValue(null);
|
||||
panelMock = new Mock<CorTabPanelComponent>();
|
||||
panelMock.setup(mock => mock.activeTab).is(activeTab);
|
||||
cookieServiceMock = new Mock<any>();
|
||||
cookieServiceMock.setup(mock => mock.putPermanent).is((cookieName, value) => null);
|
||||
|
||||
directive = new CorCookieTabsDirective(panelMock.Object, cookieServiceMock.Object);
|
||||
directive.cookieName = "quay.credentialsTab";
|
||||
});
|
||||
|
||||
describe("ngAfterContentInit", () => {
|
||||
const tabId: string = "description";
|
||||
|
||||
beforeEach(() => {
|
||||
cookieServiceMock.setup(mock => mock.get).is((name) => tabId);
|
||||
spyOn(activeTab, "next").and.returnValue(null);
|
||||
});
|
||||
|
||||
it("calls cookie service to retrieve initial tab id", () => {
|
||||
directive.ngAfterContentInit();
|
||||
|
||||
expect((<Spy>cookieServiceMock.Object.get).calls.argsFor(0)[0]).toEqual(directive.cookieName);
|
||||
});
|
||||
|
||||
it("emits retrieved tab id as next active tab", () => {
|
||||
directive.ngAfterContentInit();
|
||||
|
||||
expect((<Spy>panelMock.Object.activeTab.next).calls.argsFor(0)[0]).toEqual(tabId);
|
||||
});
|
||||
|
||||
it("subscribes to active tab changes", () => {
|
||||
directive.ngAfterContentInit();
|
||||
|
||||
expect((<Spy>panelMock.Object.activeTab.subscribe)).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("calls cookie service to put new permanent cookie on active tab changes", () => {
|
||||
directive.ngAfterContentInit();
|
||||
const tabId: string = "description";
|
||||
(<Spy>panelMock.Object.activeTab.subscribe).calls.argsFor(0)[0](tabId);
|
||||
|
||||
expect((<Spy>cookieServiceMock.Object.putPermanent).calls.argsFor(0)[0]).toEqual(directive.cookieName);
|
||||
expect((<Spy>cookieServiceMock.Object.putPermanent).calls.argsFor(0)[1]).toEqual(tabId);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,29 @@
|
|||
import { Directive, Inject, Host, AfterContentInit, Input } from 'ng-metadata/core';
|
||||
import { CorTabPanelComponent } from '../cor-tab-panel/cor-tab-panel.component';
|
||||
|
||||
|
||||
/**
|
||||
* Adds routing capabilities to cor-tab-panel using a browser cookie.
|
||||
*/
|
||||
@Directive({
|
||||
selector: '[corCookieTabs]'
|
||||
})
|
||||
export class CorCookieTabsDirective implements AfterContentInit {
|
||||
|
||||
@Input('@corCookieTabs') public cookieName: string;
|
||||
|
||||
constructor(@Host() @Inject(CorTabPanelComponent) private panel: CorTabPanelComponent,
|
||||
@Inject('CookieService') private cookieService: any) {
|
||||
|
||||
}
|
||||
|
||||
public ngAfterContentInit(): void {
|
||||
// Set initial tab
|
||||
const tabId: string = this.cookieService.get(this.cookieName);
|
||||
this.panel.activeTab.next(tabId);
|
||||
|
||||
this.panel.activeTab.subscribe((tab: string) => {
|
||||
this.cookieService.putPermanent(this.cookieName, tab);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
import { CorNavTabsDirective } from './cor-nav-tabs.directive';
|
||||
import { CorTabPanelComponent } from '../cor-tab-panel/cor-tab-panel.component';
|
||||
import { Mock } from 'ts-mocks';
|
||||
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
|
||||
import Spy = jasmine.Spy;
|
||||
|
||||
|
||||
describe("CorNavTabsDirective", () => {
|
||||
var directive: CorNavTabsDirective;
|
||||
var panelMock: Mock<CorTabPanelComponent>;
|
||||
var $locationMock: Mock<ng.ILocationService>;
|
||||
var $rootScopeMock: Mock<ng.IRootScopeService>;
|
||||
var activeTab: BehaviorSubject<string>;
|
||||
const tabId: string = "description";
|
||||
|
||||
beforeEach(() => {
|
||||
activeTab = new BehaviorSubject<string>(null);
|
||||
spyOn(activeTab, "next").and.returnValue(null);
|
||||
panelMock = new Mock<CorTabPanelComponent>();
|
||||
panelMock.setup(mock => mock.activeTab).is(activeTab);
|
||||
$locationMock = new Mock<ng.ILocationService>();
|
||||
$locationMock.setup(mock => mock.search).is(() => <any>{tab: tabId});
|
||||
$rootScopeMock = new Mock<ng.IRootScopeService>();
|
||||
$rootScopeMock.setup(mock => mock.$on);
|
||||
|
||||
directive = new CorNavTabsDirective(panelMock.Object, $locationMock.Object, $rootScopeMock.Object);
|
||||
});
|
||||
|
||||
describe("constructor", () => {
|
||||
|
||||
it("subscribes to $routeUpdate event on the root scope", () => {
|
||||
expect((<Spy>$rootScopeMock.Object.$on).calls.argsFor(0)[0]).toEqual("$routeUpdate");
|
||||
});
|
||||
|
||||
it("calls location service to retrieve tab id from URL query parameters on route update", () => {
|
||||
(<Spy>$rootScopeMock.Object.$on).calls.argsFor(0)[1]();
|
||||
|
||||
expect(<Spy>$locationMock.Object.search).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("emits retrieved tab id as next active tab on route update", () => {
|
||||
(<Spy>$rootScopeMock.Object.$on).calls.argsFor(0)[1]();
|
||||
|
||||
expect((<Spy>activeTab.next).calls.argsFor(0)[0]).toEqual(tabId);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ngAfterContentInit", () => {
|
||||
const path: string = "quay.io/repository/devtable/simple";
|
||||
|
||||
beforeEach(() => {
|
||||
$locationMock.setup(mock => mock.path).is(() => <any>path);
|
||||
});
|
||||
|
||||
it("calls location service to retrieve the current URL path and sets panel's base path", () => {
|
||||
directive.ngAfterContentInit();
|
||||
|
||||
expect(panelMock.Object.basePath).toEqual(path);
|
||||
});
|
||||
|
||||
it("calls location service to retrieve tab id from URL query parameters", () => {
|
||||
directive.ngAfterContentInit();
|
||||
|
||||
expect(<Spy>$locationMock.Object.search).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("emits retrieved tab id as next active tab", () => {
|
||||
directive.ngAfterContentInit();
|
||||
|
||||
expect((<Spy>activeTab.next).calls.argsFor(0)[0]).toEqual(tabId);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,29 @@
|
|||
import { Directive, Inject, Host, AfterContentInit, Input } from 'ng-metadata/core';
|
||||
import { CorTabPanelComponent } from '../cor-tab-panel/cor-tab-panel.component';
|
||||
|
||||
|
||||
/**
|
||||
* Adds routing capabilities to cor-tab-panel, either using URL query parameters, or browser cookie.
|
||||
*/
|
||||
@Directive({
|
||||
selector: '[corNavTabs]'
|
||||
})
|
||||
export class CorNavTabsDirective implements AfterContentInit {
|
||||
|
||||
constructor(@Host() @Inject(CorTabPanelComponent) private panel: CorTabPanelComponent,
|
||||
@Inject('$location') private $location: ng.ILocationService,
|
||||
@Inject('$rootScope') private $rootScope: ng.IRootScopeService) {
|
||||
this.$rootScope.$on('$routeUpdate', () => {
|
||||
const tabId: string = this.$location.search()['tab'];
|
||||
this.panel.activeTab.next(tabId);
|
||||
});
|
||||
}
|
||||
|
||||
public ngAfterContentInit(): void {
|
||||
this.panel.basePath = this.$location.path();
|
||||
|
||||
// Set initial tab
|
||||
const tabId: string = this.$location.search()['tab'];
|
||||
this.panel.activeTab.next(tabId);
|
||||
}
|
||||
}
|
|
@ -6,10 +6,12 @@ import { Component } from 'ng-metadata/core';
|
|||
*/
|
||||
@Component({
|
||||
selector: 'cor-tab-content',
|
||||
templateUrl: '/static/js/directives/ui/cor-tabs/cor-tab-content.component.html',
|
||||
templateUrl: '/static/js/directives/ui/cor-tabs/cor-tab-content/cor-tab-content.component.html',
|
||||
legacy: {
|
||||
transclude: true,
|
||||
replace: true,
|
||||
}
|
||||
})
|
||||
export class CorTabContentComponent {}
|
||||
export class CorTabContentComponent {
|
||||
|
||||
}
|
|
@ -1,83 +0,0 @@
|
|||
import { CorTabComponent } from './cor-tab.component';
|
||||
import { CorTabPanelComponent } from './cor-tab-panel.component';
|
||||
|
||||
/**
|
||||
* Defines an interface for reading and writing the current tab state.
|
||||
*/
|
||||
export interface CorTabCurrentHandler {
|
||||
getInitialTabId(): string
|
||||
|
||||
notifyTabChanged(tab: CorTabComponent, isDefaultTab: boolean)
|
||||
|
||||
dispose(): void
|
||||
}
|
||||
|
||||
export function CorTabCurrentHandlerFactory(options?: any): CorTabCurrentHandler {
|
||||
switch (options.type) {
|
||||
case "cookie":
|
||||
return new CookieCurrentTabHandler(options.cookieService, options.cookieName);
|
||||
default:
|
||||
return new LocationCurrentTabHandler(options.panel, options.$location, options.$rootScope);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads and writes the tab from the `tab` query parameter in the location.
|
||||
*/
|
||||
export class LocationCurrentTabHandler implements CorTabCurrentHandler {
|
||||
private cancelWatchHandle: Function;
|
||||
|
||||
constructor (private panel: CorTabPanelComponent,
|
||||
private $location: ng.ILocationService,
|
||||
private $rootScope: ng.IRootScopeService) {
|
||||
}
|
||||
|
||||
private checkLocation(): void {
|
||||
var specifiedTabId = this.$location.search()['tab'];
|
||||
var specifiedTab = this.panel.findTab(specifiedTabId);
|
||||
this.panel.setActiveTab(specifiedTab);
|
||||
}
|
||||
|
||||
public getInitialTabId(): string {
|
||||
if (!this.cancelWatchHandle) {
|
||||
this.cancelWatchHandle = this.$rootScope.$on('$routeUpdate', () => this.checkLocation());
|
||||
}
|
||||
return this.$location.search()['tab'];
|
||||
}
|
||||
|
||||
public notifyTabChanged(tab: CorTabComponent, isDefaultTab: boolean) {
|
||||
var newSearch = $.extend(this.$location.search(), {});
|
||||
if (isDefaultTab) {
|
||||
delete newSearch['tab'];
|
||||
} else {
|
||||
newSearch['tab'] = tab.tabId;
|
||||
}
|
||||
|
||||
this.$location.search(newSearch);
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this.cancelWatchHandle();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads and writes the tab from a cookie,.
|
||||
*/
|
||||
export class CookieCurrentTabHandler implements CorTabCurrentHandler {
|
||||
constructor (private CookieService: any, private cookieName: string) {}
|
||||
|
||||
public getInitialTabId(): string {
|
||||
return this.CookieService.get(this.cookieName);
|
||||
}
|
||||
|
||||
public notifyTabChanged(tab: CorTabComponent, isDefaultTab: boolean) {
|
||||
if (isDefaultTab) {
|
||||
this.CookieService.clear(this.cookieName);
|
||||
} else {
|
||||
this.CookieService.putPermanent(this.cookieName, tab.tabId);
|
||||
}
|
||||
}
|
||||
|
||||
public dispose(): void {}
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
import { Component, Input, Inject, Host, OnInit } from 'ng-metadata/core';
|
||||
import { CorTabPanelComponent } from './cor-tab-panel.component';
|
||||
|
||||
|
||||
/**
|
||||
* A component that creates a single tab pane under a cor-tabs component.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'cor-tab-pane',
|
||||
templateUrl: '/static/js/directives/ui/cor-tabs/cor-tab-pane.component.html',
|
||||
legacy: {
|
||||
transclude: true,
|
||||
}
|
||||
})
|
||||
export class CorTabPaneComponent implements OnInit {
|
||||
@Input('@') public id: string;
|
||||
|
||||
// Whether this is the active tab.
|
||||
private isActiveTab: boolean = false;
|
||||
|
||||
constructor(@Host() @Inject(CorTabPanelComponent) private parent: CorTabPanelComponent) {
|
||||
}
|
||||
|
||||
public ngOnInit(): void {
|
||||
this.parent.addTabPane(this);
|
||||
}
|
||||
|
||||
public changeState(isActive: boolean): void {
|
||||
this.isActiveTab = isActive;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
import { CorTabPaneComponent } from './cor-tab-pane.component';
|
||||
import { CorTabPanelComponent } from '../cor-tab-panel/cor-tab-panel.component';
|
||||
import { Mock } from 'ts-mocks';
|
||||
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
|
||||
import Spy = jasmine.Spy;
|
||||
|
||||
|
||||
describe("CorTabPaneComponent", () => {
|
||||
var component: CorTabPaneComponent;
|
||||
var panelMock: Mock<CorTabPanelComponent>;
|
||||
var activeTab: BehaviorSubject<string>;
|
||||
|
||||
beforeEach(() => {
|
||||
activeTab = new BehaviorSubject<string>(null);
|
||||
spyOn(activeTab, "subscribe").and.callThrough();
|
||||
panelMock = new Mock<CorTabPanelComponent>();
|
||||
panelMock.setup(mock => mock.activeTab).is(activeTab);
|
||||
|
||||
component = new CorTabPaneComponent(panelMock.Object);
|
||||
component.id = 'description';
|
||||
});
|
||||
|
||||
describe("ngOnInit", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
panelMock.setup(mock => mock.addTabPane);
|
||||
});
|
||||
|
||||
it("adds self as tab pane to panel", () => {
|
||||
component.ngOnInit();
|
||||
|
||||
expect((<Spy>panelMock.Object.addTabPane).calls.argsFor(0)[0]).toBe(component);
|
||||
});
|
||||
|
||||
it("subscribes to active tab changes", () => {
|
||||
component.ngOnInit();
|
||||
|
||||
expect((<Spy>panelMock.Object.activeTab.subscribe)).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does nothing if active tab ID is undefined", () => {
|
||||
component.ngOnInit();
|
||||
component.isActiveTab = true;
|
||||
panelMock.Object.activeTab.next(null);
|
||||
|
||||
expect(component.isActiveTab).toEqual(true);
|
||||
});
|
||||
|
||||
it("sets self as active if active tab ID matches tab ID", () => {
|
||||
component.ngOnInit();
|
||||
panelMock.Object.activeTab.next(component.id);
|
||||
|
||||
expect(component.isActiveTab).toEqual(true);
|
||||
});
|
||||
|
||||
it("sets self as inactive if active tab ID does not match tab ID", () => {
|
||||
component.ngOnInit();
|
||||
panelMock.Object.activeTab.next(component.id.split('').reverse().join(''));
|
||||
|
||||
expect(component.isActiveTab).toEqual(false);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,35 @@
|
|||
import { Component, Input, Inject, Host, OnInit } from 'ng-metadata/core';
|
||||
import { CorTabPanelComponent } from '../cor-tab-panel/cor-tab-panel.component';
|
||||
import 'rxjs/add/operator/filter';
|
||||
|
||||
|
||||
/**
|
||||
* A component that creates a single tab pane under a cor-tabs component.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'cor-tab-pane',
|
||||
templateUrl: '/static/js/directives/ui/cor-tabs/cor-tab-pane/cor-tab-pane.component.html',
|
||||
legacy: {
|
||||
transclude: true,
|
||||
}
|
||||
})
|
||||
export class CorTabPaneComponent implements OnInit {
|
||||
|
||||
@Input('@') public id: string;
|
||||
|
||||
public isActiveTab: boolean = false;
|
||||
|
||||
constructor(@Host() @Inject(CorTabPanelComponent) private panel: CorTabPanelComponent) {
|
||||
|
||||
}
|
||||
|
||||
public ngOnInit(): void {
|
||||
this.panel.addTabPane(this);
|
||||
|
||||
this.panel.activeTab
|
||||
.filter(tabId => tabId != undefined)
|
||||
.subscribe((tabId: string) => {
|
||||
this.isActiveTab = (this.id === tabId);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
<div class="co-main-content-panel co-tab-panel co-fx-box-shadow-heavy">
|
||||
<div class="co-tab-container" ng-transclude></div>
|
||||
</div>
|
|
@ -1,117 +0,0 @@
|
|||
import { Component, Input, Inject, OnDestroy } from 'ng-metadata/core';
|
||||
import { CorTabComponent } from './cor-tab.component';
|
||||
import { CorTabPaneComponent } from './cor-tab-pane.component';
|
||||
import { CorTabCurrentHandler, LocationCurrentTabHandler, CookieCurrentTabHandler, CorTabCurrentHandlerFactory } from './cor-tab-handlers'
|
||||
|
||||
|
||||
/**
|
||||
* A component that contains a cor-tabs and handles all of its logic.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'cor-tab-panel',
|
||||
templateUrl: '/static/js/directives/ui/cor-tabs/cor-tab-panel.component.html',
|
||||
legacy: {
|
||||
transclude: true,
|
||||
}
|
||||
})
|
||||
export class CorTabPanelComponent implements OnDestroy {
|
||||
// If 'true', the currently selected tab will be remembered via a cookie and not the page URL.
|
||||
@Input('@') public rememberCookie: string;
|
||||
|
||||
// The tabs under this tabs component.
|
||||
private tabs: CorTabComponent[] = [];
|
||||
|
||||
// The tab panes under the tabs component, indexed by the tab id.
|
||||
private tabPanes: {[id: string]: CorTabPaneComponent} = {};
|
||||
|
||||
// The currently active tab, if any.
|
||||
private activeTab: CorTabComponent = null;
|
||||
|
||||
// Whether the initial tab was set.
|
||||
private initialTabSet: boolean = false;
|
||||
|
||||
// The handler to use to read/write the current tab.
|
||||
private currentTabHandler: CorTabCurrentHandler = null;
|
||||
|
||||
constructor(@Inject('$location') private $location: ng.ILocationService,
|
||||
@Inject('$rootScope') private $rootScope: ng.IRootScopeService,
|
||||
@Inject('CookieService') private CookieService: any,
|
||||
@Inject('CorTabCurrentHandlerFactory') private CorTabCurrentHandlerFactory: (Object) => CorTabCurrentHandler) {
|
||||
}
|
||||
|
||||
public ngOnDestroy(): void {
|
||||
if (this.currentTabHandler) {
|
||||
this.currentTabHandler.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public tabClicked(tab: CorTabComponent): void {
|
||||
this.setActiveTab(tab);
|
||||
}
|
||||
|
||||
public findTab(tabId: string): CorTabComponent {
|
||||
if (!this.tabs.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var tab = this.tabs.find(function(current) {
|
||||
return current.tabId == tabId;
|
||||
}) || this.tabs[0];
|
||||
|
||||
if (!this.tabPanes[tab.tabId]) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return tab;
|
||||
}
|
||||
|
||||
public setActiveTab(tab: CorTabComponent): void {
|
||||
if (this.activeTab == tab) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.activeTab != null) {
|
||||
this.activeTab.changeState(false);
|
||||
this.tabPanes[this.activeTab.tabId].changeState(false);
|
||||
}
|
||||
|
||||
this.activeTab = tab;
|
||||
this.activeTab.changeState(true);
|
||||
this.tabPanes[this.activeTab.tabId].changeState(true);
|
||||
this.currentTabHandler.notifyTabChanged(tab, this.tabs[0] == tab);
|
||||
}
|
||||
|
||||
public addTab(tab: CorTabComponent): void {
|
||||
this.tabs.push(tab);
|
||||
this.checkInitialTab();
|
||||
}
|
||||
|
||||
public addTabPane(tabPane: CorTabPaneComponent): void {
|
||||
this.tabPanes[tabPane.id] = tabPane;
|
||||
this.checkInitialTab();
|
||||
}
|
||||
|
||||
private checkInitialTab(): void {
|
||||
if (this.tabs.length < 1 || this.initialTabSet) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentTabHandler = this.CorTabCurrentHandlerFactory({
|
||||
type: this.rememberCookie ? 'cookie' : 'location',
|
||||
cookieService: this.CookieService,
|
||||
cookeName: this.rememberCookie,
|
||||
panel: this,
|
||||
$location: this.$location,
|
||||
$rootScope: this.$rootScope,
|
||||
});
|
||||
|
||||
var tabId = this.currentTabHandler.getInitialTabId();
|
||||
var tab = this.findTab(tabId);
|
||||
if (!tab) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.initialTabSet = true;
|
||||
this.setActiveTab(tab);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
<div class="co-main-content-panel co-tab-panel co-fx-box-shadow-heavy">
|
||||
<div class="co-tab-container" ng-class="$ctrl.isVertical() ? 'vertical': 'horizontal'" ng-transclude></div>
|
||||
</div>
|
|
@ -0,0 +1,132 @@
|
|||
import { CorTabPanelComponent } from './cor-tab-panel.component';
|
||||
import { CorTabComponent } from '../cor-tab/cor-tab.component';
|
||||
import { SimpleChanges } from 'ng-metadata/core';
|
||||
import Spy = jasmine.Spy;
|
||||
|
||||
|
||||
describe("CorTabPanelComponent", () => {
|
||||
var component: CorTabPanelComponent;
|
||||
|
||||
beforeEach(() => {
|
||||
component = new CorTabPanelComponent();
|
||||
});
|
||||
|
||||
describe("ngOnInit", () => {
|
||||
var tabs: CorTabComponent[] = [];
|
||||
|
||||
beforeEach(() => {
|
||||
// Add tabs to panel
|
||||
tabs.push(new CorTabComponent(component));
|
||||
tabs[0].tabId = "info";
|
||||
tabs.forEach((tab) => component.addTab(tab));
|
||||
|
||||
spyOn(component.activeTab, "subscribe").and.callThrough();
|
||||
spyOn(component.activeTab, "next").and.callThrough();
|
||||
spyOn(component.tabChange, "emit").and.returnValue(null);
|
||||
});
|
||||
|
||||
it("subscribes to active tab changes", () => {
|
||||
component.ngOnInit();
|
||||
|
||||
expect(<Spy>component.activeTab.subscribe).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("emits next active tab with tab ID of first registered tab if given tab ID is null", () => {
|
||||
component.ngOnInit();
|
||||
component.activeTab.next(null);
|
||||
|
||||
expect((<Spy>component.activeTab.next).calls.argsFor(1)[0]).toEqual(tabs[0].tabId);
|
||||
});
|
||||
|
||||
it("does not emit output event for tab change if tab ID is null", () => {
|
||||
component.ngOnInit();
|
||||
component.activeTab.next(null);
|
||||
|
||||
expect((<Spy>component.tabChange.emit).calls.allArgs).not.toContain(null);
|
||||
});
|
||||
|
||||
it("emits output event for tab change when tab ID is not null", () => {
|
||||
component.ngOnInit();
|
||||
const tabId: string = "description";
|
||||
component.activeTab.next(tabId);
|
||||
|
||||
expect((<Spy>component.tabChange.emit).calls.argsFor(1)[0]).toEqual(tabId);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ngOnChanges", () => {
|
||||
var changes: SimpleChanges;
|
||||
var tabs: CorTabComponent[] = [];
|
||||
|
||||
beforeEach(() => {
|
||||
// Add tabs to panel
|
||||
tabs.push(new CorTabComponent(component));
|
||||
tabs.forEach((tab) => component.addTab(tab));
|
||||
|
||||
changes = {
|
||||
'selectedIndex': {
|
||||
currentValue: 0,
|
||||
previousValue: null,
|
||||
isFirstChange: () => false
|
||||
},
|
||||
};
|
||||
|
||||
spyOn(component.activeTab, "next").and.returnValue(null);
|
||||
});
|
||||
|
||||
it("emits next active tab if 'selectedIndex' input changes and is valid", () => {
|
||||
component.ngOnChanges(changes);
|
||||
|
||||
expect((<Spy>component.activeTab.next).calls.argsFor(0)[0]).toEqual(tabs[changes['selectedIndex'].currentValue].tabId);
|
||||
});
|
||||
|
||||
it("does nothing if 'selectedIndex' input changed to invalid value", () => {
|
||||
changes['selectedIndex'].currentValue = 100;
|
||||
component.ngOnChanges(changes);
|
||||
|
||||
expect(<Spy>component.activeTab.next).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("addTab", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(component.activeTab, "next").and.returnValue(null);
|
||||
});
|
||||
|
||||
it("emits next active tab if it is not set", () => {
|
||||
const tab: CorTabComponent = new CorTabComponent(component);
|
||||
component.addTab(tab);
|
||||
|
||||
expect((<Spy>component.activeTab.next).calls.argsFor(0)[0]).toEqual(tab.tabId);
|
||||
});
|
||||
|
||||
it("does not emit next active tab if it is already set", () => {
|
||||
spyOn(component.activeTab, "getValue").and.returnValue("description");
|
||||
const tab: CorTabComponent = new CorTabComponent(component);
|
||||
component.addTab(tab);
|
||||
|
||||
expect(<Spy>component.activeTab.next).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("addTabPane", () => {
|
||||
|
||||
});
|
||||
|
||||
describe("isVertical", () => {
|
||||
|
||||
it("returns true if orientation is 'vertical'", () => {
|
||||
component.orientation = 'vertical';
|
||||
const isVertical: boolean = component.isVertical();
|
||||
|
||||
expect(isVertical).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false if orientation is not 'vertical'", () => {
|
||||
const isVertical: boolean = component.isVertical();
|
||||
|
||||
expect(isVertical).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,65 @@
|
|||
import { Component, Input, Output, EventEmitter, OnChanges, SimpleChanges, OnInit } from 'ng-metadata/core';
|
||||
import { CorTabComponent } from '../cor-tab/cor-tab.component';
|
||||
import { CorTabPaneComponent } from '../cor-tab-pane/cor-tab-pane.component';
|
||||
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
|
||||
|
||||
|
||||
/**
|
||||
* A component that contains a cor-tabs and handles all of its logic.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'cor-tab-panel',
|
||||
templateUrl: '/static/js/directives/ui/cor-tabs/cor-tab-panel/cor-tab-panel.component.html',
|
||||
legacy: {
|
||||
transclude: true
|
||||
}
|
||||
})
|
||||
export class CorTabPanelComponent implements OnInit, OnChanges {
|
||||
|
||||
@Input('@') public orientation: 'horizontal' | 'vertical' = 'horizontal';
|
||||
|
||||
@Output() public tabChange: EventEmitter<string> = new EventEmitter();
|
||||
|
||||
public basePath: string;
|
||||
public activeTab: BehaviorSubject<string> = new BehaviorSubject(null);
|
||||
|
||||
private tabs: CorTabComponent[] = [];
|
||||
private tabPanes: {[id: string]: CorTabPaneComponent} = {};
|
||||
|
||||
public ngOnInit(): void {
|
||||
this.activeTab.subscribe((tabId: string) => {
|
||||
// Catch null values and replace with tabId of first tab
|
||||
if (!tabId && this.tabs[0]) {
|
||||
this.activeTab.next(this.tabs[0].tabId);
|
||||
} else {
|
||||
this.tabChange.emit(tabId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public ngOnChanges(changes: SimpleChanges): void {
|
||||
switch (Object.keys(changes)[0]) {
|
||||
case 'selectedIndex':
|
||||
if (this.tabs.length > changes['selectedIndex'].currentValue) {
|
||||
this.activeTab.next(this.tabs[changes['selectedIndex'].currentValue].tabId);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public addTab(tab: CorTabComponent): void {
|
||||
this.tabs.push(tab);
|
||||
|
||||
if (!this.activeTab.getValue()) {
|
||||
this.activeTab.next(this.tabs[0].tabId);
|
||||
}
|
||||
}
|
||||
|
||||
public addTabPane(tabPane: CorTabPaneComponent): void {
|
||||
this.tabPanes[tabPane.id] = tabPane;
|
||||
}
|
||||
|
||||
public isVertical(): boolean {
|
||||
return this.orientation == 'vertical';
|
||||
}
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
<li ng-class="{'active': $ctrl.isActive}">
|
||||
<a ng-click="$ctrl.tabClicked()">
|
||||
<span data-title="{{ ::$ctrl.tabTitle }}"
|
||||
data-placement="right"
|
||||
data-container="body"
|
||||
style="display: inline-block"
|
||||
bs-tooltip><span ng-transclude/></span><span class="visible-xs-inline xs-label">{{ ::$ctrl.tabTitle }}</span>
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
|
@ -1,46 +0,0 @@
|
|||
import { Component, Input, Output, Inject, EventEmitter, Host, OnInit } from 'ng-metadata/core';
|
||||
import { CorTabPanelComponent } from './cor-tab-panel.component';
|
||||
|
||||
|
||||
/**
|
||||
* A component that creates a single tab under a cor-tabs component.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'cor-tab',
|
||||
templateUrl: '/static/js/directives/ui/cor-tabs/cor-tab.component.html',
|
||||
legacy: {
|
||||
transclude: true,
|
||||
}
|
||||
})
|
||||
export class CorTabComponent implements OnInit {
|
||||
@Input('@') public tabId: string;
|
||||
@Input('@') public tabTitle: string;
|
||||
|
||||
@Output() public tabInit: EventEmitter<any> = new EventEmitter();
|
||||
@Output() public tabShown: EventEmitter<any> = new EventEmitter();
|
||||
@Output() public tabHidden: EventEmitter<any> = new EventEmitter();
|
||||
|
||||
// Whether this is the active tab.
|
||||
private isActive: boolean = false;
|
||||
|
||||
constructor(@Host() @Inject(CorTabPanelComponent) private parent: CorTabPanelComponent) {
|
||||
}
|
||||
|
||||
public ngOnInit(): void {
|
||||
this.parent.addTab(this);
|
||||
}
|
||||
|
||||
public changeState(isActive: boolean): void {
|
||||
this.isActive = isActive;
|
||||
if (isActive) {
|
||||
this.tabInit.emit({});
|
||||
this.tabShown.emit({});
|
||||
} else {
|
||||
this.tabHidden.emit({});
|
||||
}
|
||||
}
|
||||
|
||||
private tabClicked(): void {
|
||||
this.parent.tabClicked(this);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
<li class="cor-tab-itself" ng-class="{'active': $ctrl.isActive, 'co-top-tab': !$ctrl.parent.isVertical()}">
|
||||
<a href="{{ $ctrl.panel.basePath ? $ctrl.panel.basePath + '?tab=' + $ctrl.tabId : '' }}"
|
||||
ng-click="$ctrl.tabClicked($event)">
|
||||
<span class="cor-tab-icon"
|
||||
data-title="{{ ::($ctrl.panel.isVertical() ? $ctrl.tabTitle : '') }}"
|
||||
data-placement="right"
|
||||
data-container="body"
|
||||
style="display: inline-block"
|
||||
bs-tooltip>
|
||||
<span ng-transclude /><span class="horizontal-label">{{ ::$ctrl.tabTitle }}</span>
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
|
@ -0,0 +1,85 @@
|
|||
import { CorTabComponent } from './cor-tab.component';
|
||||
import { CorTabPanelComponent } from '../cor-tab-panel/cor-tab-panel.component';
|
||||
import { Mock } from 'ts-mocks';
|
||||
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
|
||||
import Spy = jasmine.Spy;
|
||||
|
||||
|
||||
describe("CorTabComponent", () => {
|
||||
var component: CorTabComponent;
|
||||
var panelMock: Mock<CorTabPanelComponent>;
|
||||
var activeTab: BehaviorSubject<string>;
|
||||
|
||||
beforeEach(() => {
|
||||
activeTab = new BehaviorSubject<string>(null);
|
||||
spyOn(activeTab, "subscribe").and.callThrough();
|
||||
panelMock = new Mock<CorTabPanelComponent>();
|
||||
panelMock.setup(mock => mock.activeTab).is(activeTab);
|
||||
|
||||
component = new CorTabComponent(panelMock.Object);
|
||||
});
|
||||
|
||||
describe("ngOnInit", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
panelMock.setup(mock => mock.addTab);
|
||||
spyOn(component.tabInit, "emit").and.returnValue(null);
|
||||
spyOn(component.tabShow, "emit").and.returnValue(null);
|
||||
spyOn(component.tabHide, "emit").and.returnValue(null);
|
||||
component.tabId = "description";
|
||||
});
|
||||
|
||||
it("subscribes to active tab changes", () => {
|
||||
component.ngOnInit();
|
||||
|
||||
expect((<Spy>panelMock.Object.activeTab.subscribe)).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does nothing if active tab ID is undefined", () => {
|
||||
component.ngOnInit();
|
||||
panelMock.Object.activeTab.next(null);
|
||||
|
||||
expect(<Spy>component.tabInit.emit).not.toHaveBeenCalled();
|
||||
expect(<Spy>component.tabShow.emit).not.toHaveBeenCalled();
|
||||
expect(<Spy>component.tabHide.emit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("emits output event for tab init if it is new active tab", () => {
|
||||
component.ngOnInit();
|
||||
panelMock.Object.activeTab.next(component.tabId);
|
||||
|
||||
expect(<Spy>component.tabInit.emit).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("emits output event for tab show if it is new active tab", () => {
|
||||
component.ngOnInit();
|
||||
panelMock.Object.activeTab.next(component.tabId);
|
||||
|
||||
expect(<Spy>component.tabShow.emit).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("emits output event for tab hide if active tab changes to different tab", () => {
|
||||
const newTabId: string = component.tabId.split('').reverse().join('');
|
||||
component.ngOnInit();
|
||||
// Call twice, first time to set 'isActive' to true
|
||||
panelMock.Object.activeTab.next(component.tabId);
|
||||
panelMock.Object.activeTab.next(newTabId);
|
||||
|
||||
expect(<Spy>component.tabHide.emit).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not emit output event for tab hide if was not previously active tab", () => {
|
||||
const newTabId: string = component.tabId.split('').reverse().join('');
|
||||
component.ngOnInit();
|
||||
panelMock.Object.activeTab.next(newTabId);
|
||||
|
||||
expect(<Spy>component.tabHide.emit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("adds self as tab to panel", () => {
|
||||
component.ngOnInit();
|
||||
|
||||
expect((<Spy>panelMock.Object.addTab).calls.argsFor(0)[0]).toBe(component);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,54 @@
|
|||
import { Component, Input, Output, Inject, EventEmitter, Host, OnInit } from 'ng-metadata/core';
|
||||
import { CorTabPanelComponent } from '../cor-tab-panel/cor-tab-panel.component';
|
||||
import 'rxjs/add/operator/filter';
|
||||
|
||||
|
||||
/**
|
||||
* A component that creates a single tab under a cor-tabs component.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'cor-tab',
|
||||
templateUrl: '/static/js/directives/ui/cor-tabs/cor-tab/cor-tab.component.html',
|
||||
legacy: {
|
||||
transclude: true,
|
||||
}
|
||||
})
|
||||
export class CorTabComponent implements OnInit {
|
||||
|
||||
@Input('@') public tabId: string;
|
||||
@Input('@') public tabTitle: string;
|
||||
|
||||
@Output() public tabInit: EventEmitter<any> = new EventEmitter();
|
||||
@Output() public tabShow: EventEmitter<any> = new EventEmitter();
|
||||
@Output() public tabHide: EventEmitter<any> = new EventEmitter();
|
||||
|
||||
private isActive: boolean = false;
|
||||
|
||||
constructor(@Host() @Inject(CorTabPanelComponent) private panel: CorTabPanelComponent) {
|
||||
|
||||
}
|
||||
|
||||
public ngOnInit(): void {
|
||||
this.panel.activeTab
|
||||
.filter(tabId => tabId != undefined)
|
||||
.subscribe((tabId: string) => {
|
||||
if (!this.isActive && this.tabId === tabId) {
|
||||
this.isActive = true;
|
||||
this.tabInit.emit({});
|
||||
this.tabShow.emit({});
|
||||
} else if (this.isActive && this.tabId !== tabId) {
|
||||
this.isActive = false;
|
||||
this.tabHide.emit({});
|
||||
}
|
||||
});
|
||||
|
||||
this.panel.addTab(this);
|
||||
}
|
||||
|
||||
private tabClicked(event: MouseEvent): void {
|
||||
if (!this.panel.basePath) {
|
||||
event.preventDefault();
|
||||
this.panel.activeTab.next(this.tabId);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
<span class="co-tab-element" ng-class="$ctrl.isClosed ? 'closed' : 'open'">
|
||||
<span class="xs-toggle" ng-click="$ctrl.toggleClosed($event)"></span>
|
||||
<ul class="co-tabs col-md-1" ng-transclude></ul>
|
||||
<ul ng-class="$ctrl.parent.isVertical() ? 'co-tabs col-md-1' : 'co-top-tab-bar'" ng-transclude></ul>
|
||||
</span>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Component, Host, Inject } from 'ng-metadata/core';
|
||||
import { CorTabComponent } from './cor-tab.component';
|
||||
import { Component, Input, Output, Inject, EventEmitter, Host } from 'ng-metadata/core';
|
||||
import { CorTabPanelComponent } from './cor-tab-panel/cor-tab-panel.component';
|
||||
|
||||
|
||||
/**
|
||||
|
@ -13,9 +13,13 @@ import { CorTabComponent } from './cor-tab.component';
|
|||
}
|
||||
})
|
||||
export class CorTabsComponent {
|
||||
// If true, the tabs are in a closed state. Only applies in the mobile view.
|
||||
|
||||
private isClosed: boolean = true;
|
||||
|
||||
constructor(@Host() @Inject(CorTabPanelComponent) private parent: CorTabPanelComponent) {
|
||||
|
||||
}
|
||||
|
||||
private toggleClosed(e): void {
|
||||
this.isClosed = !this.isClosed;
|
||||
}
|
||||
|
|
33
static/js/directives/ui/cor-tabs/cor-tabs.module.ts
Normal file
33
static/js/directives/ui/cor-tabs/cor-tabs.module.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
import { NgModule } from 'ng-metadata/core';
|
||||
import { CorTabsComponent } from './cor-tabs.component';
|
||||
import { CorTabComponent } from './cor-tab/cor-tab.component';
|
||||
import { CorNavTabsDirective } from './cor-nav-tabs/cor-nav-tabs.directive';
|
||||
import { CorTabContentComponent } from './cor-tab-content/cor-tab-content.component';
|
||||
import { CorTabPaneComponent } from './cor-tab-pane/cor-tab-pane.component';
|
||||
import { CorTabPanelComponent } from './cor-tab-panel/cor-tab-panel.component';
|
||||
import { CorCookieTabsDirective } from './cor-cookie-tabs/cor-cookie-tabs.directive';
|
||||
|
||||
|
||||
/**
|
||||
* Module containing everything needed for cor-tabs.
|
||||
*/
|
||||
@NgModule({
|
||||
imports: [
|
||||
|
||||
],
|
||||
declarations: [
|
||||
CorNavTabsDirective,
|
||||
CorTabComponent,
|
||||
CorTabContentComponent,
|
||||
CorTabPaneComponent,
|
||||
CorTabPanelComponent,
|
||||
CorTabsComponent,
|
||||
CorCookieTabsDirective,
|
||||
],
|
||||
providers: [
|
||||
|
||||
]
|
||||
})
|
||||
export class CorTabsModule {
|
||||
|
||||
}
|
13
static/js/directives/ui/cor-tabs/cor-tabs.view-object.ts
Normal file
13
static/js/directives/ui/cor-tabs/cor-tabs.view-object.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import { element, by, browser, $, ElementFinder, ExpectedConditions as until } from 'protractor';
|
||||
|
||||
|
||||
export class CorTabsViewObject {
|
||||
|
||||
public selectTabByTitle(title: string): Promise<void> {
|
||||
return Promise.resolve($(`cor-tab[tab-title="${title}"] a`).click());
|
||||
}
|
||||
|
||||
public isActiveTab(title: string): Promise<boolean> {
|
||||
return Promise.resolve($(`cor-tab[tab-title="${title}"] .cor-tab-itself.active`).isPresent());
|
||||
}
|
||||
}
|
|
@ -60,7 +60,7 @@ angular.module('quay').directive('credentialsDialog', function () {
|
|||
|
||||
$scope.downloadFile = function(info) {
|
||||
var blob = new Blob([info.contents]);
|
||||
saveAs(blob, info.filename);
|
||||
FileSaver.saveAs(blob, info.filename);
|
||||
};
|
||||
|
||||
$scope.viewFile = function(context) {
|
||||
|
@ -170,7 +170,7 @@ angular.module('quay').directive('credentialsDialog', function () {
|
|||
return '';
|
||||
}
|
||||
|
||||
return $scope.getEscapedUsername(credentials) + '-pull-secret';
|
||||
return $scope.getEscapedUsername(credentials).toLowerCase() + '-pull-secret';
|
||||
};
|
||||
|
||||
$scope.getKubernetesFile = function(credentials) {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { DockerfilePathSelectComponent } from './dockerfile-path-select.component';
|
||||
import { DockerfilePathSelectComponent, PathChangeEvent } from './dockerfile-path-select.component';
|
||||
|
||||
|
||||
describe("DockerfilePathSelectComponent", () => {
|
||||
|
@ -60,6 +60,16 @@ describe("DockerfilePathSelectComponent", () => {
|
|||
|
||||
expect(component.isValidPath).toBe(false);
|
||||
});
|
||||
|
||||
it("emits output event indicating Dockerfile path has changed", (done) => {
|
||||
component.pathChanged.subscribe((event: PathChangeEvent) => {
|
||||
expect(event.path).toEqual(newPath);
|
||||
expect(event.isValid).toBe(component.isValidPath);
|
||||
done();
|
||||
});
|
||||
|
||||
component.setPath(newPath);
|
||||
});
|
||||
});
|
||||
|
||||
describe("setCurrentPath", () => {
|
||||
|
@ -86,5 +96,15 @@ describe("DockerfilePathSelectComponent", () => {
|
|||
|
||||
expect(component.isValidPath).toBe(false);
|
||||
});
|
||||
|
||||
it("emits output event indicating Dockerfile path has changed", (done) => {
|
||||
component.pathChanged.subscribe((event: PathChangeEvent) => {
|
||||
expect(event.path).toEqual(newPath);
|
||||
expect(event.isValid).toBe(component.isValidPath);
|
||||
done();
|
||||
});
|
||||
|
||||
component.setSelectedPath(newPath);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Input, Component, OnChanges, SimpleChanges } from 'ng-metadata/core';
|
||||
import { Input, Output, EventEmitter, Component, OnChanges, SimpleChanges } from 'ng-metadata/core';
|
||||
|
||||
|
||||
/**
|
||||
|
@ -10,11 +10,11 @@ import { Input, Component, OnChanges, SimpleChanges } from 'ng-metadata/core';
|
|||
})
|
||||
export class DockerfilePathSelectComponent implements OnChanges {
|
||||
|
||||
// FIXME: Use one-way data binding
|
||||
@Input('=') public currentPath: string = '';
|
||||
@Input('=') public isValidPath: boolean;
|
||||
@Input('=') public paths: string[];
|
||||
@Input('=') public supportsFullListing: boolean;
|
||||
@Input('<') public currentPath: string = '';
|
||||
@Input('<') public paths: string[];
|
||||
@Input('<') public supportsFullListing: boolean;
|
||||
@Output() public pathChanged: EventEmitter<PathChangeEvent> = new EventEmitter();
|
||||
public isValidPath: boolean;
|
||||
private isUnknownPath: boolean = true;
|
||||
private selectedPath: string | null = null;
|
||||
|
||||
|
@ -26,12 +26,16 @@ export class DockerfilePathSelectComponent implements OnChanges {
|
|||
this.currentPath = path;
|
||||
this.selectedPath = null;
|
||||
this.isValidPath = this.checkPath(path, this.paths, this.supportsFullListing);
|
||||
|
||||
this.pathChanged.emit({path: this.currentPath, isValid: this.isValidPath});
|
||||
}
|
||||
|
||||
public setSelectedPath(path: string): void {
|
||||
this.currentPath = path;
|
||||
this.selectedPath = path;
|
||||
this.isValidPath = this.checkPath(path, this.paths, this.supportsFullListing);
|
||||
|
||||
this.pathChanged.emit({path: this.currentPath, isValid: this.isValidPath});
|
||||
}
|
||||
|
||||
private checkPath(path: string = '', paths: string[] = [], supportsFullListing: boolean): boolean {
|
||||
|
@ -45,3 +49,12 @@ export class DockerfilePathSelectComponent implements OnChanges {
|
|||
return isValidPath;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Dockerfile path changed event.
|
||||
*/
|
||||
export type PathChangeEvent = {
|
||||
path: string;
|
||||
isValid: boolean;
|
||||
};
|
||||
|
|
|
@ -80,7 +80,7 @@ angular.module('quay').directive('dropdownSelect', function ($compile) {
|
|||
source: dropdownHound.ttAdapter(),
|
||||
templates: {
|
||||
'suggestion': function (datum) {
|
||||
template = datum['template'] ? datum['template'](datum) : datum['value'];
|
||||
template = datum['template'] ? datum['template'](datum) : '<span>' + datum['value'] + '</span>';
|
||||
return template;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { Input, Output, Component, Inject } from 'ng-metadata/core';
|
||||
import { Input, Component, Inject } from 'ng-metadata/core';
|
||||
import * as moment from "moment";
|
||||
|
||||
|
||||
/**
|
||||
* A component that allows for selecting a time duration.
|
||||
*/
|
||||
|
@ -9,6 +10,7 @@ import * as moment from "moment";
|
|||
templateUrl: '/static/js/directives/ui/duration-input/duration-input.component.html'
|
||||
})
|
||||
export class DurationInputComponent implements ng.IComponentController {
|
||||
|
||||
@Input('<') public min: string;
|
||||
@Input('<') public max: string;
|
||||
@Input('=?') public value: string;
|
||||
|
@ -17,7 +19,7 @@ export class DurationInputComponent implements ng.IComponentController {
|
|||
private min_s: number;
|
||||
private max_s: number;
|
||||
|
||||
constructor (@Inject('$scope') private $scope: ng.IScope) {
|
||||
constructor(@Inject('$scope') private $scope: ng.IScope) {
|
||||
|
||||
}
|
||||
|
||||
|
@ -33,7 +35,7 @@ export class DurationInputComponent implements ng.IComponentController {
|
|||
}
|
||||
|
||||
private updateValue(): void {
|
||||
this.value = this.seconds + 's';
|
||||
this.value = `${this.seconds}s`;
|
||||
}
|
||||
|
||||
private refresh(): void {
|
||||
|
@ -41,8 +43,8 @@ export class DurationInputComponent implements ng.IComponentController {
|
|||
this.max_s = this.toSeconds(this.max || '1h');
|
||||
|
||||
if (this.value) {
|
||||
this.seconds = this.toSeconds(this.value || '0s')
|
||||
};
|
||||
this.seconds = this.toSeconds(this.value || '0s');
|
||||
}
|
||||
}
|
||||
|
||||
private durationExplanation(durationSeconds: string): string {
|
||||
|
|
|
@ -68,7 +68,10 @@ angular.module('quay').directive('entitySearch', function () {
|
|||
};
|
||||
|
||||
$scope.lazyLoad = function() {
|
||||
if (!$scope.namespace || !$scope.lazyLoading) { return; }
|
||||
if (!$scope.namespace || !$scope.thisUser || !$scope.lazyLoading) { return; }
|
||||
$scope.isAdmin = UserService.isNamespaceAdmin($scope.namespace);
|
||||
$scope.isOrganization = !!UserService.getOrganization($scope.namespace);
|
||||
|
||||
|
||||
// Reset the cached teams and robots.
|
||||
$scope.teams = null;
|
||||
|
@ -359,8 +362,13 @@ angular.module('quay').directive('entitySearch', function () {
|
|||
|
||||
$scope.$watch('namespace', function(namespace) {
|
||||
if (!namespace) { return; }
|
||||
$scope.isAdmin = UserService.isNamespaceAdmin(namespace);
|
||||
$scope.isOrganization = !!UserService.getOrganization(namespace);
|
||||
$scope.lazyLoad();
|
||||
});
|
||||
|
||||
UserService.updateUserIn($scope, function(currentUser){
|
||||
if (currentUser.anonymous) { return; }
|
||||
$scope.thisUser = currentUser;
|
||||
$scope.lazyLoad();
|
||||
});
|
||||
|
||||
$scope.$watch('currentEntity', function(entity) {
|
||||
|
|
|
@ -125,7 +125,6 @@ angular.module('quay').directive('fetchTagDialog', function () {
|
|||
|
||||
updateFormats();
|
||||
|
||||
$element.find('#copyClipboard').clipboardCopy();
|
||||
$element.find('#fetchTagDialog').modal({});
|
||||
}
|
||||
};
|
||||
|
|
|
@ -38,15 +38,15 @@ angular.module('quay').directive('globalMessageTab', function () {
|
|||
|
||||
ApiService.createGlobalMessage(data, null).then(function (resp) {
|
||||
$scope.creatingMessage = false;
|
||||
$scope.newMessage = {
|
||||
'media_type': 'text/markdown',
|
||||
'severity': 'info'
|
||||
};
|
||||
|
||||
$('#createMessageModal').modal('hide');
|
||||
$scope.loadMessageInternal();
|
||||
}, errorHandler)
|
||||
};
|
||||
|
||||
$scope.updateMessage = function(content) {
|
||||
$scope.newMessage.content = content;
|
||||
};
|
||||
|
||||
$scope.showDeleteMessage = function (uuid) {
|
||||
$scope.messageToDelete = uuid;
|
||||
|
|
|
@ -86,6 +86,18 @@ angular.module('quay').directive('imageFeatureView', function () {
|
|||
$scope.$watch('options.reverse', buildOrderedFeatures);
|
||||
$scope.$watch('options.filter', buildOrderedFeatures);
|
||||
|
||||
$scope.$watch('repository', function(repository) {
|
||||
if ($scope.isEnabled && $scope.repository && $scope.image) {
|
||||
loadImageVulnerabilities();
|
||||
}
|
||||
});
|
||||
|
||||
$scope.$watch('image', function(image) {
|
||||
if ($scope.isEnabled && $scope.repository && $scope.image) {
|
||||
loadImageVulnerabilities();
|
||||
}
|
||||
});
|
||||
|
||||
$scope.$watch('isEnabled', function(isEnabled) {
|
||||
if ($scope.isEnabled && $scope.repository && $scope.image) {
|
||||
loadImageVulnerabilities();
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Reference in a new issue