Merge branch 'master' into no-signing-whitelist

This commit is contained in:
Evan Cordell 2017-07-12 15:50:32 -04:00 committed by GitHub
commit 45bf7efc84
434 changed files with 10877 additions and 11061 deletions

View file

@ -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 {

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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 {

View file

@ -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;
}

View file

@ -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 {

View file

@ -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;
}

View file

@ -7,6 +7,10 @@
margin-bottom: 10px;
}
.error-view-element h2 .repo-circle {
margin-right: 16px;
}
.error-view-element h3 {
font-size: 24px;
}

View file

@ -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;
}

View file

@ -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>

View file

@ -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">

View file

@ -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>

View file

@ -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">

View file

@ -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()">
&times;
</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;">
&times;
</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>

View file

@ -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 }}">

View file

@ -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>

View file

@ -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">

View file

@ -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>

View file

@ -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>

View file

@ -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 }}"

View file

@ -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>

View file

@ -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">&times;</button>
<h4 class="modal-title">Edit {{ fieldTitle }}</h4>
</div>
<div class="modal-body">
<div class="wmd-panel">
<div id="wmd-button-bar-description-{{id}}"></div>
<textarea class="wmd-input" id="wmd-input-description-{{id}}" placeholder="Enter {{ fieldTitle }}">{{ content }}</textarea>
</div>
<div id="wmd-preview-description-{{id}}" class="wmd-panel wmd-preview"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" ng-click="saveContent()">Save changes</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
</div>

View file

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

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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">

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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 -->

View 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';
}
})();

View file

@ -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.'

View file

@ -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;
}
};
}

View file

@ -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;

View file

@ -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>
);
}
}

View file

@ -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;

View file

@ -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});
}

View file

@ -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;

View file

@ -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>
);
}
}

View file

@ -50,4 +50,3 @@ angular.module('quay').directive('repoPanelInfo', function () {
};
return directiveDefinitionObject;
});

View file

@ -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};
});
};

View file

@ -38,4 +38,4 @@ export class QuayRequireDirective implements AfterContentInit {
this.$transclude
]);
}
}
}

View file

@ -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>

View file

@ -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++;
}
}
}

View file

@ -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>

View file

@ -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;

View file

@ -44,4 +44,4 @@ export class ChannelIconComponent {
var num: number = parseInt(hash.substr(0, 4));
return this.colors[num % this.colors.length];
}
}
}

View file

@ -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();
});
});
});

View file

@ -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();
}
}
}

View file

@ -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);
});
});
});

View file

@ -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;
};

View file

@ -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;

View file

@ -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;

View file

@ -0,0 +1,5 @@
.cor-table-element .co-top-bar {
display: flex;
justify-content: flex-end;
align-items: baseline;
}

View file

@ -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>

View 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();
});
});
});

View file

@ -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;
};

View file

@ -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);
});
});
});

View file

@ -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);
});
}
}

View file

@ -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);
});
});
});

View file

@ -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);
}
}

View file

@ -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 {
}

View file

@ -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 {}
}

View file

@ -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;
}
}

View file

@ -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);
});
});
});

View file

@ -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);
});
}
}

View file

@ -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>

View file

@ -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);
}
}

View file

@ -0,0 +1,3 @@
<div class="co-main-content-panel co-tab-panel co-fx-box-shadow-heavy">
<div class="co-tab-container" ng-class="$ctrl.isVertical() ? 'vertical': 'horizontal'" ng-transclude></div>
</div>

View file

@ -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);
});
});
});

View file

@ -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';
}
}

View file

@ -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>

View file

@ -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);
}
}

View file

@ -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>

View file

@ -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);
});
});
});

View file

@ -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);
}
}
}

View file

@ -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>

View file

@ -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;
}

View 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 {
}

View 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());
}
}

View file

@ -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) {

View file

@ -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);
});
});
});
});

View file

@ -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;
};

View file

@ -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;
}
}

View file

@ -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 {

View file

@ -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) {

View file

@ -125,7 +125,6 @@ angular.module('quay').directive('fetchTagDialog', function () {
updateFormats();
$element.find('#copyClipboard').clipboardCopy();
$element.find('#fetchTagDialog').modal({});
}
};

View file

@ -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;

View file

@ -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