initial import for Open Source 🎉
This commit is contained in:
parent
1898c361f3
commit
9c0dd3b722
2048 changed files with 218743 additions and 0 deletions
20
static/js/directives/ui/anchor.js
Normal file
20
static/js/directives/ui/anchor.js
Normal file
|
@ -0,0 +1,20 @@
|
|||
/**
|
||||
* An element which displays its contents wrapped in an <a> tag, but only if the href is not null.
|
||||
*/
|
||||
angular.module('quay').directive('anchor', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/anchor.html',
|
||||
replace: false,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'href': '@href',
|
||||
'target': '@target',
|
||||
'isOnlyText': '=isOnlyText'
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
|
@ -0,0 +1,138 @@
|
|||
<div class="app-public-view-element">
|
||||
<div class="co-main-content-panel">
|
||||
<div class="app-row">
|
||||
<!-- Main panel -->
|
||||
<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>
|
||||
<h3><i class="fa ci-appcube"></i>{{ $ctrl.repository.namespace }}/{{ $ctrl.repository.name }}</h3>
|
||||
</div>
|
||||
|
||||
<!-- Tabs -->
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
<!-- 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>
|
||||
</cor-tab-pane>
|
||||
|
||||
<!-- 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']"
|
||||
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>
|
||||
|
||||
<!-- 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 hidden-xs hidden-sm side-bar">
|
||||
<div>
|
||||
<visibility-indicator repository="$ctrl.repository"></visibility-indicator>
|
||||
</div>
|
||||
<div ng-if="$ctrl.repository.is_public">{{ $ctrl.repository.namespace }} is sharing this application publicly</div>
|
||||
<div ng-if="!$ctrl.repository.is_public">This application is private and only visible to those with permission</div>
|
||||
|
||||
<div class="sidebar-table" ng-if="$ctrl.repository.channels.length">
|
||||
<h4>Latest Channels</h4>
|
||||
<cor-table table-data="$ctrl.repository.channels" table-item-title="channels" filter-fields="['name']" compact="true" max-display-count="3">
|
||||
<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 style="word-wrap: break-word;"
|
||||
datafield="release" sortfield="release" title="Current Release"></cor-table-col>
|
||||
</cor-table>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-table" ng-if="$ctrl.repository.releases.length">
|
||||
<h4>Latest Releases</h4>
|
||||
<cor-table table-data="$ctrl.repository.releases" table-item-title="releases" filter-fields="['name']" compact="true" max-display-count="3">
|
||||
<cor-table-col style="word-wrap: break-word;"
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,34 @@
|
|||
import { Input, Component, Inject } from 'ng-metadata/core';
|
||||
|
||||
|
||||
/**
|
||||
* A component that displays the public information associated with an application repository.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'app-public-view',
|
||||
templateUrl: '/static/js/directives/ui/app-public-view/app-public-view.component.html'
|
||||
})
|
||||
export class AppPublicViewComponent {
|
||||
|
||||
@Input('<') public repository: any;
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
<channel-icon name="item.name"></channel-icon><span style="vertical-align: middle; margin-left: 6px;">{{ item.name }}</span>
|
19
static/js/directives/ui/app-public-view/channels-list.html
Normal file
19
static/js/directives/ui/app-public-view/channels-list.html
Normal file
|
@ -0,0 +1,19 @@
|
|||
<div style="display: flex; align-items: center;">
|
||||
<div style="display: flex; flex-wrap: wrap; width: 70%;">
|
||||
<!-- TODO: Move repeat logic to separate component -->
|
||||
<span ng-if="item.channels.length > 0"
|
||||
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>
|
|
@ -0,0 +1 @@
|
|||
<time-ago datetime="item.last_modified"></time-ago>
|
|
@ -0,0 +1,33 @@
|
|||
<div class="resource-view" resource="$ctrl.appTokensResource">
|
||||
<div style="float: right; margin-left: 10px;" ng-show="!$ctrl.inReadOnlyMode">
|
||||
<button class="btn btn-primary" ng-click="$ctrl.askCreateToken()">Create Application Token</button>
|
||||
</div>
|
||||
<cor-table table-data="$ctrl.appTokens" table-item-title="tokens" filter-fields="['title']">
|
||||
<cor-table-col datafield="title" sortfield="title" title="Title" selected="true"
|
||||
bind-model="$ctrl"
|
||||
templateurl="/static/js/directives/ui/app-specific-token-manager/token-title.html"></cor-table-col>
|
||||
<cor-table-col datafield="last_accessed" sortfield="last_accessed" title="Last Accessed"
|
||||
kindof="datetime" templateurl="/static/js/directives/ui/app-specific-token-manager/last-accessed.html"></cor-table-col>
|
||||
<cor-table-col datafield="expiration" sortfield="expiration" title="Expiration"
|
||||
kindof="datetime" templateurl="/static/js/directives/ui/app-specific-token-manager/expiration.html"></cor-table-col>
|
||||
<cor-table-col datafield="created" sortfield="created" title="Created"
|
||||
kindof="datetime" templateurl="/static/js/directives/ui/app-specific-token-manager/created.html"></cor-table-col>
|
||||
<cor-table-col templateurl="/static/js/directives/ui/app-specific-token-manager/cog.html"
|
||||
bind-model="$ctrl" class="options-col" ng-if="!$ctrl.inReadOnlyMode"></cor-table-col>
|
||||
</cor-table>
|
||||
|
||||
<div class="credentials-dialog" credentials="$ctrl.tokenCredentials" secret-title="Application Token" entity-title="application token" entity-icon="fa-key"></div>
|
||||
|
||||
<!-- Revoke token confirm -->
|
||||
<div class="cor-confirm-dialog"
|
||||
dialog-context="$ctrl.revokeTokenInfo"
|
||||
dialog-action="$ctrl.revokeToken(info.token, callback)"
|
||||
dialog-title="Revoke Application Token"
|
||||
dialog-action-title="Revoke Token">
|
||||
<div class="co-alert co-alert-warning" style="margin-bottom: 10px;">
|
||||
Application token "{{ $ctrl.revokeTokenInfo.token.title }}" will be revoked and <strong>all</strong> applications and CLIs making use of the token will no longer operate.
|
||||
</div>
|
||||
|
||||
Proceed with revocation of this token?
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,78 @@
|
|||
import { Input, Component, Inject } from 'ng-metadata/core';
|
||||
import * as bootbox from "bootbox";
|
||||
|
||||
/**
|
||||
* A component that displays and manage all app specific tokens for a user.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'app-specific-token-manager',
|
||||
templateUrl: '/static/js/directives/ui/app-specific-token-manager/app-specific-token-manager.component.html',
|
||||
})
|
||||
export class AppSpecificTokenManagerComponent {
|
||||
private appTokensResource: any;
|
||||
private appTokens: Array<any>;
|
||||
private tokenCredentials: any;
|
||||
private revokeTokenInfo: any;
|
||||
private inReadOnlyMode: boolean;
|
||||
|
||||
constructor(@Inject('ApiService') private ApiService: any, @Inject('UserService') private UserService: any,
|
||||
@Inject('NotificationService') private NotificationService: any,
|
||||
@Inject('StateService') private StateService: any) {
|
||||
this.loadTokens();
|
||||
this.inReadOnlyMode = StateService.inReadOnlyMode();
|
||||
}
|
||||
|
||||
private loadTokens() {
|
||||
this.appTokensResource = this.ApiService.listAppTokensAsResource().get((resp) => {
|
||||
this.appTokens = resp['tokens'];
|
||||
});
|
||||
}
|
||||
|
||||
private askCreateToken() {
|
||||
bootbox.prompt('Please enter a descriptive title for the new application token', (title) => {
|
||||
if (!title) { return; }
|
||||
|
||||
const errorHandler = this.ApiService.errorDisplay('Could not create the application token');
|
||||
this.ApiService.createAppToken({title}).then((resp) => {
|
||||
this.loadTokens();
|
||||
}, errorHandler);
|
||||
});
|
||||
}
|
||||
|
||||
private showRevokeToken(token) {
|
||||
this.revokeTokenInfo = {
|
||||
'token': token,
|
||||
};
|
||||
};
|
||||
|
||||
private revokeToken(token, callback) {
|
||||
const errorHandler = this.ApiService.errorDisplay('Could not revoke application token', callback);
|
||||
const params = {
|
||||
'token_uuid': token['uuid'],
|
||||
};
|
||||
|
||||
this.ApiService.revokeAppToken(null, params).then((resp) => {
|
||||
this.loadTokens();
|
||||
|
||||
// Update the notification service so it hides any banners if we revoked an expiring token.
|
||||
this.NotificationService.update();
|
||||
callback(true);
|
||||
}, errorHandler);
|
||||
}
|
||||
|
||||
private showToken(token) {
|
||||
const errorHandler = this.ApiService.errorDisplay('Could not find application token');
|
||||
const params = {
|
||||
'token_uuid': token['uuid'],
|
||||
};
|
||||
|
||||
this.ApiService.getAppToken(null, params).then((resp) => {
|
||||
this.tokenCredentials = {
|
||||
'title': resp['token']['title'],
|
||||
'namespace': this.UserService.currentUser().username,
|
||||
'username': '$app',
|
||||
'password': resp['token']['token_code'],
|
||||
};
|
||||
}, errorHandler);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
<span class="cor-options-menu">
|
||||
<span class="cor-option" option-click="col.bindModel.showRevokeToken(item)">
|
||||
<i class="fa fa-times"></i> Revoke Token
|
||||
</span>
|
||||
</span>
|
|
@ -0,0 +1 @@
|
|||
<time-ago datetime="item.created"></time-ago>
|
|
@ -0,0 +1 @@
|
|||
<expiration-status-view expiration-date="item.expiration"></expiration-status-view>
|
|
@ -0,0 +1 @@
|
|||
<time-ago datetime="item.last_accessed"></time-ago>
|
|
@ -0,0 +1 @@
|
|||
<a ng-click="col.bindModel.showToken(item)">{{ item.title }}</a>
|
18
static/js/directives/ui/application-info.js
Normal file
18
static/js/directives/ui/application-info.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
/**
|
||||
* An element which shows information about a registered OAuth application.
|
||||
*/
|
||||
angular.module('quay').directive('applicationInfo', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/application-info.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'application': '=application'
|
||||
},
|
||||
controller: function($scope, $element, ApiService) {}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
||||
|
66
static/js/directives/ui/application-manager.js
Normal file
66
static/js/directives/ui/application-manager.js
Normal file
|
@ -0,0 +1,66 @@
|
|||
/**
|
||||
* Element for managing the applications of an organization.
|
||||
*/
|
||||
angular.module('quay').directive('applicationManager', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/application-manager.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'organization': '=organization',
|
||||
'makevisible': '=makevisible'
|
||||
},
|
||||
controller: function($scope, $element, ApiService) {
|
||||
$scope.loading = false;
|
||||
$scope.applications = [];
|
||||
$scope.feedback = null;
|
||||
|
||||
$scope.createApplication = function(appName) {
|
||||
if (!appName) { return; }
|
||||
|
||||
var params = {
|
||||
'orgname': $scope.organization.name
|
||||
};
|
||||
|
||||
var data = {
|
||||
'name': appName
|
||||
};
|
||||
|
||||
ApiService.createOrganizationApplication(data, params).then(function(resp) {
|
||||
$scope.applications.push(resp);
|
||||
|
||||
$scope.feedback = {
|
||||
'kind': 'success',
|
||||
'message': 'Application {application_name} created',
|
||||
'data': {
|
||||
'application_name': appName
|
||||
}
|
||||
};
|
||||
|
||||
}, ApiService.errorDisplay('Cannot create application'));
|
||||
};
|
||||
|
||||
var update = function() {
|
||||
if (!$scope.organization || !$scope.makevisible) { return; }
|
||||
if ($scope.loading) { return; }
|
||||
|
||||
$scope.loading = true;
|
||||
|
||||
var params = {
|
||||
'orgname': $scope.organization.name
|
||||
};
|
||||
|
||||
ApiService.getOrganizationApplications(null, params).then(function(resp) {
|
||||
$scope.loading = false;
|
||||
$scope.applications = resp['applications'] || [];
|
||||
});
|
||||
};
|
||||
|
||||
$scope.$watch('organization', update);
|
||||
$scope.$watch('makevisible', update);
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
36
static/js/directives/ui/application-reference.js
Normal file
36
static/js/directives/ui/application-reference.js
Normal file
|
@ -0,0 +1,36 @@
|
|||
/**
|
||||
* An element which shows information about an OAuth application and provides a clickable link
|
||||
* for displaying a dialog with further information. Unlike application-info, this element is
|
||||
* intended for the *owner* of the application (since it requires the client ID).
|
||||
*/
|
||||
angular.module('quay').directive('applicationReference', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/application-reference.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'title': '=title',
|
||||
'clientId': '=clientId'
|
||||
},
|
||||
controller: function($scope, $element, ApiService, $modal) {
|
||||
$scope.showAppDetails = function() {
|
||||
var params = {
|
||||
'client_id': $scope.clientId
|
||||
};
|
||||
|
||||
ApiService.getApplicationInformation(null, params).then(function(resp) {
|
||||
$scope.applicationInfo = resp;
|
||||
$modal({
|
||||
title: 'Application Information',
|
||||
scope: $scope,
|
||||
template: '/static/directives/application-reference-dialog.html',
|
||||
show: true
|
||||
});
|
||||
}, ApiService.errorDisplay('Application could not be found'));
|
||||
};
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
41
static/js/directives/ui/authorized-apps-manager.js
Normal file
41
static/js/directives/ui/authorized-apps-manager.js
Normal file
|
@ -0,0 +1,41 @@
|
|||
/**
|
||||
* Element for managing the applications authorized by a user.
|
||||
*/
|
||||
angular.module('quay').directive('authorizedAppsManager', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/authorized-apps-manager.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'user': '=user',
|
||||
'isEnabled': '=isEnabled'
|
||||
},
|
||||
controller: function($scope, $element, ApiService) {
|
||||
$scope.$watch('isEnabled', function(enabled) {
|
||||
if (!enabled) { return; }
|
||||
loadAuthedApps();
|
||||
});
|
||||
|
||||
var loadAuthedApps = function() {
|
||||
if ($scope.authorizedAppsResource) { return; }
|
||||
|
||||
$scope.authorizedAppsResource = ApiService.listUserAuthorizationsAsResource().get(function(resp) {
|
||||
$scope.authorizedApps = resp['authorizations'];
|
||||
});
|
||||
};
|
||||
|
||||
$scope.deleteAccess = function(accessTokenInfo) {
|
||||
var params = {
|
||||
'access_token_uuid': accessTokenInfo['uuid']
|
||||
};
|
||||
|
||||
ApiService.deleteUserAuthorization(null, params).then(function(resp) {
|
||||
$scope.authorizedApps.splice($scope.authorizedApps.indexOf(accessTokenInfo), 1);
|
||||
}, ApiService.errorDisplay('Could not revoke authorization'));
|
||||
};
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
65
static/js/directives/ui/avatar.js
Normal file
65
static/js/directives/ui/avatar.js
Normal file
|
@ -0,0 +1,65 @@
|
|||
/**
|
||||
* An element which displays an avatar for the given avatar data.
|
||||
*/
|
||||
angular.module('quay').directive('avatar', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/avatar.html',
|
||||
replace: false,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'data': '=data',
|
||||
'size': '=size'
|
||||
},
|
||||
controller: function($scope, $element, AvatarService, Config, UIService, $timeout) {
|
||||
$scope.AvatarService = AvatarService;
|
||||
$scope.Config = Config;
|
||||
$scope.isLoading = true;
|
||||
$scope.showGravatar = false;
|
||||
$scope.loadGravatar = false;
|
||||
|
||||
$scope.imageCallback = function(result) {
|
||||
$scope.isLoading = false;
|
||||
|
||||
if (!result) {
|
||||
$scope.showGravatar = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine whether the gravatar is blank.
|
||||
var canvas = document.createElement("canvas");
|
||||
canvas.width = 512;
|
||||
canvas.height = 512;
|
||||
|
||||
var ctx = canvas.getContext("2d");
|
||||
ctx.drawImage($element.find('img')[0], 0, 0);
|
||||
|
||||
var blank = document.createElement("canvas");
|
||||
blank.width = 512;
|
||||
blank.height = 512;
|
||||
|
||||
var isBlank = canvas.toDataURL('text/png') == blank.toDataURL('text/png');
|
||||
$scope.showGravatar = !isBlank;
|
||||
};
|
||||
|
||||
$scope.$watch('size', function(size) {
|
||||
size = size * 1 || 16;
|
||||
$scope.fontSize = (size - 4) + 'px';
|
||||
$scope.lineHeight = size + 'px';
|
||||
$scope.imageSize = size;
|
||||
});
|
||||
|
||||
$scope.$watch('data', function(data) {
|
||||
if (!data) { return; }
|
||||
|
||||
$scope.loadGravatar = Config.AVATAR_KIND == 'gravatar' &&
|
||||
(data.kind == 'user' || data.kind == 'org');
|
||||
|
||||
$scope.isLoading = $scope.loadGravatar;
|
||||
$scope.hasGravatar = false;
|
||||
});
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
97
static/js/directives/ui/billing-invoices.js
Normal file
97
static/js/directives/ui/billing-invoices.js
Normal file
|
@ -0,0 +1,97 @@
|
|||
/**
|
||||
* Element for displaying the list of billing invoices for the user or organization.
|
||||
*/
|
||||
angular.module('quay').directive('billingInvoices', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/billing-invoices.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'organization': '=organization',
|
||||
'user': '=user',
|
||||
'makevisible': '=makevisible'
|
||||
},
|
||||
controller: function($scope, $element, $sce, ApiService) {
|
||||
$scope.loading = false;
|
||||
$scope.showCreateField = null;
|
||||
$scope.invoiceFields = [];
|
||||
|
||||
var update = function() {
|
||||
var hasValidUser = !!$scope.user;
|
||||
var hasValidOrg = !!$scope.organization;
|
||||
var isValid = hasValidUser || hasValidOrg;
|
||||
|
||||
if (!$scope.makevisible || !isValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.loading = true;
|
||||
|
||||
ApiService.listInvoices($scope.organization).then(function(resp) {
|
||||
$scope.invoices = resp.invoices;
|
||||
$scope.loading = false;
|
||||
}, function() {
|
||||
$scope.invoices = [];
|
||||
$scope.loading = false;
|
||||
});
|
||||
|
||||
ApiService.listInvoiceFields($scope.organization).then(function(resp) {
|
||||
$scope.invoiceFields = resp.fields || [];
|
||||
}, function() {
|
||||
$scope.invoiceFields = [];
|
||||
});
|
||||
};
|
||||
|
||||
$scope.$watch('organization', update);
|
||||
$scope.$watch('user', update);
|
||||
$scope.$watch('makevisible', update);
|
||||
|
||||
$scope.showCreateField = function() {
|
||||
$scope.createFieldInfo = {
|
||||
'title': '',
|
||||
'value': ''
|
||||
};
|
||||
};
|
||||
|
||||
$scope.askDeleteField = function(field) {
|
||||
bootbox.confirm('Are you sure you want to delete field ' + field.title + '?', function(r) {
|
||||
if (r) {
|
||||
var params = {
|
||||
'field_uuid': field.uuid
|
||||
};
|
||||
|
||||
ApiService.deleteInvoiceField($scope.organization, null, params).then(function(resp) {
|
||||
$scope.invoiceFields = $.grep($scope.invoiceFields, function(current) {
|
||||
return current.uuid != field.uuid
|
||||
});
|
||||
|
||||
}, ApiService.errorDisplay('Could not delete custom field'));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.createCustomField = function(title, value, callback) {
|
||||
var data = {
|
||||
'title': title,
|
||||
'value': value
|
||||
};
|
||||
|
||||
if (!title || !value) {
|
||||
callback(false);
|
||||
bootbox.alert('Missing title or value');
|
||||
return;
|
||||
}
|
||||
|
||||
ApiService.createInvoiceField($scope.organization, data).then(function(resp) {
|
||||
$scope.invoiceFields.push(resp);
|
||||
callback(true);
|
||||
}, ApiService.errorDisplay('Could not create custom field'));
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
return directiveDefinitionObject;
|
||||
});
|
||||
|
144
static/js/directives/ui/billing-management-panel.js
Normal file
144
static/js/directives/ui/billing-management-panel.js
Normal file
|
@ -0,0 +1,144 @@
|
|||
/**
|
||||
* An element which displays the billing options for a user or an organization.
|
||||
*/
|
||||
angular.module('quay').directive('billingManagementPanel', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/billing-management-panel.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'user': '=user',
|
||||
'organization': '=organization',
|
||||
'isEnabled': '=isEnabled',
|
||||
'subscriptionStatus': '=subscriptionStatus'
|
||||
},
|
||||
controller: function($scope, $element, PlanService, ApiService, Features) {
|
||||
$scope.currentCard = null;
|
||||
$scope.subscription = null;
|
||||
$scope.updating = true;
|
||||
$scope.changeReceiptsInfo = null;
|
||||
$scope.context = {};
|
||||
$scope.subscriptionStatus = 'loading';
|
||||
|
||||
var setSubscription = function(sub) {
|
||||
$scope.subscription = sub;
|
||||
|
||||
// Load the plan info.
|
||||
PlanService.getPlanIncludingDeprecated(sub['plan'], function(plan) {
|
||||
$scope.currentPlan = plan;
|
||||
|
||||
if (!sub.hasSubscription) {
|
||||
$scope.updating = false;
|
||||
$scope.subscriptionStatus = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
// Load credit card information.
|
||||
PlanService.getCardInfo($scope.organization ? $scope.organization.name : null, function(card) {
|
||||
$scope.currentCard = card;
|
||||
$scope.subscriptionStatus = 'valid';
|
||||
$scope.updating = false;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
var update = function() {
|
||||
if (!$scope.isEnabled || !($scope.user || $scope.organization) || !Features.BILLING) {
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.entity = $scope.user ? $scope.user : $scope.organization;
|
||||
$scope.invoice_email = $scope.entity.invoice_email;
|
||||
$scope.invoice_email_address = $scope.entity.invoice_email_address || $scope.entity.email;
|
||||
|
||||
$scope.updating = true;
|
||||
|
||||
// Load plan information.
|
||||
PlanService.getSubscription($scope.organization, setSubscription, function() {
|
||||
setSubscription({ 'plan': PlanService.getFreePlan() });
|
||||
});
|
||||
};
|
||||
|
||||
// Listen to plan changes.
|
||||
PlanService.registerListener(this, function(plan) {
|
||||
if (plan && plan.price > 0) {
|
||||
update();
|
||||
}
|
||||
});
|
||||
|
||||
$scope.$on('$destroy', function() {
|
||||
PlanService.unregisterListener(this);
|
||||
});
|
||||
|
||||
$scope.$watch('isEnabled', update);
|
||||
$scope.$watch('organization', update);
|
||||
$scope.$watch('user', update);
|
||||
|
||||
$scope.getEntityPrefix = function() {
|
||||
if ($scope.organization) {
|
||||
return '/organization/' + $scope.organization.name;
|
||||
} else {
|
||||
return '/user/' + $scope.user.username;
|
||||
}
|
||||
};
|
||||
|
||||
$scope.changeCreditCard = function() {
|
||||
var callbacks = {
|
||||
'opened': function() { },
|
||||
'closed': function() { },
|
||||
'started': function() { },
|
||||
'success': function(resp) {
|
||||
$scope.currentCard = resp.card;
|
||||
update();
|
||||
},
|
||||
'failure': function(resp) {
|
||||
if (!PlanService.isCardError(resp)) {
|
||||
bootbox.alert('Could not change credit card. Please try again later.');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
PlanService.changeCreditCard($scope, $scope.organization ? $scope.organization.name : null, callbacks);
|
||||
};
|
||||
|
||||
$scope.getCreditImage = function(creditInfo) {
|
||||
if (!creditInfo || !creditInfo.type) { return 'credit.png'; }
|
||||
|
||||
var kind = creditInfo.type.toLowerCase() || 'credit';
|
||||
var supported = {
|
||||
'american express': 'amex',
|
||||
'credit': 'credit',
|
||||
'diners club': 'diners',
|
||||
'discover': 'discover',
|
||||
'jcb': 'jcb',
|
||||
'mastercard': 'mastercard',
|
||||
'visa': 'visa'
|
||||
};
|
||||
|
||||
kind = supported[kind] || 'credit';
|
||||
return kind + '.png';
|
||||
};
|
||||
|
||||
$scope.changeReceipts = function(info, callback) {
|
||||
$scope.entity['invoice_email'] = info['sendOption'] || false;
|
||||
$scope.entity['invoice_email_address'] = info['address'] || $scope.entity.email;
|
||||
|
||||
var errorHandler = ApiService.errorDisplay('Could not change billing options', callback);
|
||||
ApiService.changeDetails($scope.organization, $scope.entity).then(function(resp) {
|
||||
callback(true);
|
||||
update();
|
||||
}, errorHandler);
|
||||
};
|
||||
|
||||
$scope.showChangeReceipts = function() {
|
||||
$scope.changeReceiptsInfo = {
|
||||
'sendOption': $scope.invoice_email,
|
||||
'address': $scope.invoice_email_address
|
||||
};
|
||||
};
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
20
static/js/directives/ui/build-info-bar.js
Normal file
20
static/js/directives/ui/build-info-bar.js
Normal file
|
@ -0,0 +1,20 @@
|
|||
/**
|
||||
* An element which displays the status of a build in a nice compact bar.
|
||||
*/
|
||||
angular.module('quay').directive('buildInfoBar', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/build-info-bar.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'build': '=build',
|
||||
'showTime': '=showTime',
|
||||
'hideId': '=hideId'
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
49
static/js/directives/ui/build-log-command.js
Normal file
49
static/js/directives/ui/build-log-command.js
Normal file
|
@ -0,0 +1,49 @@
|
|||
/**
|
||||
* An element which displays a command in a build.
|
||||
*/
|
||||
angular.module('quay').directive('buildLogCommand', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/build-log-command.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'command': '<command'
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
$scope.getWithoutStep = function(fullTitle) {
|
||||
var colon = fullTitle.indexOf(':');
|
||||
if (colon <= 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $.trim(fullTitle.substring(colon + 1));
|
||||
};
|
||||
|
||||
$scope.isSecondaryFrom = function(fullTitle) {
|
||||
if (!fullTitle) { return false; }
|
||||
|
||||
var command = $scope.getWithoutStep(fullTitle);
|
||||
return command.indexOf('FROM ') == 0 && fullTitle.indexOf('Step 1 ') < 0;
|
||||
};
|
||||
|
||||
$scope.fromName = function(fullTitle) {
|
||||
var command = $scope.getWithoutStep(fullTitle);
|
||||
if (command.indexOf('FROM ') != 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var parts = command.split(' ');
|
||||
for (var i = 0; i < parts.length - 1; i++) {
|
||||
var part = parts[i];
|
||||
if ($.trim(part) == 'as') {
|
||||
return parts[i + 1];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
71
static/js/directives/ui/build-log-error.js
Normal file
71
static/js/directives/ui/build-log-error.js
Normal file
|
@ -0,0 +1,71 @@
|
|||
/**
|
||||
* An element which displays a build error in a nice format.
|
||||
*/
|
||||
angular.module('quay').directive('buildLogError', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/build-log-error.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'error': '=error',
|
||||
'entries': '=entries',
|
||||
'isSuperuser': '<isSuperuser'
|
||||
},
|
||||
controller: function($scope, $element, Config) {
|
||||
$scope.localPullInfo = null;
|
||||
|
||||
var calculateLocalPullInfo = function(entries) {
|
||||
var localInfo = {
|
||||
'isLocal': false
|
||||
};
|
||||
|
||||
// Find the 'pulling' phase entry, and then extra any metadata found under
|
||||
// it.
|
||||
for (var i = 0; i < $scope.entries.length; ++i) {
|
||||
var entry = $scope.entries[i];
|
||||
if (entry.type == 'phase' && entry.message == 'pulling') {
|
||||
var entryData = entry.data || {};
|
||||
if (entryData.base_image) {
|
||||
localInfo['isLocal'] = true || entryData['base_image'].indexOf(Config.SERVER_HOSTNAME + '/') == 0;
|
||||
localInfo['pullUsername'] = entryData['pull_username'];
|
||||
localInfo['repo'] = entryData['base_image'].substring(Config.SERVER_HOSTNAME.length);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$scope.localPullInfo = localInfo;
|
||||
};
|
||||
|
||||
calculateLocalPullInfo($scope.entries);
|
||||
|
||||
$scope.getInternalError = function(entries) {
|
||||
var entry = entries[entries.length - 1];
|
||||
if (entry && entry.data && entry.data['internal_error']) {
|
||||
return entry.data['internal_error'];
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
$scope.getBaseError = function(error) {
|
||||
if (!error || !error.data || !error.data.base_error) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return error.data.base_error;
|
||||
};
|
||||
|
||||
$scope.isPullError = function(error) {
|
||||
if (!error || !error.data || !error.data.base_error) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return error.data.base_error.indexOf('Error: Status 403 trying to pull repository ') == 0;
|
||||
};
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
18
static/js/directives/ui/build-log-phase.js
Normal file
18
static/js/directives/ui/build-log-phase.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
/**
|
||||
* An element which displays the phase of a build nicely.
|
||||
*/
|
||||
angular.module('quay').directive('buildLogPhase', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/build-log-phase.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'phase': '=phase'
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
215
static/js/directives/ui/build-logs-view.js
Normal file
215
static/js/directives/ui/build-logs-view.js
Normal file
|
@ -0,0 +1,215 @@
|
|||
/**
|
||||
* An element which displays and auto-updates the logs from a build.
|
||||
*/
|
||||
angular.module('quay').directive('buildLogsView', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/build-logs-view.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'build': '=build',
|
||||
'useTimestamps': '=useTimestamps',
|
||||
'buildUpdated': '&buildUpdated',
|
||||
'isSuperUser': '=isSuperUser'
|
||||
},
|
||||
controller: function($scope, $element, $interval, $sanitize, ansi2html, ViewArray,
|
||||
AngularPollChannel, ApiService, Restangular, UtilService) {
|
||||
|
||||
var repoStatusApiCall = ApiService.getRepoBuildStatus;
|
||||
var repoLogApiCall = ApiService.getRepoBuildLogsAsResource;
|
||||
if( $scope.isSuperUser ){
|
||||
repoStatusApiCall = ApiService.getRepoBuildStatusSuperUser;
|
||||
repoLogApiCall = ApiService.getRepoBuildLogsSuperUserAsResource;
|
||||
}
|
||||
|
||||
$scope.logEntries = null;
|
||||
$scope.currentParentEntry = null;
|
||||
$scope.logStartIndex = 0;
|
||||
$scope.buildLogsText = '';
|
||||
$scope.currentBuild = null;
|
||||
$scope.loadError = null;
|
||||
|
||||
$scope.pollChannel = null;
|
||||
|
||||
var appendToTextLog = function(type, message) {
|
||||
if (type == 'phase') {
|
||||
text = 'Starting phase: ' + message + '\n';
|
||||
} else {
|
||||
text = message + '\n';
|
||||
}
|
||||
|
||||
$scope.buildLogsText += text.replace(new RegExp("\\033\\[[^m]+m"), '');
|
||||
};
|
||||
|
||||
var processLogs = function(logs, startIndex, endIndex) {
|
||||
if (!$scope.logEntries) { $scope.logEntries = []; }
|
||||
|
||||
// If the start index given is less than that requested, then we've received a larger
|
||||
// pool of logs, and we need to only consider the new ones.
|
||||
if (startIndex < $scope.logStartIndex) {
|
||||
logs = logs.slice($scope.logStartIndex - startIndex);
|
||||
}
|
||||
|
||||
for (var i = 0; i < logs.length; ++i) {
|
||||
var entry = logs[i];
|
||||
var type = entry['type'] || 'entry';
|
||||
if (type == 'command' || type == 'phase' || type == 'error') {
|
||||
entry['logs'] = ViewArray.create();
|
||||
entry['index'] = $scope.logStartIndex + i;
|
||||
|
||||
$scope.logEntries.push(entry);
|
||||
$scope.currentParentEntry = entry;
|
||||
} else if ($scope.currentParentEntry) {
|
||||
$scope.currentParentEntry['logs'].push(entry);
|
||||
}
|
||||
|
||||
appendToTextLog(type, entry['message']);
|
||||
}
|
||||
|
||||
return endIndex;
|
||||
};
|
||||
|
||||
var handleLogsData = function(logsData, callback) {
|
||||
// Process the logs we've received.
|
||||
$scope.logStartIndex = processLogs(logsData['logs'], logsData['start'], logsData['total']);
|
||||
|
||||
// If the build status is an error, automatically open the last command run.
|
||||
var currentBuild = $scope.currentBuild;
|
||||
if (currentBuild['phase'] == 'error') {
|
||||
for (var i = $scope.logEntries.length - 1; i >= 0; i--) {
|
||||
var currentEntry = $scope.logEntries[i];
|
||||
if (currentEntry['type'] == 'command') {
|
||||
currentEntry['logs'].setVisible(true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If the build phase is an error or a complete, then we mark the channel
|
||||
// as closed.
|
||||
callback(currentBuild['phase'] != 'error' && currentBuild['phase'] != 'complete');
|
||||
}
|
||||
|
||||
var getBuildStatusAndLogs = function(build, callback) {
|
||||
var params = {
|
||||
'repository': build.repository.namespace + '/' + build.repository.name,
|
||||
'build_uuid': build.id
|
||||
};
|
||||
|
||||
|
||||
repoStatusApiCall(null, params, true).then(function(resp) {
|
||||
if (resp.id != $scope.build.id) { callback(false); return; }
|
||||
|
||||
// Call the build updated handler.
|
||||
$scope.buildUpdated({'build': resp});
|
||||
|
||||
// Save the current build.
|
||||
$scope.currentBuild = resp;
|
||||
|
||||
// Load the updated logs for the build.
|
||||
var options = {
|
||||
'start': $scope.logStartIndex
|
||||
};
|
||||
|
||||
repoLogApiCall(params, true).withOptions(options).get(function(resp) {
|
||||
// If we get a logs url back, then we need to make another XHR request to retrieve the
|
||||
// data.
|
||||
var logsUrl = resp['logs_url'];
|
||||
if (logsUrl) {
|
||||
$.ajax({
|
||||
url: logsUrl,
|
||||
}).done(function(r) {
|
||||
$scope.$apply(function() {
|
||||
handleLogsData(r, callback);
|
||||
});
|
||||
}).error(function(xhr) {
|
||||
$scope.$apply(function() {
|
||||
if (xhr.status == 0) {
|
||||
UtilService.isAdBlockEnabled(function(result) {
|
||||
$scope.loadError = result ? 'blocked': 'request-failed';
|
||||
});
|
||||
} else {
|
||||
$scope.loadError = 'request-failed';
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
handleLogsData(resp, callback);
|
||||
}, function(resp) {
|
||||
if (resp.status == 403) {
|
||||
$scope.loadError = 'unauthorized';
|
||||
} else {
|
||||
$scope.loadError = 'request-failed';
|
||||
}
|
||||
callback(false);
|
||||
});
|
||||
}, function() {
|
||||
$scope.loadError = 'request-failed';
|
||||
callback(false);
|
||||
});
|
||||
};
|
||||
|
||||
var startWatching = function(build) {
|
||||
// Create a new channel for polling the build status and logs.
|
||||
var conductStatusAndLogRequest = function(callback) {
|
||||
getBuildStatusAndLogs(build, callback);
|
||||
};
|
||||
|
||||
// Make sure to cancel any existing watchers first.
|
||||
stopWatching();
|
||||
|
||||
// Register a new poll channel to start watching.
|
||||
$scope.pollChannel = AngularPollChannel.create($scope, conductStatusAndLogRequest, 5 * 1000 /* 5s */);
|
||||
$scope.pollChannel.start();
|
||||
};
|
||||
|
||||
var stopWatching = function() {
|
||||
if ($scope.pollChannel) {
|
||||
$scope.pollChannel.stop();
|
||||
$scope.pollChannel = null;
|
||||
}
|
||||
};
|
||||
|
||||
$scope.$watch('useTimestamps', function() {
|
||||
if (!$scope.logEntries) { return; }
|
||||
$scope.logEntries = $scope.logEntries.slice();
|
||||
});
|
||||
|
||||
$scope.$watch('build', function(build) {
|
||||
if (build) {
|
||||
startWatching(build);
|
||||
} else {
|
||||
stopWatching();
|
||||
}
|
||||
});
|
||||
|
||||
$scope.hasLogs = function(container) {
|
||||
return container.logs.hasEntries;
|
||||
};
|
||||
|
||||
$scope.formatDatetime = function(datetimeString) {
|
||||
// Note: The standard format required by the Date constructor in JS is
|
||||
// "2011-10-10T14:48:00" for date-times. The date-time string we get is exactly that,
|
||||
// but with a space instead of a 'T', so we just replace it.
|
||||
var dt = new Date(datetimeString.replace(' ', 'T'));
|
||||
return dt.toLocaleString();
|
||||
}
|
||||
|
||||
$scope.processANSI = function(message, container) {
|
||||
var filter = container.logs._filter = (container.logs._filter || ansi2html.create());
|
||||
|
||||
// Note: order is important here.
|
||||
var setup = filter.getSetupHtml();
|
||||
var stream = filter.addInputToStream(message || '');
|
||||
var teardown = filter.getTeardownHtml();
|
||||
return setup + stream + teardown;
|
||||
};
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
21
static/js/directives/ui/build-message.js
Normal file
21
static/js/directives/ui/build-message.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
/**
|
||||
* An element which displays a user-friendly message for the current phase of a build.
|
||||
*/
|
||||
angular.module('quay').directive('buildMessage', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/build-message.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'phase': '=phase'
|
||||
},
|
||||
controller: function($scope, $element, BuildService) {
|
||||
$scope.getBuildMessage = function (phase) {
|
||||
return BuildService.getBuildMessage(phase);
|
||||
};
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
23
static/js/directives/ui/build-mini-status.js
Normal file
23
static/js/directives/ui/build-mini-status.js
Normal file
|
@ -0,0 +1,23 @@
|
|||
/**
|
||||
* An element which displays the status of a build as a mini-bar.
|
||||
*/
|
||||
angular.module('quay').directive('buildMiniStatus', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/build-mini-status.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'build': '=build',
|
||||
'canView': '=canView'
|
||||
},
|
||||
controller: function($scope, $element, BuildService) {
|
||||
$scope.isBuilding = function(build) {
|
||||
if (!build) { return true; }
|
||||
return BuildService.isActive(build)
|
||||
};
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
53
static/js/directives/ui/build-progress.js
Normal file
53
static/js/directives/ui/build-progress.js
Normal file
|
@ -0,0 +1,53 @@
|
|||
/**
|
||||
* An element which displays a progressbar for the given build.
|
||||
*/
|
||||
angular.module('quay').directive('buildProgress', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/build-progress.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'build': '=build'
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
$scope.getPercentage = function(buildInfo) {
|
||||
switch (buildInfo.phase) {
|
||||
case 'pulling':
|
||||
return buildInfo.status.pull_completion * 100;
|
||||
break;
|
||||
|
||||
case 'building':
|
||||
return (buildInfo.status.current_command / buildInfo.status.total_commands) * 100;
|
||||
break;
|
||||
|
||||
case 'pushing':
|
||||
return buildInfo.status.push_completion * 100;
|
||||
break;
|
||||
|
||||
case 'priming-cache':
|
||||
return buildInfo.status.cache_completion * 100;
|
||||
break;
|
||||
|
||||
case 'complete':
|
||||
return 100;
|
||||
break;
|
||||
|
||||
case 'initializing':
|
||||
case 'checking-cache':
|
||||
case 'starting':
|
||||
case 'waiting':
|
||||
case 'cannot_load':
|
||||
case 'unpacking':
|
||||
return 0;
|
||||
break;
|
||||
}
|
||||
|
||||
return -1;
|
||||
};
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
||||
|
22
static/js/directives/ui/build-state-icon.js
Normal file
22
static/js/directives/ui/build-state-icon.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
/**
|
||||
* An element which displays an icon representing the state of the build.
|
||||
*/
|
||||
angular.module('quay').directive('buildStateIcon', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/build-state-icon.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'build': '=build'
|
||||
},
|
||||
controller: function($scope, $element, BuildService) {
|
||||
$scope.isBuilding = function(build) {
|
||||
if (!build) { return true; }
|
||||
return BuildService.isActive(build);
|
||||
};
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
18
static/js/directives/ui/build-status.js
Normal file
18
static/js/directives/ui/build-status.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
/**
|
||||
* DEPRECATED: An element which displays the status of a build.
|
||||
*/
|
||||
angular.module('quay').directive('buildStatus', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/build-status.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'build': '=build'
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
|
@ -0,0 +1,7 @@
|
|||
<span class="channel-icon-element" data-title="{{ $ctrl.name }}" bs-tooltip>
|
||||
<span class="hexagon" ng-style="{'background-color': $ctrl.color($ctrl.name)}">
|
||||
<span class="before" ng-style="{'border-bottom-color': $ctrl.color($ctrl.name)}"></span>
|
||||
<span class="after" ng-style="{'border-top-color': $ctrl.color($ctrl.name)}"></span>
|
||||
</span>
|
||||
<b>{{ $ctrl.initial($ctrl.name) }}</b>
|
||||
</span>
|
|
@ -0,0 +1,47 @@
|
|||
import { Input, Component, Inject } from 'ng-metadata/core';
|
||||
|
||||
|
||||
/**
|
||||
* A component that displays the icon of a channel.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'channel-icon',
|
||||
templateUrl: '/static/js/directives/ui/channel-icon/channel-icon.component.html',
|
||||
})
|
||||
export class ChannelIconComponent {
|
||||
@Input('<') public name: string;
|
||||
private colors: any;
|
||||
|
||||
constructor(@Inject('Config') Config: any, @Inject('md5') private md5: any) {
|
||||
this.colors = Config['CHANNEL_COLORS'];
|
||||
}
|
||||
|
||||
private initial(name: string): string {
|
||||
if (name == 'alpha') {
|
||||
return 'α';
|
||||
}
|
||||
if (name == 'beta') {
|
||||
return 'β';
|
||||
}
|
||||
if (name == 'stable') {
|
||||
return 'S';
|
||||
}
|
||||
return name[0].toUpperCase();
|
||||
}
|
||||
|
||||
private color(name: string): string {
|
||||
if (name == 'alpha') {
|
||||
return this.colors[0];
|
||||
}
|
||||
if (name == 'beta') {
|
||||
return this.colors[1];
|
||||
}
|
||||
if (name == 'stable') {
|
||||
return this.colors[2];
|
||||
}
|
||||
|
||||
var hash: string = this.md5.createHash(name);
|
||||
var num: number = parseInt(hash.substr(0, 4));
|
||||
return this.colors[num % this.colors.length];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
import { ClipboardCopyDirective } from './clipboard-copy.directive';
|
||||
import * as Clipboard from 'clipboard';
|
||||
import { Mock } from 'ts-mocks';
|
||||
import Spy = jasmine.Spy;
|
||||
|
||||
|
||||
describe("ClipboardCopyDirective", () => {
|
||||
var directive: ClipboardCopyDirective;
|
||||
var $elementMock: any;
|
||||
var $timeoutMock: any;
|
||||
var $documentMock: any;
|
||||
var clipboardFactory: any;
|
||||
var clipboardMock: Mock<Clipboard>;
|
||||
|
||||
beforeEach(() => {
|
||||
$elementMock = new Mock<ng.IAugmentedJQuery>();
|
||||
$timeoutMock = jasmine.createSpy('$timeoutSpy').and.callFake((fn: () => void, delay) => fn());
|
||||
$documentMock = new Mock<ng.IDocumentService>();
|
||||
clipboardMock = new Mock<Clipboard>();
|
||||
clipboardMock.setup(mock => mock.on).is((eventName: string, callback: (event) => void) => {});
|
||||
clipboardFactory = jasmine.createSpy('clipboardFactory').and.returnValue(clipboardMock.Object);
|
||||
directive = new ClipboardCopyDirective(<any>[$elementMock.Object],
|
||||
$timeoutMock,
|
||||
<any>[$documentMock.Object],
|
||||
clipboardFactory);
|
||||
directive.copyTargetSelector = "#copy-input-box-0";
|
||||
});
|
||||
|
||||
describe("ngAfterContentInit", () => {
|
||||
|
||||
it("initializes new Clipboard instance", () => {
|
||||
const target = new Mock<ng.IAugmentedJQuery>();
|
||||
$documentMock.setup(mock => mock.querySelector).is(selector => target.Object);
|
||||
directive.ngAfterContentInit();
|
||||
|
||||
expect(clipboardFactory).toHaveBeenCalled();
|
||||
expect((<Spy>clipboardFactory.calls.argsFor(0)[0])).toEqual($elementMock.Object);
|
||||
expect((<Spy>clipboardFactory.calls.argsFor(0)[1]['target']())).toEqual(target.Object);
|
||||
});
|
||||
|
||||
it("sets error callback for Clipboard instance", () => {
|
||||
directive.ngAfterContentInit();
|
||||
|
||||
expect((<Spy>clipboardMock.Object.on.calls.argsFor(0)[0])).toEqual('error');
|
||||
expect((<Spy>clipboardMock.Object.on.calls.argsFor(0)[1])).toBeDefined();
|
||||
});
|
||||
|
||||
it("sets success callback for Clipboard instance", (done) => {
|
||||
directive.ngAfterContentInit();
|
||||
|
||||
expect((<Spy>clipboardMock.Object.on.calls.argsFor(1)[0])).toEqual('success');
|
||||
expect((<Spy>clipboardMock.Object.on.calls.argsFor(1)[1])).toBeDefined();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe("ngOnDestroy", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
clipboardMock.setup(mock => mock.destroy).is(() => null);
|
||||
});
|
||||
|
||||
it("calls method to destroy Clipboard instance if set", (done) => {
|
||||
directive.ngAfterContentInit();
|
||||
directive.ngOnDestroy();
|
||||
|
||||
expect((<Spy>clipboardMock.Object.destroy)).toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
|
||||
it("does not call method to destroy Clipboard instance if not set", () => {
|
||||
directive.ngOnDestroy();
|
||||
|
||||
expect((<Spy>clipboardMock.Object.destroy)).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,63 @@
|
|||
import { Directive, Inject, Input, AfterContentInit, OnDestroy } from 'ng-metadata/core';
|
||||
import * as Clipboard from 'clipboard';
|
||||
|
||||
|
||||
@Directive({
|
||||
selector: '[clipboardCopy]'
|
||||
})
|
||||
export class ClipboardCopyDirective implements AfterContentInit, OnDestroy {
|
||||
|
||||
@Input('@clipboardCopy') public copyTargetSelector: string;
|
||||
|
||||
private clipboard: Clipboard;
|
||||
|
||||
constructor(@Inject('$element') private $element: ng.IAugmentedJQuery,
|
||||
@Inject('$timeout') private $timeout: ng.ITimeoutService,
|
||||
@Inject('$document') private $document: ng.IDocumentService,
|
||||
@Inject('clipboardFactory') private clipboardFactory: (elem, options) => Clipboard) {
|
||||
|
||||
}
|
||||
|
||||
public ngAfterContentInit(): void {
|
||||
// FIXME: Need to wait for DOM to render to find target element
|
||||
this.$timeout(() => {
|
||||
this.clipboard = this.clipboardFactory(this.$element[0], {target: (trigger) => {
|
||||
return this.$document[0].querySelector(this.copyTargetSelector);
|
||||
}});
|
||||
|
||||
this.clipboard.on("error", (e) => {
|
||||
console.error(e);
|
||||
});
|
||||
|
||||
this.clipboard.on('success', (e) => {
|
||||
const container = e.trigger.parentNode.parentNode.parentNode;
|
||||
const messageElem = container.querySelector('.clipboard-copied-message');
|
||||
if (!messageElem) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Resets the animation.
|
||||
var elem = messageElem;
|
||||
elem.style.display = 'none';
|
||||
elem.classList.remove('animated');
|
||||
|
||||
// Show the notification.
|
||||
setTimeout(() => {
|
||||
elem.style.display = 'inline-block';
|
||||
elem.classList.add('animated');
|
||||
}, 10);
|
||||
|
||||
// Reset the notification.
|
||||
setTimeout(() => {
|
||||
elem.style.display = 'none';
|
||||
}, 5000);
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
|
||||
public ngOnDestroy(): void {
|
||||
if (this.clipboard) {
|
||||
this.clipboard.destroy();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
<div class="context-path-select-element">
|
||||
<div class="dropdown-select" placeholder="'Enter a docker context'"
|
||||
selected-item="$ctrl.selectedContext"
|
||||
lookahead-items="$ctrl.contexts"
|
||||
handle-input="$ctrl.setContext(input)"
|
||||
handle-item-selected="$ctrl.setSelectedContext(datum.value)"
|
||||
allow-custom-input="true"
|
||||
hide-dropdown="$ctrl.contexts.length <= 0">
|
||||
<!-- Icons -->
|
||||
<i class="dropdown-select-icon none-icon fa fa-folder-o fa-lg"
|
||||
ng-show="$ctrl.isUnknownContext"></i>
|
||||
<i class="dropdown-select-icon none-icon fa fa-folder fa-lg" style="color: black;"
|
||||
ng-show="!$ctrl.isUnknownContext"></i>
|
||||
<i class="dropdown-select-icon fa fa-folder fa-lg"></i>
|
||||
|
||||
<!-- Dropdown menu -->
|
||||
<ul class="dropdown-select-menu pull-right" role="menu">
|
||||
<li ng-repeat="context in $ctrl.contexts">
|
||||
<a ng-click="$ctrl.setSelectedContext(context)"
|
||||
ng-if="context">
|
||||
<i class="fa fa-folder fa-lg"></i> {{ context }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div style="padding: 10px">
|
||||
<div class="co-alert co-alert-danger"
|
||||
ng-show="!$ctrl.isValidContext && $ctrl.currentContext">
|
||||
Path is an invalid context.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,107 @@
|
|||
import { ContextPathSelectComponent, ContextChangeEvent } from './context-path-select.component';
|
||||
|
||||
|
||||
describe("ContextPathSelectComponent", () => {
|
||||
var component: ContextPathSelectComponent;
|
||||
var currentContext: string;
|
||||
var isValidContext: boolean;
|
||||
var contexts: string[];
|
||||
|
||||
beforeEach(() => {
|
||||
component = new ContextPathSelectComponent();
|
||||
currentContext = '/';
|
||||
isValidContext = false;
|
||||
contexts = ['/'];
|
||||
component.currentContext = currentContext;
|
||||
component.isValidContext = isValidContext;
|
||||
component.contexts = contexts;
|
||||
});
|
||||
|
||||
describe("ngOnChanges", () => {
|
||||
|
||||
it("sets valid context flag to true if current context is valid", () => {
|
||||
component.ngOnChanges({});
|
||||
|
||||
expect(component.isValidContext).toBe(true);
|
||||
});
|
||||
|
||||
it("sets valid context flag to false if current context is invalid", () => {
|
||||
component.currentContext = "asdfdsf";
|
||||
component.ngOnChanges({});
|
||||
|
||||
expect(component.isValidContext).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("setContext", () => {
|
||||
var newContext: string;
|
||||
|
||||
beforeEach(() => {
|
||||
newContext = '/conf';
|
||||
});
|
||||
|
||||
it("sets current context to given context", () => {
|
||||
component.setContext(newContext);
|
||||
|
||||
expect(component.currentContext).toEqual(newContext);
|
||||
});
|
||||
|
||||
it("sets valid context flag to true if given context is valid", () => {
|
||||
component.setContext(newContext);
|
||||
|
||||
expect(component.isValidContext).toBe(true);
|
||||
});
|
||||
|
||||
it("sets valid context flag to false if given context is invalid", () => {
|
||||
component.setContext("asdfsadfs");
|
||||
|
||||
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 newContext: string;
|
||||
|
||||
beforeEach(() => {
|
||||
newContext = '/conf';
|
||||
});
|
||||
|
||||
it("sets current context to given context", () => {
|
||||
component.setSelectedContext(newContext);
|
||||
|
||||
expect(component.currentContext).toEqual(newContext);
|
||||
});
|
||||
|
||||
it("sets valid context flag to true if given context is valid", () => {
|
||||
component.setSelectedContext(newContext);
|
||||
|
||||
expect(component.isValidContext).toBe(true);
|
||||
});
|
||||
|
||||
it("sets valid context flag to false if given context is invalid", () => {
|
||||
component.setSelectedContext("a;lskjdf;ldsa");
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,59 @@
|
|||
import { Input, Component, OnChanges, SimpleChanges, Output, EventEmitter } from 'ng-metadata/core';
|
||||
|
||||
|
||||
/**
|
||||
* A component that allows the user to select the location of the Context in their source code repository.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'context-path-select',
|
||||
templateUrl: '/static/js/directives/ui/context-path-select/context-path-select.component.html'
|
||||
})
|
||||
export class ContextPathSelectComponent implements OnChanges {
|
||||
|
||||
@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;
|
||||
|
||||
public ngOnChanges(changes: SimpleChanges): void {
|
||||
this.isValidContext = this.checkContext(this.currentContext, this.contexts);
|
||||
}
|
||||
|
||||
public setContext(context: string): void {
|
||||
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 {
|
||||
this.isUnknownContext = false;
|
||||
var isValidContext: boolean = false;
|
||||
|
||||
if (context.length > 0 && context[0] === '/') {
|
||||
isValidContext = true;
|
||||
this.isUnknownContext = contexts.indexOf(context) != -1;
|
||||
}
|
||||
return isValidContext;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Build context changed event.
|
||||
*/
|
||||
export type ContextChangeEvent = {
|
||||
contextDir: string;
|
||||
isValid: boolean;
|
||||
};
|
74
static/js/directives/ui/convert-user-to-org.js
Normal file
74
static/js/directives/ui/convert-user-to-org.js
Normal file
|
@ -0,0 +1,74 @@
|
|||
/**
|
||||
* Displays a panel for converting the current user to an organization.
|
||||
*/
|
||||
angular.module('quay').directive('convertUserToOrg', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/convert-user-to-org.html',
|
||||
replace: false,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'info': '=info'
|
||||
},
|
||||
controller: function($scope, $element, $location, Features, PlanService, Config, ApiService, CookieService, UserService) {
|
||||
$scope.convertStep = 0;
|
||||
$scope.org = {};
|
||||
$scope.loading = false;
|
||||
$scope.user = null;
|
||||
$scope.Features = Features;
|
||||
|
||||
$scope.$watch('info', function(info) {
|
||||
if (info && info.user) {
|
||||
$scope.user = info.user;
|
||||
$scope.accountType = 'user';
|
||||
$scope.convertStep = 0;
|
||||
$('#convertAccountModal').modal({});
|
||||
}
|
||||
});
|
||||
|
||||
$scope.showConvertForm = function() {
|
||||
$scope.convertStep = 1;
|
||||
};
|
||||
|
||||
$scope.nextStep = function() {
|
||||
if (Features.BILLING) {
|
||||
PlanService.getMatchingBusinessPlan(function(plan) {
|
||||
$scope.org.plan = plan;
|
||||
});
|
||||
|
||||
PlanService.getPlans(function(plans) {
|
||||
$scope.orgPlans = plans;
|
||||
});
|
||||
|
||||
$scope.convertStep = 2;
|
||||
} else {
|
||||
$scope.performConversion();
|
||||
}
|
||||
};
|
||||
|
||||
$scope.performConversion = function() {
|
||||
if (Config.AUTHENTICATION_TYPE != 'Database') { return; }
|
||||
$scope.convertStep = 3;
|
||||
|
||||
var errorHandler = ApiService.errorDisplay(function() {
|
||||
$('#convertAccountModal').modal('hide');
|
||||
});
|
||||
|
||||
var data = {
|
||||
'adminUser': $scope.org.adminUser,
|
||||
'adminPassword': $scope.org.adminPassword,
|
||||
'plan': $scope.org.plan ? $scope.org.plan.stripeId : ''
|
||||
};
|
||||
|
||||
ApiService.convertUserToOrganization(data).then(function(resp) {
|
||||
CookieService.putPermanent('quay.namespace', $scope.user.username);
|
||||
UserService.load();
|
||||
$('#convertAccountModal').modal('hide');
|
||||
$location.path('/');
|
||||
}, errorHandler);
|
||||
};
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
23
static/js/directives/ui/copy-box.js
Normal file
23
static/js/directives/ui/copy-box.js
Normal file
|
@ -0,0 +1,23 @@
|
|||
/**
|
||||
* An element which displays a textfield with a "Copy to Clipboard" icon next to it.
|
||||
*/
|
||||
angular.module('quay').directive('copyBox', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/copy-box.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'value': '=value',
|
||||
},
|
||||
controller: function($scope, $element, $rootScope) {
|
||||
$scope.disabled = false;
|
||||
|
||||
var number = $rootScope.__copyBoxIdCounter || 0;
|
||||
$rootScope.__copyBoxIdCounter = number + 1;
|
||||
$scope.inputId = "copy-box-input-" + number;
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
45
static/js/directives/ui/cor-table/cor-table-col.component.ts
Normal file
45
static/js/directives/ui/cor-table/cor-table-col.component.ts
Normal file
|
@ -0,0 +1,45 @@
|
|||
import { Input, Component, OnInit, Inject, Host } from 'ng-metadata/core';
|
||||
import { CorTableComponent } from './cor-table.component';
|
||||
|
||||
|
||||
/**
|
||||
* Defines a column (optionally sortable) in the table.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'cor-table-col',
|
||||
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,
|
||||
@Inject('TableService') private tableService: any) {
|
||||
|
||||
}
|
||||
|
||||
public ngOnInit(): void {
|
||||
this.parent.addColumn(this);
|
||||
}
|
||||
|
||||
public isNumeric(): boolean {
|
||||
return this.kindof == 'datetime';
|
||||
}
|
||||
|
||||
public processColumnForOrdered(value: any): any {
|
||||
if (this.kindof == 'datetime' && value) {
|
||||
return this.tableService.getReversedTimestamp(value);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
.cor-table-element .co-top-bar {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: baseline;
|
||||
}
|
67
static/js/directives/ui/cor-table/cor-table.component.html
Normal file
67
static/js/directives/ui/cor-table/cor-table.component.html
Normal file
|
@ -0,0 +1,67 @@
|
|||
<div class="cor-table-element">
|
||||
<span ng-transclude></span>
|
||||
|
||||
<!-- Filter -->
|
||||
<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 }}
|
||||
</span>
|
||||
<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>
|
||||
|
||||
<!-- Table -->
|
||||
<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)" style="{{ ::col.style }}"
|
||||
class="{{ ::col.class }}">
|
||||
<a ng-click="$ctrl.setOrder(col)">{{ ::col.title }}</a>
|
||||
</td>
|
||||
</thead>
|
||||
<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"
|
||||
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>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<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-secondary-msg">Try adjusting your filter above.</div>
|
||||
</div>
|
||||
</div>
|
126
static/js/directives/ui/cor-table/cor-table.component.spec.ts
Normal file
126
static/js/directives/ui/cor-table/cor-table.component.spec.ts
Normal file
|
@ -0,0 +1,126 @@
|
|||
import { Mock } from 'ts-mocks';
|
||||
import { CorTableComponent, CorTableOptions } from './cor-table.component';
|
||||
import { CorTableColumn } from './cor-table-col.component';
|
||||
import { SimpleChanges } from 'ng-metadata/core';
|
||||
import { ViewArray } from '../../../services/view-array/view-array';
|
||||
import Spy = jasmine.Spy;
|
||||
|
||||
|
||||
describe("CorTableComponent", () => {
|
||||
var component: CorTableComponent;
|
||||
var tableServiceMock: Mock<any>;
|
||||
var tableData: any[];
|
||||
var columnMocks: Mock<CorTableColumn>[];
|
||||
var orderedDataMock: Mock<ViewArray>;
|
||||
|
||||
beforeEach(() => {
|
||||
orderedDataMock = new Mock<ViewArray>();
|
||||
orderedDataMock.setup(mock => mock.visibleEntries).is([]);
|
||||
tableServiceMock = new Mock<any>();
|
||||
tableServiceMock.setup(mock => mock.buildOrderedItems)
|
||||
.is((items, options, filterFields, numericFields, extrafilter?) => orderedDataMock.Object);
|
||||
|
||||
tableData = [
|
||||
{name: "apple", last_modified: 1496068383000, version: "1.0.0"},
|
||||
{name: "pear", last_modified: 1496068383001, version: "1.1.0"},
|
||||
{name: "orange", last_modified: 1496068383002, version: "1.0.0"},
|
||||
{name: "banana", last_modified: 1496068383000, version: "2.0.0"},
|
||||
];
|
||||
|
||||
columnMocks = Object.keys(tableData[0])
|
||||
.map((key, index) => {
|
||||
const col = new Mock<CorTableColumn>();
|
||||
col.setup(mock => mock.isNumeric).is(() => index == 1 ? true : false);
|
||||
col.setup(mock => mock.processColumnForOrdered).is((value) => "dummy");
|
||||
col.setup(mock => mock.datafield).is(key);
|
||||
|
||||
return col;
|
||||
});
|
||||
|
||||
component = new CorTableComponent(tableServiceMock.Object);
|
||||
component.tableData = tableData;
|
||||
component.filterFields = ['name', 'version'];
|
||||
component.compact = false;
|
||||
component.tableItemTitle = "fruits";
|
||||
component.maxDisplayCount = 10;
|
||||
// Add columns
|
||||
columnMocks.forEach(colMock => component.addColumn(colMock.Object));
|
||||
(<Spy>tableServiceMock.Object.buildOrderedItems).calls.reset();
|
||||
});
|
||||
|
||||
describe("constructor", () => {
|
||||
|
||||
it("sets table options", () => {
|
||||
expect(component.options.filter).toEqual('');
|
||||
expect(component.options.reverse).toBe(false);
|
||||
expect(component.options.predicate).toEqual('');
|
||||
expect(component.options.page).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ngOnChanges", () => {
|
||||
var changes: SimpleChanges;
|
||||
|
||||
it("calls table service to build ordered items if table data is changed", () => {
|
||||
changes = {tableData: {currentValue: [], previousValue: [], isFirstChange: () => false}};
|
||||
component.ngOnChanges(changes);
|
||||
|
||||
expect((<Spy>tableServiceMock.Object.buildOrderedItems)).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("passes processed table data to table service", () => {
|
||||
changes = {tableData: {currentValue: [], previousValue: [], isFirstChange: () => false}};
|
||||
component.tableData = changes['tableData'].currentValue;
|
||||
component.ngOnChanges(changes);
|
||||
|
||||
expect((<Spy>tableServiceMock.Object.buildOrderedItems).calls.argsFor(0)[0]).not.toEqual(tableData);
|
||||
});
|
||||
|
||||
it("passes options to table service", () => {
|
||||
changes = {tableData: {currentValue: [], previousValue: [], isFirstChange: () => false}};
|
||||
component.ngOnChanges(changes);
|
||||
|
||||
expect((<Spy>tableServiceMock.Object.buildOrderedItems).calls.argsFor(0)[1]).toEqual(component.options);
|
||||
});
|
||||
|
||||
it("passes filter fields to table service", () => {
|
||||
changes = {tableData: {currentValue: [], previousValue: [], isFirstChange: () => false}};
|
||||
component.ngOnChanges(changes);
|
||||
|
||||
expect((<Spy>tableServiceMock.Object.buildOrderedItems).calls.argsFor(0)[2]).toEqual(component.filterFields);
|
||||
});
|
||||
|
||||
it("passes numeric fields to table service", () => {
|
||||
changes = {tableData: {currentValue: [], previousValue: [], isFirstChange: () => false}};
|
||||
component.ngOnChanges(changes);
|
||||
|
||||
const expectedNumericCols: string[] = columnMocks.filter(colMock => colMock.Object.isNumeric())
|
||||
.map(colMock => colMock.Object.datafield);
|
||||
|
||||
expect((<Spy>tableServiceMock.Object.buildOrderedItems).calls.argsFor(0)[3]).toEqual(expectedNumericCols);
|
||||
});
|
||||
|
||||
it("resets to first page if table data is changed", () => {
|
||||
component.options.page = 1;
|
||||
changes = {tableData: {currentValue: [], previousValue: [], isFirstChange: () => false}};
|
||||
component.ngOnChanges(changes);
|
||||
|
||||
expect(component.options.page).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("addColumn", () => {
|
||||
var columnMock: Mock<CorTableColumn>;
|
||||
|
||||
beforeEach(() => {
|
||||
columnMock = new Mock<CorTableColumn>();
|
||||
columnMock.setup(mock => mock.isNumeric).is(() => false);
|
||||
});
|
||||
|
||||
it("calls table service to build ordered items with new column", () => {
|
||||
component.addColumn(columnMock.Object);
|
||||
|
||||
expect((<Spy>tableServiceMock.Object.buildOrderedItems)).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
110
static/js/directives/ui/cor-table/cor-table.component.ts
Normal file
110
static/js/directives/ui/cor-table/cor-table.component.ts
Normal file
|
@ -0,0 +1,110 @@
|
|||
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';
|
||||
|
||||
|
||||
/**
|
||||
* A component that displays a table of information, with optional filtering and automatic sorting.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'cor-table',
|
||||
templateUrl: '/static/js/directives/ui/cor-table/cor-table.component.html',
|
||||
legacy: {
|
||||
transclude: true
|
||||
}
|
||||
})
|
||||
export class CorTableComponent implements OnChanges {
|
||||
|
||||
@Input('<') public tableData: any[] = [];
|
||||
@Input('@') public tableItemTitle: string;
|
||||
@Input('<') public filterFields: string[];
|
||||
@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) {
|
||||
|
||||
}
|
||||
|
||||
public ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes['tableData'] !== undefined) {
|
||||
this.refreshOrder();
|
||||
}
|
||||
}
|
||||
|
||||
public addColumn(col: CorTableColumn): void {
|
||||
this.columns.push(col);
|
||||
|
||||
if (col.selected == 'true') {
|
||||
this.options['predicate'] = col.datafield;
|
||||
}
|
||||
|
||||
this.refreshOrder();
|
||||
}
|
||||
|
||||
private setOrder(col: CorTableColumn): void {
|
||||
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);
|
||||
}
|
||||
|
||||
private refreshOrder(): void {
|
||||
this.options.page = 0;
|
||||
|
||||
var columnMap: {[name: string]: CorTableColumn} = {};
|
||||
this.columns.forEach(function(col) {
|
||||
columnMap[col.datafield] = col;
|
||||
});
|
||||
|
||||
const numericCols: string[] = this.columns.filter(col => col.isNumeric())
|
||||
.map(col => col.datafield);
|
||||
|
||||
const processed: any[] = this.tableData.map((item) => {
|
||||
Object.keys(item).forEach((key) => {
|
||||
if (columnMap[key]) {
|
||||
item[key] = columnMap[key].processColumnForOrdered(item[key]);
|
||||
}
|
||||
});
|
||||
|
||||
return item;
|
||||
});
|
||||
|
||||
this.orderedData = this.tableService.buildOrderedItems(processed, this.options, this.filterFields, numericCols);
|
||||
this.rows = this.orderedData.visibleEntries.map((item) => Object.assign({}, {expanded: false, rowData: item}));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export type CorTableOptions = {
|
||||
filter: string;
|
||||
reverse: boolean;
|
||||
predicate: string;
|
||||
page: number;
|
||||
};
|
||||
|
||||
|
||||
export type CorTableRow = {
|
||||
expanded: boolean;
|
||||
rowData: any;
|
||||
};
|
|
@ -0,0 +1,61 @@
|
|||
import { CorCookieTabsDirective } from './cor-cookie-tabs.directive';
|
||||
import { CorTabPanelComponent } from '../cor-tab-panel/cor-tab-panel.component';
|
||||
import { Mock } from 'ts-mocks';
|
||||
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
|
||||
import Spy = jasmine.Spy;
|
||||
|
||||
|
||||
describe("CorCookieTabsDirective", () => {
|
||||
var directive: CorCookieTabsDirective;
|
||||
var panelMock: Mock<CorTabPanelComponent>;
|
||||
var cookieServiceMock: Mock<any>;
|
||||
var activeTab: BehaviorSubject<string>;
|
||||
|
||||
beforeEach(() => {
|
||||
activeTab = new BehaviorSubject<string>(null);
|
||||
spyOn(activeTab, "subscribe").and.returnValue(null);
|
||||
panelMock = new Mock<CorTabPanelComponent>();
|
||||
panelMock.setup(mock => mock.activeTab).is(activeTab);
|
||||
cookieServiceMock = new Mock<any>();
|
||||
cookieServiceMock.setup(mock => mock.putPermanent).is((cookieName, value) => null);
|
||||
|
||||
directive = new CorCookieTabsDirective(panelMock.Object, cookieServiceMock.Object);
|
||||
directive.cookieName = "quay.credentialsTab";
|
||||
});
|
||||
|
||||
describe("ngAfterContentInit", () => {
|
||||
const tabId: string = "description";
|
||||
|
||||
beforeEach(() => {
|
||||
cookieServiceMock.setup(mock => mock.get).is((name) => tabId);
|
||||
spyOn(activeTab, "next").and.returnValue(null);
|
||||
});
|
||||
|
||||
it("calls cookie service to retrieve initial tab id", () => {
|
||||
directive.ngAfterContentInit();
|
||||
|
||||
expect((<Spy>cookieServiceMock.Object.get).calls.argsFor(0)[0]).toEqual(directive.cookieName);
|
||||
});
|
||||
|
||||
it("emits retrieved tab id as next active tab", () => {
|
||||
directive.ngAfterContentInit();
|
||||
|
||||
expect((<Spy>panelMock.Object.activeTab.next).calls.argsFor(0)[0]).toEqual(tabId);
|
||||
});
|
||||
|
||||
it("subscribes to active tab changes", () => {
|
||||
directive.ngAfterContentInit();
|
||||
|
||||
expect((<Spy>panelMock.Object.activeTab.subscribe)).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("calls cookie service to put new permanent cookie on active tab changes", () => {
|
||||
directive.ngAfterContentInit();
|
||||
const tabId: string = "description";
|
||||
(<Spy>panelMock.Object.activeTab.subscribe).calls.argsFor(0)[0](tabId);
|
||||
|
||||
expect((<Spy>cookieServiceMock.Object.putPermanent).calls.argsFor(0)[0]).toEqual(directive.cookieName);
|
||||
expect((<Spy>cookieServiceMock.Object.putPermanent).calls.argsFor(0)[1]).toEqual(tabId);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,30 @@
|
|||
import { Directive, Inject, Host, AfterContentInit, Input } from 'ng-metadata/core';
|
||||
import { CorTabPanelComponent } from '../cor-tab-panel/cor-tab-panel.component';
|
||||
|
||||
|
||||
/**
|
||||
* Adds routing capabilities to cor-tab-panel using a browser cookie.
|
||||
*/
|
||||
@Directive({
|
||||
selector: '[corCookieTabs]'
|
||||
})
|
||||
export class CorCookieTabsDirective implements AfterContentInit {
|
||||
|
||||
@Input('@corCookieTabs') public cookieName: string;
|
||||
|
||||
constructor(@Host() @Inject(CorTabPanelComponent) private panel: CorTabPanelComponent,
|
||||
@Inject('CookieService') private cookieService: any) {
|
||||
|
||||
}
|
||||
|
||||
public ngAfterContentInit(): void {
|
||||
// Set initial tab
|
||||
const tabId: string = this.cookieService.get(this.cookieName);
|
||||
|
||||
this.panel.activeTab.next(tabId);
|
||||
|
||||
this.panel.activeTab.subscribe((tab: string) => {
|
||||
this.cookieService.putPermanent(this.cookieName, tab);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
import { CorNavTabsDirective } from './cor-nav-tabs.directive';
|
||||
import { CorTabPanelComponent } from '../cor-tab-panel/cor-tab-panel.component';
|
||||
import { Mock } from 'ts-mocks';
|
||||
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
|
||||
import Spy = jasmine.Spy;
|
||||
|
||||
|
||||
describe("CorNavTabsDirective", () => {
|
||||
var directive: CorNavTabsDirective;
|
||||
var panelMock: Mock<CorTabPanelComponent>;
|
||||
var $locationMock: Mock<ng.ILocationService>;
|
||||
var $rootScopeMock: Mock<ng.IRootScopeService>;
|
||||
var activeTab: BehaviorSubject<string>;
|
||||
const tabId: string = "description";
|
||||
|
||||
beforeEach(() => {
|
||||
activeTab = new BehaviorSubject<string>(null);
|
||||
spyOn(activeTab, "next").and.returnValue(null);
|
||||
panelMock = new Mock<CorTabPanelComponent>();
|
||||
panelMock.setup(mock => mock.activeTab).is(activeTab);
|
||||
$locationMock = new Mock<ng.ILocationService>();
|
||||
$locationMock.setup(mock => mock.search).is(() => <any>{tab: tabId});
|
||||
$rootScopeMock = new Mock<ng.IRootScopeService>();
|
||||
$rootScopeMock.setup(mock => mock.$on);
|
||||
|
||||
directive = new CorNavTabsDirective(panelMock.Object, $locationMock.Object, $rootScopeMock.Object);
|
||||
});
|
||||
|
||||
describe("constructor", () => {
|
||||
|
||||
it("subscribes to $routeUpdate event on the root scope", () => {
|
||||
expect((<Spy>$rootScopeMock.Object.$on).calls.argsFor(0)[0]).toEqual("$routeUpdate");
|
||||
});
|
||||
|
||||
it("calls location service to retrieve tab id from URL query parameters on route update", () => {
|
||||
(<Spy>$rootScopeMock.Object.$on).calls.argsFor(0)[1]();
|
||||
|
||||
expect(<Spy>$locationMock.Object.search).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("emits retrieved tab id as next active tab on route update", () => {
|
||||
(<Spy>$rootScopeMock.Object.$on).calls.argsFor(0)[1]();
|
||||
|
||||
expect((<Spy>activeTab.next).calls.argsFor(0)[0]).toEqual(tabId);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ngAfterContentInit", () => {
|
||||
const path: string = "quay.io/repository/devtable/simple";
|
||||
|
||||
beforeEach(() => {
|
||||
$locationMock.setup(mock => mock.path).is(() => <any>path);
|
||||
});
|
||||
|
||||
it("calls location service to retrieve the current URL path and sets panel's base path", () => {
|
||||
directive.ngAfterContentInit();
|
||||
|
||||
expect(panelMock.Object.basePath).toEqual(path);
|
||||
});
|
||||
|
||||
it("calls location service to retrieve tab id from URL query parameters", () => {
|
||||
directive.ngAfterContentInit();
|
||||
|
||||
expect(<Spy>$locationMock.Object.search).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("emits retrieved tab id as next active tab", () => {
|
||||
directive.ngAfterContentInit();
|
||||
|
||||
expect((<Spy>activeTab.next).calls.argsFor(0)[0]).toEqual(tabId);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,29 @@
|
|||
import { Directive, Inject, Host, AfterContentInit, Input } from 'ng-metadata/core';
|
||||
import { CorTabPanelComponent } from '../cor-tab-panel/cor-tab-panel.component';
|
||||
|
||||
|
||||
/**
|
||||
* Adds routing capabilities to cor-tab-panel, either using URL query parameters, or browser cookie.
|
||||
*/
|
||||
@Directive({
|
||||
selector: '[corNavTabs]'
|
||||
})
|
||||
export class CorNavTabsDirective implements AfterContentInit {
|
||||
|
||||
constructor(@Host() @Inject(CorTabPanelComponent) private panel: CorTabPanelComponent,
|
||||
@Inject('$location') private $location: ng.ILocationService,
|
||||
@Inject('$rootScope') private $rootScope: ng.IRootScopeService) {
|
||||
this.$rootScope.$on('$routeUpdate', () => {
|
||||
const tabId: string = this.$location.search()['tab'];
|
||||
this.panel.activeTab.next(tabId);
|
||||
});
|
||||
}
|
||||
|
||||
public ngAfterContentInit(): void {
|
||||
this.panel.basePath = this.$location.path();
|
||||
|
||||
// Set initial tab
|
||||
const tabId: string = this.$location.search()['tab'];
|
||||
this.panel.activeTab.next(tabId);
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
<div class="co-tab-content tab-content col-md-11" ng-transclude></div>
|
|
@ -0,0 +1,17 @@
|
|||
import { Component } from 'ng-metadata/core';
|
||||
|
||||
|
||||
/**
|
||||
* A component that is placed under a cor-tabs to wrap tab content with additional styling.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'cor-tab-content',
|
||||
templateUrl: '/static/js/directives/ui/cor-tabs/cor-tab-content/cor-tab-content.component.html',
|
||||
legacy: {
|
||||
transclude: true,
|
||||
replace: true,
|
||||
}
|
||||
})
|
||||
export class CorTabContentComponent {
|
||||
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
<div class="co-tab-pane" ng-show="$ctrl.isActiveTab">
|
||||
<div ng-transclude />
|
||||
</div>
|
|
@ -0,0 +1,63 @@
|
|||
import { CorTabPaneComponent } from './cor-tab-pane.component';
|
||||
import { CorTabPanelComponent } from '../cor-tab-panel/cor-tab-panel.component';
|
||||
import { Mock } from 'ts-mocks';
|
||||
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
|
||||
import Spy = jasmine.Spy;
|
||||
|
||||
|
||||
describe("CorTabPaneComponent", () => {
|
||||
var component: CorTabPaneComponent;
|
||||
var panelMock: Mock<CorTabPanelComponent>;
|
||||
var activeTab: BehaviorSubject<string>;
|
||||
|
||||
beforeEach(() => {
|
||||
activeTab = new BehaviorSubject<string>(null);
|
||||
spyOn(activeTab, "subscribe").and.callThrough();
|
||||
panelMock = new Mock<CorTabPanelComponent>();
|
||||
panelMock.setup(mock => mock.activeTab).is(activeTab);
|
||||
|
||||
component = new CorTabPaneComponent(panelMock.Object);
|
||||
component.id = 'description';
|
||||
});
|
||||
|
||||
describe("ngOnInit", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
panelMock.setup(mock => mock.addTabPane);
|
||||
});
|
||||
|
||||
it("adds self as tab pane to panel", () => {
|
||||
component.ngOnInit();
|
||||
|
||||
expect((<Spy>panelMock.Object.addTabPane).calls.argsFor(0)[0]).toBe(component);
|
||||
});
|
||||
|
||||
it("subscribes to active tab changes", () => {
|
||||
component.ngOnInit();
|
||||
|
||||
expect((<Spy>panelMock.Object.activeTab.subscribe)).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does nothing if active tab ID is undefined", () => {
|
||||
component.ngOnInit();
|
||||
component.isActiveTab = true;
|
||||
panelMock.Object.activeTab.next(null);
|
||||
|
||||
expect(component.isActiveTab).toEqual(true);
|
||||
});
|
||||
|
||||
it("sets self as active if active tab ID matches tab ID", () => {
|
||||
component.ngOnInit();
|
||||
panelMock.Object.activeTab.next(component.id);
|
||||
|
||||
expect(component.isActiveTab).toEqual(true);
|
||||
});
|
||||
|
||||
it("sets self as inactive if active tab ID does not match tab ID", () => {
|
||||
component.ngOnInit();
|
||||
panelMock.Object.activeTab.next(component.id.split('').reverse().join(''));
|
||||
|
||||
expect(component.isActiveTab).toEqual(false);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,35 @@
|
|||
import { Component, Input, Inject, Host, OnInit } from 'ng-metadata/core';
|
||||
import { CorTabPanelComponent } from '../cor-tab-panel/cor-tab-panel.component';
|
||||
import 'rxjs/add/operator/filter';
|
||||
|
||||
|
||||
/**
|
||||
* A component that creates a single tab pane under a cor-tabs component.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'cor-tab-pane',
|
||||
templateUrl: '/static/js/directives/ui/cor-tabs/cor-tab-pane/cor-tab-pane.component.html',
|
||||
legacy: {
|
||||
transclude: true,
|
||||
}
|
||||
})
|
||||
export class CorTabPaneComponent implements OnInit {
|
||||
|
||||
@Input('@') public id: string;
|
||||
|
||||
public isActiveTab: boolean = false;
|
||||
|
||||
constructor(@Host() @Inject(CorTabPanelComponent) private panel: CorTabPanelComponent) {
|
||||
|
||||
}
|
||||
|
||||
public ngOnInit(): void {
|
||||
this.panel.addTabPane(this);
|
||||
|
||||
this.panel.activeTab
|
||||
.filter(tabId => tabId != undefined)
|
||||
.subscribe((tabId: string) => {
|
||||
this.isActiveTab = (this.id === tabId);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
<div class="co-main-content-panel co-tab-panel co-fx-box-shadow-heavy">
|
||||
<div class="co-tab-container" ng-class="$ctrl.isVertical() ? 'vertical': 'horizontal'" ng-transclude></div>
|
||||
</div>
|
|
@ -0,0 +1,132 @@
|
|||
import { CorTabPanelComponent } from './cor-tab-panel.component';
|
||||
import { CorTabComponent } from '../cor-tab/cor-tab.component';
|
||||
import { SimpleChanges } from 'ng-metadata/core';
|
||||
import Spy = jasmine.Spy;
|
||||
|
||||
|
||||
describe("CorTabPanelComponent", () => {
|
||||
var component: CorTabPanelComponent;
|
||||
|
||||
beforeEach(() => {
|
||||
component = new CorTabPanelComponent();
|
||||
});
|
||||
|
||||
describe("ngOnInit", () => {
|
||||
var tabs: CorTabComponent[] = [];
|
||||
|
||||
beforeEach(() => {
|
||||
// Add tabs to panel
|
||||
tabs.push(new CorTabComponent(component));
|
||||
tabs[0].tabId = "info";
|
||||
tabs.forEach((tab) => component.addTab(tab));
|
||||
|
||||
spyOn(component.activeTab, "subscribe").and.callThrough();
|
||||
spyOn(component.activeTab, "next").and.callThrough();
|
||||
spyOn(component.tabChange, "emit").and.returnValue(null);
|
||||
});
|
||||
|
||||
it("subscribes to active tab changes", () => {
|
||||
component.ngOnInit();
|
||||
|
||||
expect(<Spy>component.activeTab.subscribe).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("emits next active tab with tab ID of first registered tab if given tab ID is null", () => {
|
||||
component.ngOnInit();
|
||||
component.activeTab.next(null);
|
||||
|
||||
expect((<Spy>component.activeTab.next).calls.argsFor(1)[0]).toEqual(tabs[0].tabId);
|
||||
});
|
||||
|
||||
it("does not emit output event for tab change if tab ID is null", () => {
|
||||
component.ngOnInit();
|
||||
component.activeTab.next(null);
|
||||
|
||||
expect((<Spy>component.tabChange.emit).calls.allArgs).not.toContain(null);
|
||||
});
|
||||
|
||||
it("emits output event for tab change when tab ID is not null", () => {
|
||||
component.ngOnInit();
|
||||
const tabId: string = "description";
|
||||
component.activeTab.next(tabId);
|
||||
|
||||
expect((<Spy>component.tabChange.emit).calls.argsFor(1)[0]).toEqual(tabId);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ngOnChanges", () => {
|
||||
var changes: SimpleChanges;
|
||||
var tabs: CorTabComponent[] = [];
|
||||
|
||||
beforeEach(() => {
|
||||
// Add tabs to panel
|
||||
tabs.push(new CorTabComponent(component));
|
||||
tabs.forEach((tab) => component.addTab(tab));
|
||||
|
||||
changes = {
|
||||
'selectedIndex': {
|
||||
currentValue: 0,
|
||||
previousValue: null,
|
||||
isFirstChange: () => false
|
||||
},
|
||||
};
|
||||
|
||||
spyOn(component.activeTab, "next").and.returnValue(null);
|
||||
});
|
||||
|
||||
it("emits next active tab if 'selectedIndex' input changes and is valid", () => {
|
||||
component.ngOnChanges(changes);
|
||||
|
||||
expect((<Spy>component.activeTab.next).calls.argsFor(0)[0]).toEqual(tabs[changes['selectedIndex'].currentValue].tabId);
|
||||
});
|
||||
|
||||
it("does nothing if 'selectedIndex' input changed to invalid value", () => {
|
||||
changes['selectedIndex'].currentValue = 100;
|
||||
component.ngOnChanges(changes);
|
||||
|
||||
expect(<Spy>component.activeTab.next).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("addTab", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(component.activeTab, "next").and.returnValue(null);
|
||||
});
|
||||
|
||||
it("emits next active tab if it is not set", () => {
|
||||
const tab: CorTabComponent = new CorTabComponent(component);
|
||||
component.addTab(tab);
|
||||
|
||||
expect((<Spy>component.activeTab.next).calls.argsFor(0)[0]).toEqual(tab.tabId);
|
||||
});
|
||||
|
||||
it("does not emit next active tab if it is already set", () => {
|
||||
spyOn(component.activeTab, "getValue").and.returnValue("description");
|
||||
const tab: CorTabComponent = new CorTabComponent(component);
|
||||
component.addTab(tab);
|
||||
|
||||
expect(<Spy>component.activeTab.next).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("addTabPane", () => {
|
||||
|
||||
});
|
||||
|
||||
describe("isVertical", () => {
|
||||
|
||||
it("returns true if orientation is 'vertical'", () => {
|
||||
component.orientation = 'vertical';
|
||||
const isVertical: boolean = component.isVertical();
|
||||
|
||||
expect(isVertical).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false if orientation is not 'vertical'", () => {
|
||||
const isVertical: boolean = component.isVertical();
|
||||
|
||||
expect(isVertical).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,65 @@
|
|||
import { Component, Input, Output, EventEmitter, OnChanges, SimpleChanges, OnInit } from 'ng-metadata/core';
|
||||
import { CorTabComponent } from '../cor-tab/cor-tab.component';
|
||||
import { CorTabPaneComponent } from '../cor-tab-pane/cor-tab-pane.component';
|
||||
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
|
||||
|
||||
|
||||
/**
|
||||
* A component that contains a cor-tabs and handles all of its logic.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'cor-tab-panel',
|
||||
templateUrl: '/static/js/directives/ui/cor-tabs/cor-tab-panel/cor-tab-panel.component.html',
|
||||
legacy: {
|
||||
transclude: true
|
||||
}
|
||||
})
|
||||
export class CorTabPanelComponent implements OnInit, OnChanges {
|
||||
|
||||
@Input('@') public orientation: 'horizontal' | 'vertical' = 'horizontal';
|
||||
|
||||
@Output() public tabChange: EventEmitter<string> = new EventEmitter();
|
||||
|
||||
public basePath: string;
|
||||
public activeTab = new BehaviorSubject<string>(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';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
<li class="cor-tab-itself" ng-class="{'active': $ctrl.isActive, 'co-top-tab': !$ctrl.parent.isVertical()}">
|
||||
<a href="{{ $ctrl.panel.basePath ? $ctrl.panel.basePath + '?tab=' + $ctrl.tabId : '' }}"
|
||||
ng-click="$ctrl.tabClicked($event)">
|
||||
<span class="cor-tab-icon"
|
||||
data-title="{{ ::($ctrl.panel.isVertical() ? $ctrl.tabTitle : '') }}"
|
||||
data-placement="right"
|
||||
data-container="body"
|
||||
style="display: inline-block"
|
||||
bs-tooltip>
|
||||
<span ng-transclude /><span class="horizontal-label">{{ ::$ctrl.tabTitle }}</span>
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
|
@ -0,0 +1,85 @@
|
|||
import { CorTabComponent } from './cor-tab.component';
|
||||
import { CorTabPanelComponent } from '../cor-tab-panel/cor-tab-panel.component';
|
||||
import { Mock } from 'ts-mocks';
|
||||
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
|
||||
import Spy = jasmine.Spy;
|
||||
|
||||
|
||||
describe("CorTabComponent", () => {
|
||||
var component: CorTabComponent;
|
||||
var panelMock: Mock<CorTabPanelComponent>;
|
||||
var activeTab: BehaviorSubject<string>;
|
||||
|
||||
beforeEach(() => {
|
||||
activeTab = new BehaviorSubject<string>(null);
|
||||
spyOn(activeTab, "subscribe").and.callThrough();
|
||||
panelMock = new Mock<CorTabPanelComponent>();
|
||||
panelMock.setup(mock => mock.activeTab).is(activeTab);
|
||||
|
||||
component = new CorTabComponent(panelMock.Object);
|
||||
});
|
||||
|
||||
describe("ngOnInit", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
panelMock.setup(mock => mock.addTab);
|
||||
spyOn(component.tabInit, "emit").and.returnValue(null);
|
||||
spyOn(component.tabShow, "emit").and.returnValue(null);
|
||||
spyOn(component.tabHide, "emit").and.returnValue(null);
|
||||
component.tabId = "description";
|
||||
});
|
||||
|
||||
it("subscribes to active tab changes", () => {
|
||||
component.ngOnInit();
|
||||
|
||||
expect((<Spy>panelMock.Object.activeTab.subscribe)).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does nothing if active tab ID is undefined", () => {
|
||||
component.ngOnInit();
|
||||
panelMock.Object.activeTab.next(null);
|
||||
|
||||
expect(<Spy>component.tabInit.emit).not.toHaveBeenCalled();
|
||||
expect(<Spy>component.tabShow.emit).not.toHaveBeenCalled();
|
||||
expect(<Spy>component.tabHide.emit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("emits output event for tab init if it is new active tab", () => {
|
||||
component.ngOnInit();
|
||||
panelMock.Object.activeTab.next(component.tabId);
|
||||
|
||||
expect(<Spy>component.tabInit.emit).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("emits output event for tab show if it is new active tab", () => {
|
||||
component.ngOnInit();
|
||||
panelMock.Object.activeTab.next(component.tabId);
|
||||
|
||||
expect(<Spy>component.tabShow.emit).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("emits output event for tab hide if active tab changes to different tab", () => {
|
||||
const newTabId: string = component.tabId.split('').reverse().join('');
|
||||
component.ngOnInit();
|
||||
// Call twice, first time to set 'isActive' to true
|
||||
panelMock.Object.activeTab.next(component.tabId);
|
||||
panelMock.Object.activeTab.next(newTabId);
|
||||
|
||||
expect(<Spy>component.tabHide.emit).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not emit output event for tab hide if was not previously active tab", () => {
|
||||
const newTabId: string = component.tabId.split('').reverse().join('');
|
||||
component.ngOnInit();
|
||||
panelMock.Object.activeTab.next(newTabId);
|
||||
|
||||
expect(<Spy>component.tabHide.emit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("adds self as tab to panel", () => {
|
||||
component.ngOnInit();
|
||||
|
||||
expect((<Spy>panelMock.Object.addTab).calls.argsFor(0)[0]).toBe(component);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,56 @@
|
|||
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;
|
||||
@Input('<') public tabActive: boolean = false;
|
||||
|
||||
@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.isActive = this.tabActive;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
4
static/js/directives/ui/cor-tabs/cor-tabs.component.html
Normal file
4
static/js/directives/ui/cor-tabs/cor-tabs.component.html
Normal file
|
@ -0,0 +1,4 @@
|
|||
<span class="co-tab-element" ng-class="$ctrl.isClosed ? 'closed' : 'open'">
|
||||
<span class="xs-toggle" ng-click="$ctrl.toggleClosed($event)"></span>
|
||||
<ul ng-class="$ctrl.parent.isVertical() ? 'co-tabs col-md-1' : 'co-top-tab-bar'" ng-transclude></ul>
|
||||
</span>
|
26
static/js/directives/ui/cor-tabs/cor-tabs.component.ts
Normal file
26
static/js/directives/ui/cor-tabs/cor-tabs.component.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
import { Component, Input, Output, Inject, EventEmitter, Host } from 'ng-metadata/core';
|
||||
import { CorTabPanelComponent } from './cor-tab-panel/cor-tab-panel.component';
|
||||
|
||||
|
||||
/**
|
||||
* A component that holds the actual tabs.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'cor-tabs',
|
||||
templateUrl: '/static/js/directives/ui/cor-tabs/cor-tabs.component.html',
|
||||
legacy: {
|
||||
transclude: true,
|
||||
}
|
||||
})
|
||||
export class CorTabsComponent {
|
||||
|
||||
private isClosed: boolean = true;
|
||||
|
||||
constructor(@Host() @Inject(CorTabPanelComponent) private parent: CorTabPanelComponent) {
|
||||
|
||||
}
|
||||
|
||||
private toggleClosed(e): void {
|
||||
this.isClosed = !this.isClosed;
|
||||
}
|
||||
}
|
33
static/js/directives/ui/cor-tabs/cor-tabs.module.ts
Normal file
33
static/js/directives/ui/cor-tabs/cor-tabs.module.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
import { NgModule } from 'ng-metadata/core';
|
||||
import { CorTabsComponent } from './cor-tabs.component';
|
||||
import { CorTabComponent } from './cor-tab/cor-tab.component';
|
||||
import { CorNavTabsDirective } from './cor-nav-tabs/cor-nav-tabs.directive';
|
||||
import { CorTabContentComponent } from './cor-tab-content/cor-tab-content.component';
|
||||
import { CorTabPaneComponent } from './cor-tab-pane/cor-tab-pane.component';
|
||||
import { CorTabPanelComponent } from './cor-tab-panel/cor-tab-panel.component';
|
||||
import { CorCookieTabsDirective } from './cor-cookie-tabs/cor-cookie-tabs.directive';
|
||||
|
||||
|
||||
/**
|
||||
* Module containing everything needed for cor-tabs.
|
||||
*/
|
||||
@NgModule({
|
||||
imports: [
|
||||
|
||||
],
|
||||
declarations: [
|
||||
CorNavTabsDirective,
|
||||
CorTabComponent,
|
||||
CorTabContentComponent,
|
||||
CorTabPaneComponent,
|
||||
CorTabPanelComponent,
|
||||
CorTabsComponent,
|
||||
CorCookieTabsDirective,
|
||||
],
|
||||
providers: [
|
||||
|
||||
]
|
||||
})
|
||||
export class CorTabsModule {
|
||||
|
||||
}
|
13
static/js/directives/ui/cor-tabs/cor-tabs.view-object.ts
Normal file
13
static/js/directives/ui/cor-tabs/cor-tabs.view-object.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import { element, by, browser, $, ElementFinder, ExpectedConditions as until } from 'protractor';
|
||||
|
||||
|
||||
export class CorTabsViewObject {
|
||||
|
||||
public selectTabByTitle(title: string) {
|
||||
return $(`cor-tab[tab-title="${title}"] a`).click();
|
||||
}
|
||||
|
||||
public isActiveTab(title: string) {
|
||||
return $(`cor-tab[tab-title="${title}"] .cor-tab-itself.active`).isPresent();
|
||||
}
|
||||
}
|
119
static/js/directives/ui/create-entity-dialog.js
Normal file
119
static/js/directives/ui/create-entity-dialog.js
Normal file
|
@ -0,0 +1,119 @@
|
|||
/**
|
||||
* An element which displays a create entity dialog.
|
||||
*/
|
||||
angular.module('quay').directive('createEntityDialog', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/create-entity-dialog.html',
|
||||
replace: false,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'info': '=info',
|
||||
|
||||
'entityKind': '@entityKind',
|
||||
'entityTitle': '@entityTitle',
|
||||
'entityIcon': '@entityIcon',
|
||||
'entityNameRegex': '@entityNameRegex',
|
||||
'allowEntityDescription': '@allowEntityDescription',
|
||||
|
||||
'entityCreateRequested': '&entityCreateRequested',
|
||||
'entityCreateCompleted': '&entityCreateCompleted'
|
||||
},
|
||||
|
||||
controller: function($scope, $element, ApiService, UIService, UserService) {
|
||||
$scope.context = {
|
||||
'setPermissionsCounter': 0
|
||||
};
|
||||
|
||||
$scope.$on('$destroy', function() {
|
||||
if ($scope.inBody) {
|
||||
document.body.removeChild($element[0]);
|
||||
}
|
||||
});
|
||||
|
||||
$scope.hide = function() {
|
||||
$element.find('.modal').modal('hide');
|
||||
if ($scope.entity) {
|
||||
$scope.entityCreateCompleted({'entity': $scope.entity});
|
||||
$scope.entity = null;
|
||||
}
|
||||
};
|
||||
|
||||
$scope.show = function() {
|
||||
$scope.entityName = null;
|
||||
$scope.entityDescription = null;
|
||||
$scope.entity = null;
|
||||
$scope.entityForPermissions = null;
|
||||
$scope.creating = false;
|
||||
$scope.view = 'enterName';
|
||||
$scope.enterNameForm.$setPristine(true);
|
||||
|
||||
// Move the dialog to the body to prevent it from nesting if called
|
||||
// from within another dialog.
|
||||
$element.find('.modal').modal({});
|
||||
$scope.inBody = true;
|
||||
document.body.appendChild($element[0]);
|
||||
};
|
||||
|
||||
var entityCreateCallback = function(entity) {
|
||||
$scope.entity = entity;
|
||||
|
||||
if (!entity || $scope.info.skip_permissions) {
|
||||
$scope.hide();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
$scope.createEntity = function() {
|
||||
$scope.view = 'creating';
|
||||
$scope.entityCreateRequested({
|
||||
'name': $scope.entityName,
|
||||
'description': $scope.entityDescription,
|
||||
'callback': entityCreateCallback
|
||||
});
|
||||
};
|
||||
|
||||
$scope.permissionsSet = function(repositories) {
|
||||
$scope.entity['repo_count'] = repositories.length;
|
||||
$scope.hide();
|
||||
};
|
||||
|
||||
$scope.settingPermissions = function() {
|
||||
$scope.view = 'settingperms';
|
||||
};
|
||||
|
||||
$scope.setPermissions = function() {
|
||||
$scope.context.setPermissionsCounter++;
|
||||
};
|
||||
|
||||
$scope.repositoriesLoaded = function(repositories) {
|
||||
if (repositories && !repositories.length) {
|
||||
$scope.hide();
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.view = 'setperms';
|
||||
};
|
||||
|
||||
$scope.$watch('entityNameRegex', function(r) {
|
||||
if (r) {
|
||||
$scope.entityNameRegexObj = new RegExp(r);
|
||||
}
|
||||
});
|
||||
|
||||
$scope.$watch('info', function(info) {
|
||||
if (!info || !info.namespace) {
|
||||
$scope.hide();
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.namespace = UserService.getNamespace(info.namespace);
|
||||
if ($scope.namespace) {
|
||||
$scope.show();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
170
static/js/directives/ui/create-external-notification.js
Normal file
170
static/js/directives/ui/create-external-notification.js
Normal file
|
@ -0,0 +1,170 @@
|
|||
/**
|
||||
* An element which displays a form to register a new external notification on a repository.
|
||||
*/
|
||||
angular.module('quay').directive('createExternalNotification', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/create-external-notification.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'repository': '=repository',
|
||||
'notificationCreated': '¬ificationCreated',
|
||||
'defaultData': '=defaultData'
|
||||
},
|
||||
controller: function($scope, $element, ExternalNotificationData, ApiService, $timeout, StringBuilderService) {
|
||||
$scope.currentEvent = null;
|
||||
$scope.currentMethod = null;
|
||||
$scope.status = '';
|
||||
$scope.currentConfig = {};
|
||||
$scope.currentEventConfig = {};
|
||||
$scope.clearCounter = 0;
|
||||
$scope.unauthorizedEmail = false;
|
||||
|
||||
$scope.events = ExternalNotificationData.getSupportedEvents();
|
||||
$scope.methods = ExternalNotificationData.getSupportedMethods();
|
||||
|
||||
$scope.getPattern = function(field) {
|
||||
if (field._cached_regex) {
|
||||
return field._cached_regex;
|
||||
}
|
||||
|
||||
field._cached_regex = new RegExp(field.pattern);
|
||||
return field._cached_regex;
|
||||
};
|
||||
|
||||
$scope.setEvent = function(event) {
|
||||
$scope.currentEvent = event;
|
||||
$scope.currentEventConfig = {};
|
||||
};
|
||||
|
||||
$scope.setMethod = function(method) {
|
||||
$scope.currentConfig = {};
|
||||
$scope.currentMethod = method;
|
||||
$scope.unauthorizedEmail = false;
|
||||
};
|
||||
|
||||
$scope.hasRegexMismatch = function(err, fieldName) {
|
||||
if (!err.pattern) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (var i = 0; i < err.pattern.length; ++i) {
|
||||
var current = err.pattern[i];
|
||||
var value = current.$viewValue;
|
||||
var elem = $element.find('#' + fieldName);
|
||||
if (value == elem[0].value) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
$scope.createNotification = function() {
|
||||
if (!$scope.currentConfig.email) {
|
||||
$scope.performCreateNotification();
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.status = 'checking-email';
|
||||
$scope.checkEmailAuthorization();
|
||||
};
|
||||
|
||||
$scope.checkEmailAuthorization = function() {
|
||||
var params = {
|
||||
'repository': $scope.repository.namespace + '/' + $scope.repository.name,
|
||||
'email': $scope.currentConfig.email
|
||||
};
|
||||
|
||||
ApiService.checkRepoEmailAuthorized(null, params).then(function(resp) {
|
||||
$scope.handleEmailCheck(resp.confirmed);
|
||||
}, function(resp) {
|
||||
$scope.handleEmailCheck(false);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.performCreateNotification = function() {
|
||||
$scope.status = 'creating';
|
||||
|
||||
var params = {
|
||||
'repository': $scope.repository.namespace + '/' + $scope.repository.name
|
||||
};
|
||||
|
||||
var data = {
|
||||
'event': $scope.currentEvent.id,
|
||||
'method': $scope.currentMethod.id,
|
||||
'config': $scope.currentConfig,
|
||||
'eventConfig': $scope.currentEventConfig,
|
||||
'title': $scope.currentTitle
|
||||
};
|
||||
|
||||
ApiService.createRepoNotification(data, params).then(function(resp) {
|
||||
$scope.status = '';
|
||||
$scope.notificationCreated({'notification': resp});
|
||||
});
|
||||
};
|
||||
|
||||
$scope.handleEmailCheck = function(isAuthorized) {
|
||||
if (isAuthorized) {
|
||||
$scope.performCreateNotification();
|
||||
return;
|
||||
}
|
||||
|
||||
if ($scope.status == 'authorizing-email-sent') {
|
||||
$scope.watchEmail();
|
||||
} else {
|
||||
$scope.status = 'unauthorized-email';
|
||||
}
|
||||
|
||||
$scope.unauthorizedEmail = true;
|
||||
$('#authorizeEmailModal').modal({});
|
||||
};
|
||||
|
||||
$scope.sendAuthEmail = function() {
|
||||
$scope.status = 'authorizing-email';
|
||||
|
||||
var params = {
|
||||
'repository': $scope.repository.namespace + '/' + $scope.repository.name,
|
||||
'email': $scope.currentConfig.email
|
||||
};
|
||||
|
||||
ApiService.sendAuthorizeRepoEmail(null, params).then(function(resp) {
|
||||
$scope.status = 'authorizing-email-sent';
|
||||
$scope.watchEmail();
|
||||
});
|
||||
};
|
||||
|
||||
$scope.watchEmail = function() {
|
||||
// TODO: change this to SSE?
|
||||
$timeout(function() {
|
||||
$scope.checkEmailAuthorization();
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
$scope.cancelEmailAuth = function() {
|
||||
$scope.status = '';
|
||||
$('#authorizeEmailModal').modal('hide');
|
||||
};
|
||||
|
||||
$scope.getHelpUrl = function(field, config) {
|
||||
var helpUrl = field['help_url'];
|
||||
if (!helpUrl) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return StringBuilderService.buildUrl(helpUrl, config);
|
||||
};
|
||||
|
||||
$scope.$watch('defaultData', function(counter) {
|
||||
if ($scope.defaultData && $scope.defaultData['currentEvent']) {
|
||||
$scope.setEvent($scope.defaultData['currentEvent']);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
||||
|
||||
|
47
static/js/directives/ui/create-robot-dialog.js
Normal file
47
static/js/directives/ui/create-robot-dialog.js
Normal file
|
@ -0,0 +1,47 @@
|
|||
/**
|
||||
* An element which displays a dialog for creating a robot account.
|
||||
*/
|
||||
angular.module('quay').directive('createRobotDialog', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/create-robot-dialog.html',
|
||||
replace: false,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'info': '=info',
|
||||
'robotCreated': '&robotCreated'
|
||||
},
|
||||
controller: function($scope, $element, ApiService, UserService, NAME_PATTERNS) {
|
||||
$scope.ROBOT_PATTERN = NAME_PATTERNS.ROBOT_PATTERN;
|
||||
|
||||
$scope.robotFinished = function(robot) {
|
||||
$scope.robotCreated({'robot': robot});
|
||||
};
|
||||
|
||||
$scope.createRobot = function(name, description, callback) {
|
||||
var organization = $scope.info.namespace;
|
||||
if (!UserService.isOrganization(organization)) {
|
||||
organization = null;
|
||||
}
|
||||
|
||||
var params = {
|
||||
'robot_shortname': name
|
||||
};
|
||||
|
||||
var data = {
|
||||
'description': description || ''
|
||||
};
|
||||
|
||||
var errorDisplay = ApiService.errorDisplay('Cannot create robot account', function() {
|
||||
callback(null);
|
||||
});
|
||||
|
||||
ApiService.createRobot(organization, data, params).then(function(resp) {
|
||||
callback(resp);
|
||||
}, errorDisplay);
|
||||
};
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
49
static/js/directives/ui/create-team-dialog.js
Normal file
49
static/js/directives/ui/create-team-dialog.js
Normal file
|
@ -0,0 +1,49 @@
|
|||
/**
|
||||
* An element which displays a dialog for creating a team.
|
||||
*/
|
||||
angular.module('quay').directive('createTeamDialog', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/create-team-dialog.html',
|
||||
replace: false,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'info': '=info',
|
||||
'teamCreated': '&teamCreated'
|
||||
},
|
||||
controller: function($scope, $element, ApiService, UserService, NAME_PATTERNS) {
|
||||
$scope.TEAM_PATTERN = NAME_PATTERNS.TEAM_PATTERN;
|
||||
|
||||
$scope.teamFinished = function(team) {
|
||||
$scope.teamCreated({'team': team});
|
||||
};
|
||||
|
||||
$scope.createTeam = function(name, callback) {
|
||||
var data = {
|
||||
'name': name,
|
||||
'role': 'member'
|
||||
};
|
||||
|
||||
var params = {
|
||||
'orgname': $scope.info.namespace,
|
||||
'teamname': name
|
||||
};
|
||||
|
||||
var errorDisplay = ApiService.errorDisplay('Cannot create team', function() {
|
||||
callback(null);
|
||||
});
|
||||
|
||||
ApiService.updateOrganizationTeam(data, params).then(function(resp) {
|
||||
if (!resp.new_team) {
|
||||
callback(null);
|
||||
bootbox.alert('Team with name "' + resp.name + '" already exists')
|
||||
return;
|
||||
}
|
||||
callback(resp);
|
||||
}, errorDisplay);
|
||||
};
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
218
static/js/directives/ui/credentials-dialog.js
Normal file
218
static/js/directives/ui/credentials-dialog.js
Normal file
|
@ -0,0 +1,218 @@
|
|||
/**
|
||||
* An element which displays a credentials dialog.
|
||||
*/
|
||||
angular.module('quay').directive('credentialsDialog', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/credentials-dialog.html',
|
||||
replace: false,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'credentials': '=credentials',
|
||||
'secretTitle': '@secretTitle',
|
||||
'entityTitle': '@entityTitle',
|
||||
'entityIcon': '@entityIcon'
|
||||
},
|
||||
|
||||
controller: function($scope, $element, $rootScope, Config) {
|
||||
$scope.Config = Config;
|
||||
|
||||
$scope.k8s = {};
|
||||
$scope.rkt = {};
|
||||
$scope.docker = {};
|
||||
|
||||
$scope.$on('$destroy', function() {
|
||||
if ($scope.inBody) {
|
||||
document.body.removeChild($element[0]);
|
||||
}
|
||||
});
|
||||
|
||||
// Generate a unique ID for the dialog.
|
||||
if (!$rootScope.credentialsDialogCounter) {
|
||||
$rootScope.credentialsDialogCounter = 0;
|
||||
}
|
||||
|
||||
$rootScope.credentialsDialogCounter++;
|
||||
|
||||
$scope.hide = function() {
|
||||
$element.find('.modal').modal('hide');
|
||||
};
|
||||
|
||||
$scope.show = function() {
|
||||
$element.find('.modal').modal({});
|
||||
|
||||
// Move the dialog to the body to prevent it from being affected
|
||||
// by being placed inside other tables.
|
||||
$scope.inBody = true;
|
||||
document.body.appendChild($element[0]);
|
||||
};
|
||||
|
||||
$scope.$watch('credentials', function(credentials) {
|
||||
if (!credentials) {
|
||||
$scope.hide();
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.show();
|
||||
});
|
||||
|
||||
$scope.downloadFile = function(info) {
|
||||
var blob = new Blob([info.contents]);
|
||||
FileSaver.saveAs(blob, info.filename);
|
||||
};
|
||||
|
||||
$scope.viewFile = function(context) {
|
||||
context.viewingFile = true;
|
||||
};
|
||||
|
||||
$scope.isDownloadSupported = function() {
|
||||
var isSafari = /^((?!chrome).)*safari/i.test(navigator.userAgent);
|
||||
if (isSafari) {
|
||||
// Doesn't work properly in Safari, sadly.
|
||||
return false;
|
||||
}
|
||||
|
||||
try { return !!new Blob(); } catch(e) {}
|
||||
return false;
|
||||
};
|
||||
|
||||
$scope.getNamespace = function(credentials) {
|
||||
if (!credentials || !credentials.username) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (credentials.namespace) {
|
||||
return credentials.namespace;
|
||||
}
|
||||
|
||||
return credentials.username.split('+')[0];
|
||||
};
|
||||
|
||||
$scope.getMesosFilename = function(credentials) {
|
||||
return $scope.getSuffixedFilename(credentials, 'auth.tar.gz');
|
||||
};
|
||||
|
||||
$scope.getMesosFile = function(credentials) {
|
||||
var tarFile = new Tar();
|
||||
tarFile.append('.docker/config.json', $scope.getDockerConfig(credentials), {});
|
||||
contents = (new Zlib.Gzip(tarFile.getData())).compress();
|
||||
return {
|
||||
'filename': $scope.getMesosFilename(credentials),
|
||||
'contents': contents
|
||||
}
|
||||
};
|
||||
|
||||
$scope.getDockerConfig = function(credentials) {
|
||||
var auths = {};
|
||||
auths[Config['SERVER_HOSTNAME']] = {
|
||||
'auth': $.base64.encode(credentials.username + ":" + credentials.password),
|
||||
'email': ''
|
||||
};
|
||||
|
||||
var config = {
|
||||
'auths': auths
|
||||
};
|
||||
|
||||
return JSON.stringify(config, null, ' ');
|
||||
};
|
||||
|
||||
$scope.getDockerFile = function(credentials) {
|
||||
return {
|
||||
'filename': $scope.getRktFilename(credentials),
|
||||
'contents': $scope.getDockerConfig(credentials)
|
||||
}
|
||||
};
|
||||
|
||||
$scope.getDockerLogin = function(credentials) {
|
||||
if (!credentials || !credentials.username) {
|
||||
return '';
|
||||
}
|
||||
|
||||
var escape = function(v) {
|
||||
if (!v) { return v; }
|
||||
return v.replace('$', '\\$');
|
||||
};
|
||||
|
||||
return 'docker login -u="' + escape(credentials.username) + '" -p="' + credentials.password + '" ' + Config['SERVER_HOSTNAME'];
|
||||
};
|
||||
|
||||
$scope.getDockerFilename = function(credentials) {
|
||||
return $scope.getSuffixedFilename(credentials, 'auth.json')
|
||||
};
|
||||
|
||||
$scope.getRktFile = function(credentials) {
|
||||
var config = {
|
||||
'rktKind': 'auth',
|
||||
'rktVersion': 'v1',
|
||||
'domains': [Config['SERVER_HOSTNAME']],
|
||||
'type': 'basic',
|
||||
'credentials': {
|
||||
'user': credentials['username'],
|
||||
'password': credentials['password']
|
||||
}
|
||||
};
|
||||
|
||||
var contents = JSON.stringify(config, null, ' ');
|
||||
return {
|
||||
'filename': $scope.getRktFilename(credentials),
|
||||
'contents': contents
|
||||
}
|
||||
};
|
||||
|
||||
$scope.getRktFilename = function(credentials) {
|
||||
return $scope.getSuffixedFilename(credentials, 'auth.json')
|
||||
};
|
||||
|
||||
$scope.getKubernetesSecretName = function(credentials) {
|
||||
if (!credentials || !credentials.username) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $scope.getSuffixedFilename(credentials, 'pull-secret');
|
||||
};
|
||||
|
||||
$scope.getKubernetesFile = function(credentials) {
|
||||
var dockerConfigJson = $scope.getDockerConfig(credentials);
|
||||
var contents = 'apiVersion: v1\n' +
|
||||
'kind: Secret\n' +
|
||||
'metadata:\n' +
|
||||
' name: ' + $scope.getKubernetesSecretName(credentials) + '\n' +
|
||||
'data:\n' +
|
||||
' .dockerconfigjson: ' + $.base64.encode(dockerConfigJson) + '\n' +
|
||||
'type: kubernetes.io/dockerconfigjson'
|
||||
|
||||
return {
|
||||
'filename': $scope.getKubernetesFilename(credentials),
|
||||
'contents': contents
|
||||
}
|
||||
};
|
||||
|
||||
$scope.getKubernetesFilename = function(credentials) {
|
||||
return $scope.getSuffixedFilename(credentials, 'secret.yml')
|
||||
};
|
||||
|
||||
$scope.getEscaped = function(item) {
|
||||
var escaped = item.replace(/[^a-zA-Z0-9]/g, '-');
|
||||
if (escaped[0] == '-') {
|
||||
escaped = escaped.substr(1);
|
||||
}
|
||||
return escaped;
|
||||
};
|
||||
|
||||
$scope.getSuffixedFilename = function(credentials, suffix) {
|
||||
if (!credentials || !credentials.username) {
|
||||
return '';
|
||||
}
|
||||
|
||||
var prefix = $scope.getEscaped(credentials.username);
|
||||
if (credentials.title) {
|
||||
prefix = $scope.getEscaped(credentials.title);
|
||||
}
|
||||
|
||||
return prefix + '-' + suffix;
|
||||
};
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
18
static/js/directives/ui/credentials.js
Normal file
18
static/js/directives/ui/credentials.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
/**
|
||||
* An element which displays a credentials for a build trigger.
|
||||
*/
|
||||
angular.module('quay').directive('credentials', function() {
|
||||
var directiveDefinitionObject = {
|
||||
templateUrl: '/static/directives/credentials.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'trigger': '=trigger'
|
||||
},
|
||||
controller: function($scope, TriggerService) {
|
||||
TriggerService.populateTemplate($scope, 'credentials');
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
59
static/js/directives/ui/datetime-picker.js
Normal file
59
static/js/directives/ui/datetime-picker.js
Normal file
|
@ -0,0 +1,59 @@
|
|||
/**
|
||||
* An element which displays a datetime picker.
|
||||
*/
|
||||
angular.module('quay').directive('datetimePicker', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/datetime-picker.html',
|
||||
replace: false,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'datetime': '=datetime',
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
var datetimeSet = false;
|
||||
|
||||
$(function() {
|
||||
$element.find('input').datetimepicker({
|
||||
'format': 'LLL',
|
||||
'sideBySide': true,
|
||||
'showClear': true,
|
||||
'minDate': new Date(),
|
||||
'debug': false
|
||||
});
|
||||
|
||||
$element.find('input').on("dp.change", function (e) {
|
||||
$scope.$apply(function() {
|
||||
$scope.datetime = e.date ? e.date.unix() : null;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
$scope.$watch('selected_datetime', function(value) {
|
||||
if (!datetimeSet) { return; }
|
||||
|
||||
if (!value) {
|
||||
if ($scope.datetime) {
|
||||
$scope.datetime = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.datetime = (new Date(value)).getTime()/1000;
|
||||
});
|
||||
|
||||
$scope.$watch('datetime', function(value) {
|
||||
if (!value) {
|
||||
$scope.selected_datetime = null;
|
||||
datetimeSet = true;
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.selected_datetime = moment.unix(value).format('LLL');
|
||||
datetimeSet = true;
|
||||
});
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
35
static/js/directives/ui/delete-namespace-view.js
Normal file
35
static/js/directives/ui/delete-namespace-view.js
Normal file
|
@ -0,0 +1,35 @@
|
|||
/**
|
||||
* An element which displays a settings table row for deleting a namespace (user or organization).
|
||||
*/
|
||||
angular.module('quay').directive('deleteNamespaceView', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/delete-namespace-view.html',
|
||||
replace: false,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'user': '=user',
|
||||
'organization': '=organization',
|
||||
'subscriptionStatus': '=subscriptionStatus',
|
||||
'namespaceTitle': '@namespaceTitle'
|
||||
},
|
||||
controller: function($scope, $element, UserService) {
|
||||
$scope.context = {};
|
||||
|
||||
$scope.showDeleteNamespace = function() {
|
||||
$scope.deleteNamespaceInfo = {
|
||||
'user': $scope.user,
|
||||
'organization': $scope.organization,
|
||||
'namespace': $scope.user ? $scope.user.username : $scope.organization.name,
|
||||
'verification': ''
|
||||
};
|
||||
};
|
||||
|
||||
$scope.deleteNamespace = function(info, callback) {
|
||||
UserService.deleteNamespace(info, callback);
|
||||
};
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
26
static/js/directives/ui/delete-ui.js
Normal file
26
static/js/directives/ui/delete-ui.js
Normal file
|
@ -0,0 +1,26 @@
|
|||
/**
|
||||
* A two-step delete button that slides into view when clicked.
|
||||
*/
|
||||
angular.module('quay').directive('deleteUi', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/delete-ui.html',
|
||||
replace: false,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'deleteTitle': '=deleteTitle',
|
||||
'buttonTitle': '=buttonTitle',
|
||||
'performDelete': '&performDelete'
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
$scope.buttonTitleInternal = $scope.buttonTitle || 'Delete';
|
||||
|
||||
$element.children().attr('tabindex', 0);
|
||||
$scope.focus = function() {
|
||||
$element[0].firstChild.focus();
|
||||
};
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
79
static/js/directives/ui/dockerfile-build-dialog.js
Normal file
79
static/js/directives/ui/dockerfile-build-dialog.js
Normal file
|
@ -0,0 +1,79 @@
|
|||
/**
|
||||
* An element which displays a dialog for manually starting a dockerfile build.
|
||||
*/
|
||||
angular.module('quay').directive('dockerfileBuildDialog', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/dockerfile-build-dialog.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'repository': '=repository',
|
||||
'showNow': '=showNow',
|
||||
'buildStarted': '&buildStarted'
|
||||
},
|
||||
controller: function($scope, $element, ApiService) {
|
||||
$scope.viewTriggers = false;
|
||||
$scope.triggers = null;
|
||||
$scope.viewCounter = 0;
|
||||
|
||||
$scope.startTriggerCounter = 0;
|
||||
$scope.startTrigger = null;
|
||||
|
||||
$scope.showTriggers = function(value) {
|
||||
$scope.viewTriggers = value;
|
||||
};
|
||||
|
||||
$scope.runTriggerNow = function(trigger) {
|
||||
$element.find('.dockerfilebuildModal').modal('hide');
|
||||
$scope.startTrigger = trigger;
|
||||
$scope.startTriggerCounter++;
|
||||
};
|
||||
|
||||
$scope.startBuild = function() {
|
||||
$scope.buildStarting = true;
|
||||
$scope.startBuildCallback(function(status, messageOrBuild) {
|
||||
$element.find('.dockerfilebuildModal').modal('hide');
|
||||
if (status) {
|
||||
$scope.buildStarted({'build': messageOrBuild});
|
||||
} else {
|
||||
bootbox.alert(messageOrBuild || 'Could not start build');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.readyForBuild = function(startBuild) {
|
||||
$scope.startBuildCallback = startBuild;
|
||||
};
|
||||
|
||||
$scope.$watch('showNow', function(sn) {
|
||||
if (sn && $scope.repository) {
|
||||
$scope.viewTriggers = false;
|
||||
$scope.startTrigger = null;
|
||||
$scope.buildStarting = false;
|
||||
$scope.viewCounter++;
|
||||
|
||||
$element.find('.dockerfilebuildModal').modal({});
|
||||
|
||||
// Load the triggers (if necessary).
|
||||
if (!$scope.repository || !$scope.repository.can_admin) {
|
||||
$scope.triggersResource = null;
|
||||
$scope.triggers = null;
|
||||
return;
|
||||
}
|
||||
|
||||
var params = {
|
||||
'repository': $scope.repository.namespace + '/' + $scope.repository.name
|
||||
};
|
||||
|
||||
$scope.triggersResource = ApiService.listBuildTriggersAsResource(params).get(function(resp) {
|
||||
$scope.triggers = resp.triggers;
|
||||
$scope.viewTriggers = $scope.triggers.length > 0;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
143
static/js/directives/ui/dockerfile-build-form.js
Normal file
143
static/js/directives/ui/dockerfile-build-form.js
Normal file
|
@ -0,0 +1,143 @@
|
|||
/**
|
||||
* An element which displays a form for manually starting a dockerfile build.
|
||||
*/
|
||||
angular.module('quay').directive('dockerfileBuildForm', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/dockerfile-build-form.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'repository': '=repository',
|
||||
'isReady': '=?isReady',
|
||||
'reset': '=?reset',
|
||||
'readyForBuild': '&readyForBuild'
|
||||
},
|
||||
controller: function($scope, $element, ApiService, DockerfileService, Config) {
|
||||
$scope.state = 'empty';
|
||||
|
||||
var checkPrivateImage = function(baseImage) {
|
||||
var params = {
|
||||
'repository': baseImage
|
||||
};
|
||||
|
||||
$scope.state = 'checking-image';
|
||||
ApiService.getRepo(null, params).then(function(repository) {
|
||||
$scope.privateBaseRepository = repository.is_public ? null : baseImage;
|
||||
$scope.state = repository.is_public ? 'ready' : 'awaiting-bot';
|
||||
}, function() {
|
||||
$scope.privateBaseRepository = null;
|
||||
$scope.state = 'ready';
|
||||
});
|
||||
};
|
||||
|
||||
$scope.handleFilesSelected = function(files, opt_callback) {
|
||||
$scope.pullEntity = null;
|
||||
$scope.state = 'checking';
|
||||
$scope.selectedFiles = files;
|
||||
|
||||
DockerfileService.getDockerfile(files[0])
|
||||
.then(function(dockerfileInfo) {
|
||||
var baseImage = dockerfileInfo.getRegistryBaseImage();
|
||||
if (baseImage) {
|
||||
checkPrivateImage(baseImage);
|
||||
} else {
|
||||
$scope.state = 'ready';
|
||||
}
|
||||
|
||||
$scope.$apply(function() {
|
||||
opt_callback && opt_callback(true, 'Dockerfile found and valid')
|
||||
});
|
||||
})
|
||||
.catch(function(error) {
|
||||
$scope.state = 'empty';
|
||||
$scope.privateBaseRepository = null;
|
||||
|
||||
$scope.$apply(function() {
|
||||
opt_callback && opt_callback(false, error || 'Could not find valid Dockerfile');
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$scope.handleFilesCleared = function() {
|
||||
$scope.state = 'empty';
|
||||
$scope.pullEntity = null;
|
||||
$scope.privateBaseRepository = null;
|
||||
};
|
||||
|
||||
$scope.handleFilesValidated = function(uploadFiles) {
|
||||
$scope.uploadFilesCallback = uploadFiles;
|
||||
};
|
||||
|
||||
var requestRepoBuild = function(buildPackId, opt_callback) {
|
||||
var repo = $scope.repository;
|
||||
var data = {
|
||||
'file_id': buildPackId
|
||||
};
|
||||
|
||||
if ($scope.pullEntity) {
|
||||
data['pull_robot'] = $scope.pullEntity['name'];
|
||||
}
|
||||
|
||||
var params = {
|
||||
'repository': repo.namespace + '/' + repo.name,
|
||||
};
|
||||
|
||||
ApiService.requestRepoBuild(data, params).then(function(resp) {
|
||||
opt_callback && opt_callback(true, resp);
|
||||
}, function(resp) {
|
||||
opt_callback && opt_callback(false, 'Could not start build');
|
||||
$scope.handleFilesSelected($scope.selectedFiles);
|
||||
});
|
||||
};
|
||||
|
||||
var startBuild = function(opt_callback) {
|
||||
$scope.state = 'uploading-files';
|
||||
$scope.uploadFilesCallback(function(status, messageOrIds) {
|
||||
$scope.state = 'starting-build';
|
||||
requestRepoBuild(messageOrIds[0], opt_callback);
|
||||
});
|
||||
};
|
||||
|
||||
var checkEntity = function() {
|
||||
if (!$scope.pullEntity) {
|
||||
$scope.state = 'awaiting-bot';
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.state = 'checking-bot';
|
||||
$scope.currentRobotHasPermission = null;
|
||||
|
||||
var permParams = {
|
||||
'repository': $scope.privateBaseRepository,
|
||||
'username': $scope.pullEntity.name
|
||||
};
|
||||
|
||||
ApiService.getUserTransitivePermission(null, permParams).then(function(resp) {
|
||||
$scope.currentRobotHasPermission = resp['permissions'].length > 0;
|
||||
$scope.state = $scope.currentRobotHasPermission ? 'ready' : 'perm-error';
|
||||
});
|
||||
};
|
||||
|
||||
$scope.$watch('pullEntity', checkEntity);
|
||||
$scope.$watch('reset', function(reset) {
|
||||
if (reset) {
|
||||
$scope.state = 'empty';
|
||||
$scope.pullEntity = null;
|
||||
$scope.privateBaseRepository = null;
|
||||
}
|
||||
});
|
||||
|
||||
$scope.$watch('state', function(state) {
|
||||
$scope.isReady = state == 'ready';
|
||||
if ($scope.isReady) {
|
||||
$scope.readyForBuild({
|
||||
'startBuild': startBuild
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
|
@ -0,0 +1,60 @@
|
|||
.label.FROM {
|
||||
border-color: #5bc0de !important;
|
||||
}
|
||||
|
||||
.label.ARG {
|
||||
border-color: #eaef4d !important;
|
||||
}
|
||||
|
||||
.label.ONBUILD {
|
||||
border-color: #6813d8 !important;
|
||||
}
|
||||
|
||||
.label.CMD, .label.EXPOSE, .label.ENTRYPOINT {
|
||||
border-color: #428bca !important;
|
||||
}
|
||||
|
||||
.label.RUN, .label.ADD, .label.COPY {
|
||||
border-color: #5cb85c !important;
|
||||
}
|
||||
|
||||
.label.ENV, .label.VOLUME, .label.USER, .label.WORKDIR, .label.HEALTHCHECK, .label.STOPSIGNAL, .label.SHELL {
|
||||
border-color: #f0ad4e !important;
|
||||
}
|
||||
|
||||
.label.MAINTAINER {
|
||||
border-color: #aaa !important;
|
||||
}
|
||||
|
||||
.dockerfile-command {
|
||||
display: block;
|
||||
position: relative;
|
||||
padding-left: 96px;
|
||||
}
|
||||
|
||||
.dockerfile-command .command-title {
|
||||
font-family: Consolas, "Lucida Console", Monaco, monospace !important;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.dockerfile-command .command-title a {
|
||||
color: #5bc0de;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.dockerfile-command .label {
|
||||
color: white;
|
||||
|
||||
padding-top: 4px;
|
||||
text-align: right;
|
||||
margin-right: 4px;
|
||||
width: 86px;
|
||||
display: inline-block;
|
||||
|
||||
border-right: 4px solid #aaa;
|
||||
background-color: #333;
|
||||
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 0px;
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
<span class="dockerfile-command dockerfile-command-element">
|
||||
<span class="label" ng-class="::$ctrl.getCommandKind($ctrl.command)"
|
||||
ng-if="::$ctrl.getCommandKind($ctrl.command)">
|
||||
{{ ::$ctrl.getCommandKind($ctrl.command) }}
|
||||
</span>
|
||||
<span class="command-title" ng-bind-html="::$ctrl.getCommandTitleHtml($ctrl.command)"></span>
|
||||
</span>
|
|
@ -0,0 +1,75 @@
|
|||
import { Input, Component, Inject } from 'ng-metadata/core';
|
||||
import './dockerfile-command.component.css';
|
||||
|
||||
/**
|
||||
* A component which displays a Dockerfile command, nicely formatted.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'dockerfile-command',
|
||||
templateUrl: '/static/js/directives/ui/dockerfile-command/dockerfile-command.component.html',
|
||||
})
|
||||
export class DockerfileCommandComponent {
|
||||
@Input('<') public command: string;
|
||||
|
||||
private registryHandlers: {[domain: string]: Function};
|
||||
|
||||
constructor (@Inject('Config') Config: any, @Inject('UtilService') private utilService: any) {
|
||||
var registryHandlers = {
|
||||
'quay.io': function(pieces) {
|
||||
var rnamespace = pieces[pieces.length - 2];
|
||||
var rname = pieces[pieces.length - 1].split(':')[0];
|
||||
return '/repository/' + rnamespace + '/' + rname + '/';
|
||||
},
|
||||
|
||||
'': function(pieces) {
|
||||
var rnamespace = pieces.length == 1 ? '_' : 'u/' + pieces[0];
|
||||
var rname = pieces[pieces.length - 1].split(':')[0];
|
||||
return 'https://registry.hub.docker.com/' + rnamespace + '/' + rname + '/';
|
||||
}
|
||||
};
|
||||
|
||||
registryHandlers[Config.getDomain()] = registryHandlers['quay.io'];
|
||||
this.registryHandlers = registryHandlers;
|
||||
}
|
||||
|
||||
private getCommandKind(command: string): string {
|
||||
command = command.trim();
|
||||
if (!command) { return ''; }
|
||||
|
||||
var space = command.indexOf(' ');
|
||||
return command.substring(0, space);
|
||||
}
|
||||
|
||||
private getCommandTitleHtml(command: string): string {
|
||||
command = command.trim();
|
||||
if (!command) { return ''; }
|
||||
|
||||
var kindHandlers = {
|
||||
'FROM': (command) => {
|
||||
var parts = command.split(' ');
|
||||
var pieces = parts[0].split('/');
|
||||
var registry = pieces.length < 3 ? '' : pieces[0];
|
||||
if (!this.registryHandlers[registry]) {
|
||||
return command;
|
||||
}
|
||||
|
||||
return '<a href="' + this.registryHandlers[registry](pieces) + '" target="_blank">' + parts[0] + '</a> ' + (parts.splice(1).join(' '));
|
||||
}
|
||||
};
|
||||
|
||||
var space = command.indexOf(' ');
|
||||
if (space <= 0) {
|
||||
return this.utilService.textToSafeHtml(command);
|
||||
}
|
||||
|
||||
var kind = this.getCommandKind(command);
|
||||
var sanitized = this.utilService.textToSafeHtml(command.substring(space + 1));
|
||||
|
||||
var handler = kindHandlers[kind || ''];
|
||||
if (handler) {
|
||||
return handler(sanitized);
|
||||
} else {
|
||||
return sanitized;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
<div class="dockerfile-path-select-element">
|
||||
<div class="dropdown-select"
|
||||
placeholder="'Enter path containing a Dockerfile'"
|
||||
selected-item="$ctrl.selectedPath"
|
||||
lookahead-items="$ctrl.paths"
|
||||
handle-input="$ctrl.setPath(input)"
|
||||
handle-item-selected="$ctrl.setSelectedPath(datum.value)"
|
||||
allow-custom-input="true"
|
||||
hide-dropdown="!$ctrl.supportsFullListing">
|
||||
<!-- Icons -->
|
||||
<i class="dropdown-select-icon none-icon fa fa-folder-o fa-lg"
|
||||
ng-show="$ctrl.isUnknownPath"></i>
|
||||
<i class="dropdown-select-icon none-icon fa fa-folder fa-lg" style="color: black;"
|
||||
ng-show="!$ctrl.isUnknownPath"></i>
|
||||
<i class="dropdown-select-icon fa fa-folder fa-lg"></i>
|
||||
|
||||
<!-- Dropdown menu -->
|
||||
<ul class="dropdown-select-menu pull-right" role="menu">
|
||||
<li ng-repeat="path in $ctrl.paths">
|
||||
<a ng-click="$ctrl.setSelectedPath(path)"
|
||||
ng-if="path">
|
||||
<i class="fa fa-folder fa-lg"></i> {{ path }}
|
||||
</a>
|
||||
</li>
|
||||
<li class="dropdown-header" role="presentation"
|
||||
ng-show="!$ctrl.paths.length">
|
||||
No Dockerfiles found in repository
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div style="padding: 10px">
|
||||
<div class="co-alert co-alert-danger"
|
||||
ng-show="!$ctrl.isValidPath && $ctrl.currentPath">
|
||||
Path entered for folder containing Dockerfile is invalid: Must start with a '/'.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,110 @@
|
|||
import { DockerfilePathSelectComponent, PathChangeEvent } from './dockerfile-path-select.component';
|
||||
|
||||
|
||||
describe("DockerfilePathSelectComponent", () => {
|
||||
var component: DockerfilePathSelectComponent;
|
||||
var currentPath: string;
|
||||
var isValidPath: boolean;
|
||||
var paths: string[];
|
||||
var supportsFullListing: boolean;
|
||||
|
||||
beforeEach(() => {
|
||||
component = new DockerfilePathSelectComponent();
|
||||
currentPath = '/';
|
||||
isValidPath = false;
|
||||
paths = ['/'];
|
||||
supportsFullListing = true;
|
||||
component.currentPath = currentPath;
|
||||
component.isValidPath = isValidPath;
|
||||
component.paths = paths;
|
||||
component.supportsFullListing = supportsFullListing;
|
||||
});
|
||||
|
||||
describe("ngOnChanges", () => {
|
||||
|
||||
it("sets valid path flag to true if current path is valid", () => {
|
||||
component.ngOnChanges({});
|
||||
|
||||
expect(component.isValidPath).toBe(true);
|
||||
});
|
||||
|
||||
it("sets valid path flag to false if current path is invalid", () => {
|
||||
component.currentPath = "asdfdsf";
|
||||
component.ngOnChanges({});
|
||||
|
||||
expect(component.isValidPath).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("setPath", () => {
|
||||
var newPath: string;
|
||||
|
||||
beforeEach(() => {
|
||||
newPath = '/conf';
|
||||
});
|
||||
|
||||
it("sets current path to given path", () => {
|
||||
component.setPath(newPath);
|
||||
|
||||
expect(component.currentPath).toEqual(newPath);
|
||||
});
|
||||
|
||||
it("sets valid path flag to true if given path is valid", () => {
|
||||
component.setPath(newPath);
|
||||
|
||||
expect(component.isValidPath).toBe(true);
|
||||
});
|
||||
|
||||
it("sets valid path flag to false if given path is invalid", () => {
|
||||
component.setPath("asdfsadfs");
|
||||
|
||||
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", () => {
|
||||
var newPath: string;
|
||||
|
||||
beforeEach(() => {
|
||||
newPath = '/conf';
|
||||
});
|
||||
|
||||
it("sets current path to given path", () => {
|
||||
component.setSelectedPath(newPath);
|
||||
|
||||
expect(component.currentPath).toEqual(newPath);
|
||||
});
|
||||
|
||||
it("sets valid path flag to true if given path is valid", () => {
|
||||
component.setSelectedPath(newPath);
|
||||
|
||||
expect(component.isValidPath).toBe(true);
|
||||
});
|
||||
|
||||
it("sets valid path flag to false if given path is invalid", () => {
|
||||
component.setSelectedPath("a;lskjdf;ldsa");
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,60 @@
|
|||
import { Input, Output, EventEmitter, Component, OnChanges, SimpleChanges } from 'ng-metadata/core';
|
||||
|
||||
|
||||
/**
|
||||
* A component that allows the user to select the location of the Dockerfile in their source code repository.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'dockerfile-path-select',
|
||||
templateUrl: '/static/js/directives/ui/dockerfile-path-select/dockerfile-path-select.component.html'
|
||||
})
|
||||
export class DockerfilePathSelectComponent implements OnChanges {
|
||||
|
||||
@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;
|
||||
|
||||
public ngOnChanges(changes: SimpleChanges): void {
|
||||
this.isValidPath = this.checkPath(this.currentPath, this.paths, this.supportsFullListing);
|
||||
}
|
||||
|
||||
public setPath(path: string): void {
|
||||
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 {
|
||||
this.isUnknownPath = false;
|
||||
var isValidPath: boolean = false;
|
||||
|
||||
if (path != null && path.length > 0 && path[0] === '/') {
|
||||
isValidPath = true;
|
||||
this.isUnknownPath = supportsFullListing && paths.indexOf(path) < 0;
|
||||
}
|
||||
return isValidPath;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Dockerfile path changed event.
|
||||
*/
|
||||
export type PathChangeEvent = {
|
||||
path: string;
|
||||
isValid: boolean;
|
||||
};
|
94
static/js/directives/ui/donut-chart.js
Normal file
94
static/js/directives/ui/donut-chart.js
Normal file
|
@ -0,0 +1,94 @@
|
|||
/**
|
||||
* An element which displays a donut chart of data.
|
||||
*/
|
||||
angular.module('quay').directive('donutChart', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/donut-chart.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'width': '@width',
|
||||
'minPercent': '@minPercent',
|
||||
'data': '=data',
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
$scope.created = false;
|
||||
|
||||
// Based on: http://bl.ocks.org/erichoco/6694616
|
||||
var chart = d3.select($element.find('.donut-chart-element')[0]);
|
||||
|
||||
var renderChart = function() {
|
||||
if (!$scope.data || !$scope.data.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Adjust the data to make sure each non-zero entry is at minimum. While technically
|
||||
// not accurate, it will make the section visible to the user.
|
||||
var adjustedData = []
|
||||
var total = 0;
|
||||
$scope.data.map(function(entry) {
|
||||
total += entry.value;
|
||||
});
|
||||
|
||||
adjustedData = $scope.data.map(function(entry) {
|
||||
var value = entry.value;
|
||||
if ($scope.minPercent) {
|
||||
if (value / total < $scope.minPercent / 100) {
|
||||
value = total * $scope.minPercent / 100;
|
||||
}
|
||||
}
|
||||
|
||||
var copy = $.extend({}, entry);
|
||||
copy.value = value;
|
||||
return copy;
|
||||
});
|
||||
|
||||
|
||||
var $chart = $element.find('.donut-chart-element');
|
||||
$chart.empty();
|
||||
|
||||
var width = $scope.width * 1;
|
||||
var chart_m = width / 2 * 0.14;
|
||||
var chart_r = width / 2 * 0.85;
|
||||
|
||||
var topG = chart.append('svg:svg')
|
||||
.attr('width', (chart_r + chart_m) * 2)
|
||||
.attr('height', (chart_r + chart_m) * 2)
|
||||
.append('svg:g')
|
||||
.attr('class', 'donut')
|
||||
.attr('transform', 'translate(' + (chart_r + chart_m) + ',' +
|
||||
(chart_r + chart_m) + ')');
|
||||
|
||||
|
||||
var arc = d3.svg.arc()
|
||||
.innerRadius(chart_r * 0.6)
|
||||
.outerRadius(function(d, i) {
|
||||
return i == adjustedData.length - 1 ? chart_r * 1.2 : chart_r * 1;
|
||||
});
|
||||
|
||||
var pie = d3.layout.pie()
|
||||
.sort(null)
|
||||
.value(function(d) {
|
||||
return d.value;
|
||||
});
|
||||
|
||||
var reversed = adjustedData.reverse();
|
||||
var g = topG.selectAll(".arc")
|
||||
.data(pie(reversed))
|
||||
.enter().append("g")
|
||||
.attr("class", "arc");
|
||||
|
||||
g.append("path")
|
||||
.attr("d", arc)
|
||||
.style('stroke', '#fff')
|
||||
.style("fill", function(d) { return d.data.color; });
|
||||
|
||||
};
|
||||
|
||||
$scope.$watch('data', renderChart);
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
118
static/js/directives/ui/dropdown-select-direct.js
Normal file
118
static/js/directives/ui/dropdown-select-direct.js
Normal file
|
@ -0,0 +1,118 @@
|
|||
/**
|
||||
* An element which displays a dropdown select box which is (optionally) editable. This box
|
||||
* is displayed with an <input> and a menu on the right.
|
||||
*/
|
||||
angular.module('quay').directive('dropdownSelectDirect', function ($compile) {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/dropdown-select-direct.html',
|
||||
replace: true,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'selectedItem': '=selectedItem',
|
||||
|
||||
'placeholder': '=placeholder',
|
||||
'items': '=items',
|
||||
'iconMap': '=iconMap',
|
||||
|
||||
'valueKey': '@valueKey',
|
||||
'titleKey': '@titleKey',
|
||||
'iconKey': '@iconKey',
|
||||
|
||||
'noneIcon': '@noneIcon',
|
||||
|
||||
'clearValue': '=clearValue'
|
||||
},
|
||||
controller: function($scope, $element, $rootScope) {
|
||||
if (!$rootScope.__dropdownSelectCounter) {
|
||||
$rootScope.__dropdownSelectCounter = 1;
|
||||
}
|
||||
|
||||
$scope.placeholder = $scope.placeholder || '';
|
||||
$scope.internalItem = null;
|
||||
|
||||
// Setup lookahead.
|
||||
var input = $($element).find('.lookahead-input');
|
||||
|
||||
$scope.setItem = function(item) {
|
||||
$scope.selectedItem = item;
|
||||
};
|
||||
|
||||
$scope.$watch('clearValue', function(cv) {
|
||||
if (cv) {
|
||||
$scope.selectedItem = null;
|
||||
$(input).val('');
|
||||
}
|
||||
});
|
||||
|
||||
$scope.$watch('selectedItem', function(item) {
|
||||
if ($scope.selectedItem == $scope.internalItem) {
|
||||
// The item has already been set due to an internal action.
|
||||
return;
|
||||
}
|
||||
|
||||
if ($scope.selectedItem != null) {
|
||||
$(input).val(item[$scope.valueKey]);
|
||||
} else {
|
||||
$(input).val('');
|
||||
}
|
||||
});
|
||||
|
||||
$scope.$watch('items', function(items) {
|
||||
$(input).off();
|
||||
if (!items || !$scope.valueKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
var formattedItems = [];
|
||||
for (var i = 0; i < items.length; ++i) {
|
||||
var currentItem = items[i];
|
||||
var formattedItem = {
|
||||
'value': currentItem[$scope.valueKey],
|
||||
'item': currentItem
|
||||
};
|
||||
|
||||
formattedItems.push(formattedItem);
|
||||
}
|
||||
|
||||
var dropdownHound = new Bloodhound({
|
||||
name: 'dropdown-items-' + $rootScope.__dropdownSelectCounter,
|
||||
local: formattedItems,
|
||||
datumTokenizer: function(d) {
|
||||
return Bloodhound.tokenizers.whitespace(d.val || d.value || '');
|
||||
},
|
||||
queryTokenizer: Bloodhound.tokenizers.whitespace
|
||||
});
|
||||
dropdownHound.initialize();
|
||||
|
||||
$(input).typeahead({}, {
|
||||
source: dropdownHound.ttAdapter(),
|
||||
templates: {
|
||||
'suggestion': function (datum) {
|
||||
template = datum['template'] ? datum['template'](datum) : datum['value'];
|
||||
return template;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$(input).on('input', function(e) {
|
||||
$scope.$apply(function() {
|
||||
$scope.internalItem = null;
|
||||
$scope.selectedItem = null;
|
||||
});
|
||||
});
|
||||
|
||||
$(input).on('typeahead:selected', function(e, datum) {
|
||||
$scope.$apply(function() {
|
||||
$scope.internalItem = datum['item'];
|
||||
$scope.selectedItem = datum['item'];
|
||||
});
|
||||
});
|
||||
|
||||
$rootScope.__dropdownSelectCounter++;
|
||||
});
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
170
static/js/directives/ui/dropdown-select.js
Normal file
170
static/js/directives/ui/dropdown-select.js
Normal file
|
@ -0,0 +1,170 @@
|
|||
/**
|
||||
* An element which displays a dropdown select box which is (optionally) editable. This box
|
||||
* is displayed with an <input> and a menu on the right.
|
||||
*/
|
||||
angular.module('quay').directive('dropdownSelect', function ($compile) {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/dropdown-select.html',
|
||||
replace: true,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'selectedItem': '=selectedItem',
|
||||
'placeholder': '=placeholder',
|
||||
'lookaheadItems': '=lookaheadItems',
|
||||
'hideDropdown': '=hideDropdown',
|
||||
|
||||
'allowCustomInput': '@allowCustomInput',
|
||||
|
||||
'handleItemSelected': '&handleItemSelected',
|
||||
'handleInput': '&handleInput',
|
||||
|
||||
'clearValue': '=clearValue'
|
||||
},
|
||||
controller: function($scope, $element, $rootScope) {
|
||||
if (!$rootScope.__dropdownSelectCounter) {
|
||||
$rootScope.__dropdownSelectCounter = 1;
|
||||
}
|
||||
|
||||
$scope.placeholder = $scope.placeholder || '';
|
||||
$scope.lookaheadSetup = false;
|
||||
|
||||
// Setup lookahead.
|
||||
var input = $($element).find('.lookahead-input');
|
||||
|
||||
$scope.$watch('clearValue', function(cv) {
|
||||
if (cv && $scope.lookaheadSetup) {
|
||||
$scope.selectedItem = null;
|
||||
$(input).typeahead('val', '');
|
||||
$(input).typeahead('close');
|
||||
}
|
||||
});
|
||||
|
||||
$scope.$watch('selectedItem', function(item) {
|
||||
if (item != null && $scope.lookaheadSetup) {
|
||||
$(input).typeahead('val', item.toString());
|
||||
$(input).typeahead('close');
|
||||
}
|
||||
});
|
||||
|
||||
$scope.$watch('lookaheadItems', function(items) {
|
||||
$(input).off();
|
||||
items = items || [];
|
||||
|
||||
var formattedItems = [];
|
||||
for (var i = 0; i < items.length; ++i) {
|
||||
var formattedItem = items[i];
|
||||
if (typeof formattedItem == 'string') {
|
||||
formattedItem = {
|
||||
'value': formattedItem
|
||||
};
|
||||
}
|
||||
formattedItems.push(formattedItem);
|
||||
}
|
||||
|
||||
var dropdownHound = new Bloodhound({
|
||||
name: 'dropdown-items-' + $rootScope.__dropdownSelectCounter,
|
||||
local: formattedItems,
|
||||
datumTokenizer: function(d) {
|
||||
return Bloodhound.tokenizers.whitespace(d.val || d.value || '');
|
||||
},
|
||||
queryTokenizer: Bloodhound.tokenizers.whitespace
|
||||
});
|
||||
dropdownHound.initialize();
|
||||
|
||||
$(input).typeahead({
|
||||
'hint': false,
|
||||
'highlight': false
|
||||
}, {
|
||||
source: dropdownHound.ttAdapter(),
|
||||
templates: {
|
||||
'suggestion': function (datum) {
|
||||
template = datum['template'] ? datum['template'](datum) : '<span>' + datum['value'] + '</span>';
|
||||
return template;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$(input).on('input', function(e) {
|
||||
$scope.$apply(function() {
|
||||
$scope.selectedItem = null;
|
||||
if ($scope.handleInput) {
|
||||
$scope.handleInput({'input': $(input).val()});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$(input).on('typeahead:selected', function(e, datum) {
|
||||
$scope.$apply(function() {
|
||||
$scope.selectedItem = datum['item'] || datum['value'];
|
||||
if ($scope.handleItemSelected) {
|
||||
$scope.handleItemSelected({'datum': datum});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$rootScope.__dropdownSelectCounter++;
|
||||
$scope.lookaheadSetup = true;
|
||||
});
|
||||
},
|
||||
link: function(scope, element, attrs) {
|
||||
var transcludedBlock = element.find('div.transcluded');
|
||||
var transcludedElements = transcludedBlock.children();
|
||||
|
||||
var iconContainer = element.find('div.dropdown-select-icon-transclude');
|
||||
var menuContainer = element.find('div.dropdown-select-menu-transclude');
|
||||
|
||||
angular.forEach(transcludedElements, function(elem) {
|
||||
if (angular.element(elem).hasClass('dropdown-select-icon')) {
|
||||
iconContainer.append(elem);
|
||||
} else if (angular.element(elem).hasClass('dropdown-select-menu')) {
|
||||
menuContainer.replaceWith(elem);
|
||||
}
|
||||
});
|
||||
|
||||
transcludedBlock.remove();
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* An icon in the dropdown select. Only one icon will be displayed at a time.
|
||||
*/
|
||||
angular.module('quay').directive('dropdownSelectIcon', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 1,
|
||||
require: '^dropdownSelect',
|
||||
templateUrl: '/static/directives/dropdown-select-icon.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* The menu for the dropdown select.
|
||||
*/
|
||||
angular.module('quay').directive('dropdownSelectMenu', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 1,
|
||||
require: '^dropdownSelect',
|
||||
templateUrl: '/static/directives/dropdown-select-menu.html',
|
||||
replace: true,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
|
@ -0,0 +1,4 @@
|
|||
<div class="duration-input-element">
|
||||
<range-slider ng-range-min="$ctrl.min_s" ng-range-max="$ctrl.max_s" ng-model="$ctrl.seconds"></range-slider>
|
||||
<span class="duration-explanation">{{ $ctrl.durationExplanation($ctrl.seconds) }}</span>
|
||||
</div>
|
|
@ -0,0 +1,59 @@
|
|||
import { Input, Component, Inject } from 'ng-metadata/core';
|
||||
import * as moment from "moment";
|
||||
|
||||
|
||||
/**
|
||||
* A component that allows for selecting a time duration.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'duration-input',
|
||||
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;
|
||||
@Input('=?') public seconds: number;
|
||||
|
||||
private min_s: number;
|
||||
private max_s: number;
|
||||
|
||||
constructor(@Inject('$scope') private $scope: ng.IScope) {
|
||||
|
||||
}
|
||||
|
||||
public $onInit(): void {
|
||||
// TODO: replace this.
|
||||
this.$scope.$watch(() => this.seconds, this.updateValue.bind(this));
|
||||
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
public $onChanges(changes: ng.IOnChangesObject): void {
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
private updateValue(): void {
|
||||
this.value = `${this.seconds}s`;
|
||||
}
|
||||
|
||||
private refresh(): void {
|
||||
this.min_s = this.toSeconds(this.min || '0s');
|
||||
this.max_s = this.toSeconds(this.max || '1h');
|
||||
|
||||
if (this.value) {
|
||||
this.seconds = this.toSeconds(this.value || '0s');
|
||||
}
|
||||
}
|
||||
|
||||
private durationExplanation(durationSeconds: string): string {
|
||||
return moment.duration(parseInt(durationSeconds), 's').humanize();
|
||||
}
|
||||
|
||||
private toSeconds(durationStr: string): number {
|
||||
var number = durationStr.substring(0, durationStr.length - 1);
|
||||
var suffix = durationStr.substring(durationStr.length - 1);
|
||||
return moment.duration(parseInt(number), <moment.unitOfTime.Base>suffix).asSeconds();
|
||||
}
|
||||
}
|
60
static/js/directives/ui/entity-reference.js
Normal file
60
static/js/directives/ui/entity-reference.js
Normal file
|
@ -0,0 +1,60 @@
|
|||
/**
|
||||
* An element which shows an icon and a name/title for an entity (user, org, robot, team),
|
||||
* optionally linking to that entity if applicable.
|
||||
*/
|
||||
angular.module('quay').directive('entityReference', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/entity-reference.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'entity': '=entity',
|
||||
'namespace': '=namespace',
|
||||
'showAvatar': '@showAvatar',
|
||||
'avatarSize': '@avatarSize'
|
||||
},
|
||||
controller: function($scope, $element, UserService, UtilService, Config) {
|
||||
$scope.robotToShow = null;
|
||||
|
||||
$scope.getIsAdmin = function(namespace) {
|
||||
return UserService.isNamespaceAdmin(namespace);
|
||||
};
|
||||
|
||||
$scope.getTitle = function(entity) {
|
||||
if (!entity) { return ''; }
|
||||
|
||||
switch (entity.kind) {
|
||||
case 'org':
|
||||
return 'Organization';
|
||||
|
||||
case 'team':
|
||||
return 'Team';
|
||||
|
||||
case 'user':
|
||||
return entity.is_robot ? 'Robot Account' : 'User';
|
||||
}
|
||||
};
|
||||
|
||||
$scope.getPrefix = function(name) {
|
||||
if (!name) { return ''; }
|
||||
var plus = name.indexOf('+');
|
||||
return name.substr(0, plus);
|
||||
};
|
||||
|
||||
$scope.getShortenedName = function(name) {
|
||||
if (!name) { return ''; }
|
||||
var plus = name.indexOf('+');
|
||||
return name.substr(plus + 1);
|
||||
};
|
||||
|
||||
$scope.showRobotCredentials = function() {
|
||||
$scope.robotToShow = {
|
||||
'name': $scope.entity.name
|
||||
};
|
||||
};
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
432
static/js/directives/ui/entity-search.js
Normal file
432
static/js/directives/ui/entity-search.js
Normal file
|
@ -0,0 +1,432 @@
|
|||
|
||||
/**
|
||||
* An element which displays a box to search for an entity (org, user, robot, team). This control
|
||||
* allows for filtering of the entities found and whether to allow selection by e-mail.
|
||||
*/
|
||||
angular.module('quay').directive('entitySearch', function () {
|
||||
var number = 0;
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/entity-search.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
require: '?ngModel',
|
||||
link: function(scope, element, attr, ctrl) {
|
||||
scope.ngModel = ctrl;
|
||||
},
|
||||
scope: {
|
||||
'namespace': '=namespace',
|
||||
'placeholder': '=placeholder',
|
||||
'forRepository': '=forRepository',
|
||||
'skipPermissions': '=skipPermissions',
|
||||
|
||||
// Default: ['user', 'team', 'robot']
|
||||
'allowedEntities': '=allowedEntities',
|
||||
|
||||
'currentEntity': '=currentEntity',
|
||||
|
||||
'entitySelected': '&entitySelected',
|
||||
'emailSelected': '&emailSelected',
|
||||
|
||||
// When set to true, the contents of the control will be cleared as soon
|
||||
// as an entity is selected.
|
||||
'autoClear': '=autoClear',
|
||||
|
||||
// Set this property to immediately clear the contents of the control.
|
||||
'clearValue': '=clearValue',
|
||||
|
||||
// Whether e-mail addresses are allowed.
|
||||
'allowEmails': '=allowEmails',
|
||||
'emailMessage': '@emailMessage',
|
||||
|
||||
// True if the menu should pull right.
|
||||
'pullRight': '@pullRight'
|
||||
},
|
||||
controller: function($rootScope, $scope, $element, Restangular, UserService, ApiService, UtilService, AvatarService, Config, StateService) {
|
||||
$scope.inReadOnlyMode = StateService.inReadOnlyMode();
|
||||
$scope.requiresLazyLoading = true;
|
||||
$scope.isLazyLoading = false;
|
||||
$scope.userRequestedLazyLoading = false;
|
||||
|
||||
$scope.teams = null;
|
||||
$scope.page = {};
|
||||
$scope.page.robots = null;
|
||||
|
||||
$scope.isAdmin = false;
|
||||
$scope.isOrganization = false;
|
||||
|
||||
$scope.includeTeams = true;
|
||||
$scope.includeRobots = true;
|
||||
$scope.includeOrgs = false;
|
||||
|
||||
$scope.currentEntityInternal = $scope.currentEntity;
|
||||
$scope.createRobotInfo = null;
|
||||
$scope.createTeamInfo = null;
|
||||
|
||||
$scope.Config = Config;
|
||||
|
||||
var isSupported = function(kind, opt_array) {
|
||||
return $.inArray(kind, opt_array || $scope.allowedEntities || ['user', 'team', 'robot']) >= 0;
|
||||
};
|
||||
|
||||
var resetCache = function() {
|
||||
$scope.requiresLazyLoading = true;
|
||||
|
||||
$scope.teams = null;
|
||||
$scope.page.robots = null;
|
||||
};
|
||||
|
||||
$scope.lazyLoad = function() {
|
||||
$scope.userRequestedLazyLoading = true;
|
||||
$scope.checkLazyLoad();
|
||||
};
|
||||
|
||||
$scope.checkLazyLoad = function() {
|
||||
if (!$scope.namespace || !$scope.thisUser || !$scope.requiresLazyLoading ||
|
||||
$scope.isLazyLoading || !$scope.userRequestedLazyLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.isLazyLoading = true;
|
||||
$scope.isAdmin = UserService.isNamespaceAdmin($scope.namespace);
|
||||
$scope.isOrganization = !!UserService.getOrganization($scope.namespace);
|
||||
|
||||
// Reset the cached teams and robots, just to be sure.
|
||||
$scope.teams = null;
|
||||
$scope.page.robots = null;
|
||||
|
||||
var requiredOperations = 0;
|
||||
var operationComplete = function() {
|
||||
requiredOperations--;
|
||||
if (requiredOperations <= 0) {
|
||||
$scope.isLazyLoading = false;
|
||||
$scope.requiresLazyLoading = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Load the organization's teams (if applicable).
|
||||
if ($scope.isOrganization && isSupported('team')) {
|
||||
requiredOperations++;
|
||||
|
||||
// Note: We load the org here directly so that we always have the fully up-to-date
|
||||
// teams list.
|
||||
ApiService.getOrganization(null, {'orgname': $scope.namespace}).then(function(resp) {
|
||||
$scope.teams = Object.keys(resp.teams).map(function(key) {
|
||||
return resp.teams[key];
|
||||
});
|
||||
operationComplete();
|
||||
}, operationComplete);
|
||||
}
|
||||
|
||||
// Load the user/organization's robots (if applicable).
|
||||
if ($scope.isAdmin && isSupported('robot')) {
|
||||
requiredOperations++;
|
||||
var params = {
|
||||
'token': false,
|
||||
'limit': 20
|
||||
};
|
||||
|
||||
ApiService.getRobots($scope.isOrganization ? $scope.namespace : null, null, params).then(function(resp) {
|
||||
$scope.page.robots = resp.robots;
|
||||
operationComplete();
|
||||
}, operationComplete);
|
||||
}
|
||||
|
||||
if (requiredOperations == 0) {
|
||||
operationComplete();
|
||||
}
|
||||
};
|
||||
|
||||
$scope.askCreateTeam = function() {
|
||||
$scope.createTeamInfo = {
|
||||
'namespace': $scope.namespace,
|
||||
'repository': $scope.forRepository,
|
||||
'skip_permissions': $scope.skipPermissions
|
||||
};
|
||||
};
|
||||
|
||||
$scope.askCreateRobot = function() {
|
||||
$scope.createRobotInfo = {
|
||||
'namespace': $scope.namespace,
|
||||
'repository': $scope.forRepository,
|
||||
'skip_permissions': $scope.skipPermissions
|
||||
};
|
||||
};
|
||||
|
||||
$scope.handleTeamCreated = function(created) {
|
||||
$scope.setEntity(created.name, 'team', false, created.avatar);
|
||||
if (created.new_team) {
|
||||
$scope.teams.push(created);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.handleRobotCreated = function(created) {
|
||||
$scope.setEntity(created.name, 'user', true, created.avatar);
|
||||
$scope.page.robots.push(created);
|
||||
};
|
||||
|
||||
$scope.setEntity = function(name, kind, is_robot, avatar) {
|
||||
var entity = {
|
||||
'name': name,
|
||||
'kind': kind,
|
||||
'is_robot': is_robot,
|
||||
'avatar': avatar
|
||||
};
|
||||
|
||||
if ($scope.isOrganization) {
|
||||
entity['is_org_member'] = true;
|
||||
}
|
||||
|
||||
$scope.setEntityInternal(entity, false);
|
||||
};
|
||||
|
||||
$scope.clearEntityInternal = function() {
|
||||
$scope.currentEntityInternal = null;
|
||||
$scope.currentEntity = null;
|
||||
$scope.entitySelected({'entity': null});
|
||||
if ($scope.ngModel) {
|
||||
$scope.ngModel.$setValidity('entity', false);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.setEntityInternal = function(entity, updateTypeahead) {
|
||||
// If the entity is an external entity, convert it to a known user via an API call.
|
||||
if (entity.kind == 'external') {
|
||||
var params = {
|
||||
'username': entity.name
|
||||
};
|
||||
|
||||
ApiService.linkExternalUser(null, params).then(function(resp) {
|
||||
$scope.setEntityInternal(resp['entity'], updateTypeahead);
|
||||
}, ApiService.errorDisplay('Could not link external user'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (updateTypeahead) {
|
||||
$(input).typeahead('val', $scope.autoClear ? '' : entity.name);
|
||||
} else {
|
||||
$(input).val($scope.autoClear ? '' : entity.name);
|
||||
}
|
||||
|
||||
if (!$scope.autoClear) {
|
||||
$scope.currentEntityInternal = entity;
|
||||
$scope.currentEntity = entity;
|
||||
}
|
||||
|
||||
$scope.entitySelected({'entity': entity});
|
||||
if ($scope.ngModel) {
|
||||
$scope.ngModel.$setValidity('entity', !!entity);
|
||||
}
|
||||
};
|
||||
|
||||
// Setup the typeahead.
|
||||
var input = $element[0].firstChild.firstChild;
|
||||
|
||||
(function() {
|
||||
// Create the bloodhound search query system.
|
||||
$rootScope.__entity_search_counter = (($rootScope.__entity_search_counter || 0) + 1);
|
||||
var entitySearchB = new Bloodhound({
|
||||
name: 'entities' + $rootScope.__entity_search_counter,
|
||||
remote: {
|
||||
url: '/api/v1/entities/%QUERY',
|
||||
replace: function (query_url, uriEncodedQuery) {
|
||||
$scope.lazyLoad();
|
||||
|
||||
var namespace = $scope.namespace || '';
|
||||
|
||||
var url = UtilService.getRestUrl(query_url.replace('%QUERY', uriEncodedQuery));
|
||||
url.setQueryParameter('namespace', namespace);
|
||||
|
||||
if ($scope.isOrganization && isSupported('team')) {
|
||||
url.setQueryParameter('includeTeams', true);
|
||||
}
|
||||
|
||||
if (isSupported('org')) {
|
||||
url.setQueryParameter('includeOrgs', true);
|
||||
}
|
||||
return url;
|
||||
},
|
||||
filter: function(data) {
|
||||
var datums = [];
|
||||
for (var i = 0; i < data.results.length; ++i) {
|
||||
var entity = data.results[i];
|
||||
|
||||
var found = 'user';
|
||||
if (entity.kind == 'user' || entity.kind == 'external') {
|
||||
found = entity.is_robot ? 'robot' : 'user';
|
||||
} else if (entity.kind == 'team') {
|
||||
found = 'team';
|
||||
} else if (entity.kind == 'org') {
|
||||
found = 'org';
|
||||
}
|
||||
|
||||
if (!isSupported(found)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
datums.push({
|
||||
'value': entity.name,
|
||||
'tokens': [entity.name],
|
||||
'entity': entity
|
||||
});
|
||||
}
|
||||
return datums;
|
||||
}
|
||||
},
|
||||
datumTokenizer: function(d) {
|
||||
return Bloodhound.tokenizers.whitespace(d.val);
|
||||
},
|
||||
queryTokenizer: Bloodhound.tokenizers.whitespace
|
||||
});
|
||||
entitySearchB.initialize();
|
||||
|
||||
// Setup the typeahead.
|
||||
$(input).typeahead({
|
||||
'highlight': true,
|
||||
'hint': false,
|
||||
}, {
|
||||
display: 'value',
|
||||
source: entitySearchB.ttAdapter(),
|
||||
templates: {
|
||||
'notFound': function(info) {
|
||||
// Only display the empty dialog if the server load has finished.
|
||||
if (info.resultKind == 'remote') {
|
||||
var val = $(input).val();
|
||||
if (!val) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (UtilService.isEmailAddress(val)) {
|
||||
if ($scope.allowEmails) {
|
||||
return '<div class="tt-message">' + $scope.emailMessage + '</div>';
|
||||
} else {
|
||||
return '<div class="tt-empty">A ' + Config.REGISTRY_TITLE_SHORT + ' username (not an e-mail address) must be specified</div>';
|
||||
}
|
||||
}
|
||||
|
||||
var classes = [];
|
||||
|
||||
if (isSupported('user')) { classes.push('users'); }
|
||||
if (isSupported('org')) { classes.push('organizations'); }
|
||||
if ($scope.isAdmin && isSupported('robot')) { classes.push('robot accounts'); }
|
||||
if ($scope.isOrganization && isSupported('team')) { classes.push('teams'); }
|
||||
|
||||
if (classes.length == 0) {
|
||||
return '<div class="tt-empty">No matching entities found</div>';
|
||||
}
|
||||
|
||||
var class_string = '';
|
||||
for (var i = 0; i < classes.length; ++i) {
|
||||
if (i > 0) {
|
||||
if (i == classes.length - 1) {
|
||||
class_string += ' or ';
|
||||
} else {
|
||||
class_string += ', ';
|
||||
}
|
||||
}
|
||||
|
||||
class_string += classes[i];
|
||||
}
|
||||
|
||||
return '<div class="tt-empty">No matching ' + Config.REGISTRY_TITLE_SHORT + ' ' + class_string + ' found</div>';
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
'suggestion': function (datum) {
|
||||
template = '<div class="entity-mini-listing">';
|
||||
if (Config['AVATAR_KIND'] === 'gravatar' &&
|
||||
((datum.entity.kind == 'user' && !datum.entity.is_robot) || (datum.entity.kind == 'org'))) {
|
||||
template += '<i class="fa"><img class="avatar-image" src="' +
|
||||
AvatarService.getAvatar(datum.entity.avatar.hash, 20, 'mm') +
|
||||
'"></i>';
|
||||
} else if (datum.entity.kind == 'external') {
|
||||
template += '<i class="fa fa-user fa-lg"></i>';
|
||||
} else if (datum.entity.kind == 'user' && datum.entity.is_robot) {
|
||||
template += '<i class="fa ci-robot fa-lg"></i>';
|
||||
} else if (datum.entity.kind == 'team') {
|
||||
template += '<i class="fa fa-group fa-lg"></i>';
|
||||
}
|
||||
|
||||
template += '<span class="name">' + datum.value + '</span>';
|
||||
|
||||
if (datum.entity.title) {
|
||||
template += '<span class="title">' + datum.entity.title + '</span>';
|
||||
}
|
||||
|
||||
if (datum.entity.is_org_member === false && datum.entity.kind == 'user') {
|
||||
template += '<i class="fa fa-exclamation-triangle" title="User is outside the organization"></i>';
|
||||
}
|
||||
|
||||
template += '</div>';
|
||||
return template;
|
||||
}}
|
||||
});
|
||||
|
||||
$(input).on('keypress', function(e) {
|
||||
var val = $(input).val();
|
||||
var code = e.keyCode || e.which;
|
||||
if (code == 13 && $scope.allowEmails && UtilService.isEmailAddress(val)) {
|
||||
$scope.$apply(function() {
|
||||
$scope.emailSelected({'email': val});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$(input).on('input', function(e) {
|
||||
$scope.$apply(function() {
|
||||
$scope.clearEntityInternal();
|
||||
});
|
||||
});
|
||||
|
||||
$(input).on('typeahead:selected', function(e, datum) {
|
||||
$scope.$apply(function() {
|
||||
$scope.setEntityInternal(datum.entity, true);
|
||||
});
|
||||
});
|
||||
})();
|
||||
|
||||
$scope.$watch('clearValue', function() {
|
||||
if (!input) { return; }
|
||||
|
||||
$(input).typeahead('val', '');
|
||||
$scope.clearEntityInternal();
|
||||
});
|
||||
|
||||
$scope.$watch('placeholder', function(title) {
|
||||
input.setAttribute('placeholder', title);
|
||||
});
|
||||
|
||||
$scope.$watch('allowedEntities', function(allowed) {
|
||||
if (!allowed) { return; }
|
||||
$scope.includeTeams = isSupported('team', allowed);
|
||||
$scope.includeRobots = isSupported('robot', allowed);
|
||||
});
|
||||
|
||||
$scope.$watch('namespace', function(namespace) {
|
||||
if (!namespace) { return; }
|
||||
resetCache();
|
||||
$scope.checkLazyLoad();
|
||||
});
|
||||
|
||||
UserService.updateUserIn($scope, function(currentUser){
|
||||
if (currentUser.anonymous) { return; }
|
||||
$scope.thisUser = currentUser;
|
||||
resetCache();
|
||||
$scope.checkLazyLoad();
|
||||
});
|
||||
|
||||
$scope.$watch('currentEntity', function(entity) {
|
||||
if ($scope.currentEntityInternal != entity) {
|
||||
if (entity) {
|
||||
$scope.setEntityInternal(entity, false);
|
||||
} else {
|
||||
$scope.clearEntityInternal();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
|
@ -0,0 +1,23 @@
|
|||
.expiration-status-view-element .expired, .expiration-status-view-element .expired a {
|
||||
color: #D64456;
|
||||
}
|
||||
|
||||
.expiration-status-view-element .critical, .expiration-status-view-element .critical a {
|
||||
color: #F77454;
|
||||
}
|
||||
|
||||
.expiration-status-view-element .warning, .expiration-status-view-element .warning a {
|
||||
color: #FCA657;
|
||||
}
|
||||
|
||||
.expiration-status-view-element .info, .expiration-status-view-element .info a {
|
||||
color: #2FC98E;
|
||||
}
|
||||
|
||||
.expiration-status-view-element .no-expiration, .expiration-status-view-element .no-expiration a {
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.expiration-status-view-element .fa {
|
||||
margin-right: 6px;
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
<span class="expiration-status-view-element">
|
||||
<span ng-if="::$ctrl.expirationDate" ng-class="::$ctrl.getExpirationInfo($ctrl.expirationDate).className">
|
||||
<i class="fa" ng-class="::$ctrl.getExpirationInfo($ctrl.expirationDate).icon"></i>
|
||||
<time-ago datetime="$ctrl.expirationDate"></time-ago>
|
||||
</a>
|
||||
</span>
|
||||
<span class="no-expiration" ng-if="::!$ctrl.expirationDate">
|
||||
Never
|
||||
</span>
|
||||
</span>
|
|
@ -0,0 +1,40 @@
|
|||
import { Input, Component, Inject } from 'ng-metadata/core';
|
||||
import * as moment from "moment";
|
||||
import './expiration-status-view.component.css';
|
||||
|
||||
type expirationInfo = {
|
||||
className: string;
|
||||
icon: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* A component that displays expiration status.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'expiration-status-view',
|
||||
templateUrl: '/static/js/directives/ui/expiration-status-view/expiration-status-view.component.html',
|
||||
})
|
||||
export class ExpirationStatusViewComponent {
|
||||
@Input('<') public expirationDate: Date;
|
||||
|
||||
private getExpirationInfo(expirationDate): expirationInfo|null {
|
||||
if (!expirationDate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const expiration = moment(expirationDate);
|
||||
if (moment().isAfter(expiration)) {
|
||||
return {'className': 'expired', 'icon': 'fa-warning'};
|
||||
}
|
||||
|
||||
if (moment().add(1, 'week').isAfter(expiration)) {
|
||||
return {'className': 'critical', 'icon': 'fa-warning'};
|
||||
}
|
||||
|
||||
if (moment().add(1, 'month').isAfter(expiration)) {
|
||||
return {'className': 'warning', 'icon': 'fa-warning'};
|
||||
}
|
||||
|
||||
return {'className': 'info', 'icon': 'fa-clock-o'};
|
||||
}
|
||||
}
|
39
static/js/directives/ui/external-login-button.js
Normal file
39
static/js/directives/ui/external-login-button.js
Normal file
|
@ -0,0 +1,39 @@
|
|||
/**
|
||||
* An element which displays a button for logging into the application via an external service.
|
||||
*/
|
||||
angular.module('quay').directive('externalLoginButton', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/external-login-button.html',
|
||||
replace: false,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'signInStarted': '&signInStarted',
|
||||
'redirectUrl': '=redirectUrl',
|
||||
'isLink': '=isLink',
|
||||
'provider': '=provider',
|
||||
'action': '@action'
|
||||
},
|
||||
controller: function($scope, $timeout, $interval, ApiService, KeyService, CookieService, ExternalLoginService) {
|
||||
$scope.signingIn = false;
|
||||
|
||||
$scope.startSignin = function() {
|
||||
$scope.signInStarted({'service': $scope.provider});
|
||||
ExternalLoginService.getLoginUrl($scope.provider, $scope.action || 'login', function(url) {
|
||||
// Save the redirect URL in a cookie so that we can redirect back after the service returns to us.
|
||||
var redirectURL = $scope.redirectUrl || window.location.toString();
|
||||
CookieService.putPermanent('quay.redirectAfterLoad', redirectURL);
|
||||
|
||||
// Needed to ensure that UI work done by the started callback is finished before the location
|
||||
// changes.
|
||||
$scope.signingIn = true;
|
||||
$timeout(function() {
|
||||
document.location = url;
|
||||
}, 250);
|
||||
});
|
||||
};
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
51
static/js/directives/ui/external-logins-manager.js
Normal file
51
static/js/directives/ui/external-logins-manager.js
Normal file
|
@ -0,0 +1,51 @@
|
|||
/**
|
||||
* Element for managing the applications authorized by a user.
|
||||
*/
|
||||
angular.module('quay').directive('externalLoginsManager', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/external-logins-manager.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'user': '=user',
|
||||
},
|
||||
controller: function($scope, $element, ApiService, UserService, Features, Config, KeyService,
|
||||
ExternalLoginService) {
|
||||
$scope.Features = Features;
|
||||
$scope.Config = Config;
|
||||
$scope.KeyService = KeyService;
|
||||
|
||||
$scope.EXTERNAL_LOGINS = ExternalLoginService.EXTERNAL_LOGINS;
|
||||
$scope.externalLoginInfo = {};
|
||||
$scope.hasSingleSignin = ExternalLoginService.hasSingleSignin();
|
||||
|
||||
UserService.updateUserIn($scope, function(user) {
|
||||
$scope.cuser = jQuery.extend({}, user);
|
||||
$scope.externalLoginInfo = {};
|
||||
|
||||
if ($scope.cuser.logins) {
|
||||
for (var i = 0; i < $scope.cuser.logins.length; i++) {
|
||||
var login = $scope.cuser.logins[i];
|
||||
login.metadata = login.metadata || {};
|
||||
$scope.externalLoginInfo[login.service] = login;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$scope.detachExternalLogin = function(service_id) {
|
||||
if (!Features.DIRECT_LOGIN) { return; }
|
||||
|
||||
var params = {
|
||||
'service_id': service_id
|
||||
};
|
||||
|
||||
ApiService.detachExternalLogin(null, params).then(function() {
|
||||
UserService.load();
|
||||
}, ApiService.errorDisplay('Count not detach service'));
|
||||
};
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
59
static/js/directives/ui/external-notification-view.js
Normal file
59
static/js/directives/ui/external-notification-view.js
Normal file
|
@ -0,0 +1,59 @@
|
|||
/**
|
||||
* DEPRECATED: An element which displays controls and information about a defined external notification on
|
||||
* a repository.
|
||||
*/
|
||||
angular.module('quay').directive('externalNotificationView', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/external-notification-view.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'repository': '=repository',
|
||||
'notification': '=notification',
|
||||
'notificationDeleted': '¬ificationDeleted'
|
||||
},
|
||||
controller: function($scope, $element, ExternalNotificationData, ApiService) {
|
||||
$scope.deleteNotification = function() {
|
||||
var params = {
|
||||
'repository': $scope.repository.namespace + '/' + $scope.repository.name,
|
||||
'uuid': $scope.notification.uuid
|
||||
};
|
||||
|
||||
ApiService.deleteRepoNotification(null, params).then(function() {
|
||||
$scope.notificationDeleted({'notification': $scope.notification});
|
||||
});
|
||||
};
|
||||
|
||||
$scope.testNotification = function() {
|
||||
var params = {
|
||||
'repository': $scope.repository.namespace + '/' + $scope.repository.name,
|
||||
'uuid': $scope.notification.uuid
|
||||
};
|
||||
|
||||
ApiService.testRepoNotification(null, params).then(function() {
|
||||
bootbox.dialog({
|
||||
"title": "Test Notification Queued",
|
||||
"message": "A test version of this notification has been queued and should appear shortly",
|
||||
"buttons": {
|
||||
"close": {
|
||||
"label": "Close",
|
||||
"className": "btn-primary"
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$scope.$watch('notification', function(notification) {
|
||||
if (notification) {
|
||||
$scope.eventInfo = ExternalNotificationData.getEventInfo(notification.event);
|
||||
$scope.methodInfo = ExternalNotificationData.getMethodInfo(notification.method);
|
||||
$scope.config = notification.config;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
37
static/js/directives/ui/feedback-bar.js
Normal file
37
static/js/directives/ui/feedback-bar.js
Normal file
|
@ -0,0 +1,37 @@
|
|||
/**
|
||||
* An element which displays a feedback bar when an action has been taken.
|
||||
*/
|
||||
angular.module('quay').directive('feedbackBar', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/feedback-bar.html',
|
||||
replace: false,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'feedback': '=feedback'
|
||||
},
|
||||
controller: function($scope, $element, AvatarService, Config, UIService, $timeout, StringBuilderService) {
|
||||
$scope.viewCounter = 0;
|
||||
$scope.formattedMessage = '';
|
||||
|
||||
$scope.$watch('feedback', function(feedback) {
|
||||
if (feedback) {
|
||||
$scope.formattedMessage = StringBuilderService.buildTrustedString(feedback.message, feedback.data || {}, 'span');
|
||||
$scope.viewCounter++;
|
||||
} else {
|
||||
$scope.viewCounter = 0;
|
||||
}
|
||||
});
|
||||
|
||||
$($element).find('.feedback-bar-element')
|
||||
.on('webkitAnimationEnd oanimationend oAnimationEnd msAnimationEnd animationend',
|
||||
function(e) {
|
||||
$scope.$apply(function() {
|
||||
$scope.viewCounter = 0;
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
143
static/js/directives/ui/fetch-tag-dialog.js
Normal file
143
static/js/directives/ui/fetch-tag-dialog.js
Normal file
|
@ -0,0 +1,143 @@
|
|||
/**
|
||||
* An element which adds a of dialog for fetching a tag.
|
||||
*/
|
||||
angular.module('quay').directive('fetchTagDialog', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/fetch-tag-dialog.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'repository': '=repository',
|
||||
'actionHandler': '=actionHandler'
|
||||
},
|
||||
controller: function($scope, $element, $timeout, ApiService, UserService, Config, Features) {
|
||||
$scope.clearCounter = 0;
|
||||
$scope.currentFormat = null;
|
||||
$scope.currentEntity = null;
|
||||
$scope.currentRobot = null;
|
||||
$scope.formats = [];
|
||||
$scope.currentRobotHasPermission = null;
|
||||
|
||||
UserService.updateUserIn($scope, updateFormats);
|
||||
|
||||
var updateFormats = function() {
|
||||
$scope.formats = [];
|
||||
|
||||
$scope.formats.push({
|
||||
'title': 'Docker Pull (by tag)',
|
||||
'icon': 'docker-icon',
|
||||
'command': 'docker pull {hostname}/{namespace}/{name}:{tag}'
|
||||
});
|
||||
|
||||
if ($scope.currentTag && $scope.currentTag.manifest_digest) {
|
||||
$scope.formats.push({
|
||||
'title': 'Docker Pull (by digest)',
|
||||
'icon': 'docker-icon',
|
||||
'command': 'docker pull {hostname}/{namespace}/{name}@{manifest_digest}'
|
||||
});
|
||||
}
|
||||
|
||||
if ($scope.repository && $scope.currentTag && !$scope.currentTag.is_manifest_list) {
|
||||
$scope.formats.push({
|
||||
'title': 'Squashed Docker Image',
|
||||
'icon': 'ci-squashed',
|
||||
'command': 'curl -L -f {http}://{pull_user}:{pull_password}@{hostname}/c1/squash/{namespace}/{name}/{tag} | docker load',
|
||||
'require_creds': true,
|
||||
'has_creds': UserService.isNamespaceAdmin($scope.repository.namespace)
|
||||
});
|
||||
}
|
||||
|
||||
if (Features.ACI_CONVERSION && $scope.currentTag && !$scope.currentTag.is_manifest_list) {
|
||||
$scope.formats.push({
|
||||
'title': 'rkt Fetch',
|
||||
'icon': 'rocket-icon',
|
||||
'command': 'rkt fetch {hostname}/{namespace}/{name}:{tag}'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.$watch('currentEntity', function(entity) {
|
||||
if (!entity) {
|
||||
$scope.currentRobot = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if ($scope.currentRobot && $scope.currentRobot.name == entity.name) {
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.currentRobot = null;
|
||||
$scope.currentRobotHasPermission = null;
|
||||
|
||||
var parts = entity.name.split('+');
|
||||
var namespace = parts[0];
|
||||
var shortname = parts[1];
|
||||
|
||||
var params = {
|
||||
'robot_shortname': shortname
|
||||
};
|
||||
|
||||
var orgname = UserService.isOrganization(namespace) ? namespace : '';
|
||||
ApiService.getRobot(orgname, null, params).then(function(resp) {
|
||||
$scope.currentRobot = resp;
|
||||
}, ApiService.errorDisplay('Cannot download robot token'));
|
||||
|
||||
var permParams = {
|
||||
'repository': $scope.repository.namespace + '/' + $scope.repository.name,
|
||||
'username': entity.name
|
||||
};
|
||||
|
||||
ApiService.getUserTransitivePermission(null, permParams).then(function(resp) {
|
||||
$scope.currentRobotHasPermission = resp['permissions'].length > 0;
|
||||
});
|
||||
});
|
||||
|
||||
$scope.getCommand = function(format, robot) {
|
||||
if (!format || !format.command) { return ''; }
|
||||
if (format.require_creds && !robot) { return ''; }
|
||||
|
||||
var params = {
|
||||
'pull_user': robot ? robot.name : '',
|
||||
'pull_password': robot ? robot.token : '',
|
||||
'hostname': Config.getDomain(),
|
||||
'http': Config.getHttp(),
|
||||
'namespace': $scope.repository.namespace,
|
||||
'name': $scope.repository.name,
|
||||
'tag': $scope.currentTag.name,
|
||||
'manifest_digest': $scope.currentTag.manifest_digest
|
||||
};
|
||||
|
||||
var value = format.command;
|
||||
for (var param in params) {
|
||||
if (!params.hasOwnProperty(param)) { continue; }
|
||||
value = value.replace('{' + param + '}', params[param]);
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
$scope.setFormat = function(format) {
|
||||
$scope.currentFormat = format;
|
||||
};
|
||||
|
||||
$scope.actionHandler = {
|
||||
'askFetchTag': function(tag) {
|
||||
$scope.currentTag = tag;
|
||||
$scope.currentFormat = null;
|
||||
$scope.currentEntity = null;
|
||||
$scope.currentRobot = null;
|
||||
$scope.currentRobotHasPermission = null;
|
||||
|
||||
$scope.clearCounter++;
|
||||
|
||||
updateFormats();
|
||||
|
||||
$element.find('#fetchTagDialog').modal({});
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
172
static/js/directives/ui/file-upload-box.js
Normal file
172
static/js/directives/ui/file-upload-box.js
Normal file
|
@ -0,0 +1,172 @@
|
|||
/**
|
||||
* An element which adds a stylize box for uploading a file.
|
||||
*/
|
||||
angular.module('quay').directive('fileUploadBox', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/file-upload-box.html',
|
||||
replace: false,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'selectMessage': '@selectMessage',
|
||||
|
||||
'filesSelected': '&filesSelected',
|
||||
'filesCleared': '&filesCleared',
|
||||
'filesValidated': '&filesValidated',
|
||||
|
||||
'extensions': '<extensions',
|
||||
|
||||
'reset': '=?reset'
|
||||
},
|
||||
controller: function($rootScope, $scope, $element, ApiService) {
|
||||
var MEGABYTE = 1000000;
|
||||
var MAX_FILE_SIZE = MAX_FILE_SIZE_MB * MEGABYTE;
|
||||
var MAX_FILE_SIZE_MB = 100;
|
||||
|
||||
var number = $rootScope.__fileUploadBoxIdCounter || 0;
|
||||
$rootScope.__fileUploadBoxIdCounter = number + 1;
|
||||
|
||||
$scope.boxId = number;
|
||||
$scope.state = 'clear';
|
||||
$scope.selectedFiles = [];
|
||||
|
||||
var conductUpload = function(file, url, fileId, mimeType, progressCb, doneCb) {
|
||||
var request = new XMLHttpRequest();
|
||||
request.open('PUT', url, true);
|
||||
request.setRequestHeader('Content-Type', mimeType);
|
||||
request.onprogress = function(e) {
|
||||
$scope.$apply(function() {
|
||||
if (e.lengthComputable) { progressCb((e.loaded / e.total) * 100); }
|
||||
});
|
||||
};
|
||||
|
||||
request.onerror = function() {
|
||||
$scope.$apply(function() { doneCb(false, 'Error when uploading'); });
|
||||
};
|
||||
|
||||
request.onreadystatechange = function() {
|
||||
var state = request.readyState;
|
||||
var status = request.status;
|
||||
|
||||
if (state == 4) {
|
||||
if (Math.floor(status / 100) == 2) {
|
||||
$scope.$apply(function() { doneCb(true, fileId); });
|
||||
} else {
|
||||
var message = request.statusText;
|
||||
if (status == 413) {
|
||||
message = 'Selected file too large to upload';
|
||||
}
|
||||
|
||||
$scope.$apply(function() { doneCb(false, message); });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
request.send(file);
|
||||
};
|
||||
|
||||
var uploadFiles = function(callback) {
|
||||
var currentIndex = 0;
|
||||
var fileIds = [];
|
||||
|
||||
var progressCb = function(progress) {
|
||||
$scope.uploadProgress = progress;
|
||||
};
|
||||
|
||||
var doneCb = function(status, messageOrId) {
|
||||
if (status) {
|
||||
fileIds.push(messageOrId);
|
||||
currentIndex++;
|
||||
performFileUpload();
|
||||
} else {
|
||||
callback(false, messageOrId);
|
||||
}
|
||||
};
|
||||
|
||||
var performFileUpload = function() {
|
||||
// If we have finished uploading all of the files, invoke the overall callback
|
||||
// with the list of file IDs.
|
||||
if (currentIndex >= $scope.selectedFiles.length) {
|
||||
callback(true, fileIds);
|
||||
return;
|
||||
}
|
||||
|
||||
// For the current file, retrieve a file-drop URL from the API for the file.
|
||||
var currentFile = $scope.selectedFiles[currentIndex];
|
||||
var mimeType = currentFile.type || 'application/octet-stream';
|
||||
var data = {
|
||||
'mimeType': mimeType
|
||||
};
|
||||
|
||||
$scope.currentlyUploadingFile = currentFile;
|
||||
$scope.uploadProgress = 0;
|
||||
|
||||
ApiService.getFiledropUrl(data).then(function(resp) {
|
||||
// Perform the upload.
|
||||
conductUpload(currentFile, resp.url, resp.file_id, mimeType, progressCb, doneCb);
|
||||
}, function() {
|
||||
callback(false, 'Could not retrieve upload URL');
|
||||
});
|
||||
};
|
||||
|
||||
// Start the uploading.
|
||||
$scope.state = 'uploading';
|
||||
performFileUpload();
|
||||
};
|
||||
|
||||
$scope.handleFilesChanged = function(files) {
|
||||
if ($scope.state == 'uploading') { return; }
|
||||
|
||||
$scope.message = null;
|
||||
$scope.selectedFiles = files;
|
||||
|
||||
if (files.length == 0) {
|
||||
$scope.state = 'clear';
|
||||
$scope.filesCleared();
|
||||
} else {
|
||||
for (var i = 0; i < files.length; ++i) {
|
||||
if (files[i].size > MAX_FILE_SIZE) {
|
||||
$scope.state = 'error';
|
||||
$scope.message = 'File ' + files[i].name + ' is larger than the maximum file ' +
|
||||
'size of ' + MAX_FILE_SIZE_MB + ' MB';
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$scope.state = 'checking';
|
||||
$scope.filesSelected({
|
||||
'files': files,
|
||||
'callback': function(status, message) {
|
||||
$scope.state = status ? 'okay' : 'error';
|
||||
$scope.message = message;
|
||||
|
||||
if (status) {
|
||||
$scope.filesValidated({
|
||||
'files': files,
|
||||
'uploadFiles': uploadFiles
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.getAccepts = function(extensions) {
|
||||
if (!extensions || !extensions.length) {
|
||||
return '*';
|
||||
}
|
||||
|
||||
return extensions.join(',');
|
||||
};
|
||||
|
||||
$scope.$watch('reset', function(reset) {
|
||||
if (reset) {
|
||||
$scope.state = 'clear';
|
||||
$element.find('#file-drop-' + $scope.boxId).parent().trigger('reset');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
21
static/js/directives/ui/filter-box.js
Normal file
21
static/js/directives/ui/filter-box.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
/**
|
||||
* An element which displays a right-aligned control bar with an <input> for filtering a collection.
|
||||
*/
|
||||
angular.module('quay').directive('filterBox', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/filter-box.html',
|
||||
replace: false,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'collection': '=collection',
|
||||
'filterModel': '=filterModel',
|
||||
'filterName': '@filterName'
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
22
static/js/directives/ui/filter-control.js
Normal file
22
static/js/directives/ui/filter-control.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
/**
|
||||
* An element which displays a link to change a lookup filter, and shows whether it is selected.
|
||||
*/
|
||||
angular.module('quay').directive('filterControl', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/filter-control.html',
|
||||
replace: false,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'filter': '=filter',
|
||||
'value': '@value'
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
$scope.setFilter = function() {
|
||||
$scope.filter = $scope.value;
|
||||
};
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
102
static/js/directives/ui/global-message-tab.js
Normal file
102
static/js/directives/ui/global-message-tab.js
Normal file
|
@ -0,0 +1,102 @@
|
|||
/**
|
||||
* An element for managing global messages.
|
||||
*/
|
||||
angular.module('quay').directive('globalMessageTab', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/global-message-tab.html',
|
||||
replace: false,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'isEnabled': '=isEnabled'
|
||||
},
|
||||
controller: function ($scope, $element, ApiService, StateService) {
|
||||
$scope.inReadOnlyMode = StateService.inReadOnlyMode();
|
||||
|
||||
$scope.newMessage = {
|
||||
'media_type': 'text/markdown',
|
||||
'severity': 'info'
|
||||
};
|
||||
$scope.creatingMessage = false;
|
||||
|
||||
$scope.showCreateMessage = function () {
|
||||
$scope.createdMessage = null;
|
||||
$('#createMessageModal').modal('show');
|
||||
};
|
||||
|
||||
$scope.createNewMessage = function () {
|
||||
if (StateService.inReadOnlyMode()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.creatingMessage = true;
|
||||
$scope.createdMessage = null;
|
||||
|
||||
var errorHandler = ApiService.errorDisplay('Cannot create message', function () {
|
||||
$scope.creatingMessage = false;
|
||||
$('#createMessageModal').modal('hide');
|
||||
});
|
||||
|
||||
var data = {
|
||||
'message': $scope.newMessage
|
||||
};
|
||||
|
||||
ApiService.createGlobalMessage(data, null).then(function (resp) {
|
||||
$scope.creatingMessage = false;
|
||||
|
||||
$('#createMessageModal').modal('hide');
|
||||
$scope.loadMessageInternal();
|
||||
}, errorHandler)
|
||||
};
|
||||
|
||||
$scope.updateMessage = function(content) {
|
||||
$scope.newMessage.content = content;
|
||||
};
|
||||
|
||||
$scope.showDeleteMessage = function (uuid) {
|
||||
if (StateService.inReadOnlyMode()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.messageToDelete = uuid;
|
||||
$('#confirmDeleteMessageModal').modal({});
|
||||
};
|
||||
|
||||
$scope.deleteMessage = function (uuid) {
|
||||
if (StateService.inReadOnlyMode()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$('#confirmDeleteMessageModal').modal('hide');
|
||||
ApiService.deleteGlobalMessage(null, {uuid: uuid}).then(function (resp) {
|
||||
$scope.loadMessageInternal();
|
||||
}, ApiService.errorDisplay('Can not delete message'));
|
||||
};
|
||||
|
||||
$scope.loadMessageOfTheDay = function () {
|
||||
if ($scope.messages) {
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.loadMessageInternal();
|
||||
};
|
||||
|
||||
$scope.loadMessageInternal = function () {
|
||||
ApiService.getGlobalMessages().then(function (resp) {
|
||||
$scope.messages = resp['messages'];
|
||||
}, function (resp) {
|
||||
$scope.messages = [];
|
||||
$scope.messagesErrors = ApiService.getErrorMessage(resp);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.$watch('isEnabled', function (value) {
|
||||
if (value) {
|
||||
$scope.loadMessageInternal();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
151
static/js/directives/ui/header-bar.js
Normal file
151
static/js/directives/ui/header-bar.js
Normal file
|
@ -0,0 +1,151 @@
|
|||
|
||||
/**
|
||||
* The application header bar.
|
||||
*/
|
||||
angular.module('quay').directive('headerBar', function () {
|
||||
var number = 0;
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/header-bar.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
},
|
||||
controller: function($rootScope, $scope, $element, $location, $timeout, hotkeys, UserService,
|
||||
PlanService, ApiService, NotificationService, Config, Features,
|
||||
ExternalLoginService, StateService) {
|
||||
|
||||
ExternalLoginService.getSingleSigninUrl(function(url) {
|
||||
$scope.externalSigninUrl = url;
|
||||
});
|
||||
|
||||
var hotkeysAdded = false;
|
||||
var userUpdated = function(cUser) {
|
||||
$scope.searchingAllowed = Features.ANONYMOUS_ACCESS || !cUser.anonymous;
|
||||
|
||||
if (hotkeysAdded) { return; }
|
||||
hotkeysAdded = true;
|
||||
|
||||
// Register hotkeys.
|
||||
if (!cUser.anonymous) {
|
||||
hotkeys.add({
|
||||
combo: 'alt+c',
|
||||
description: 'Create new repository',
|
||||
callback: function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
$location.url('/new');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.Config = Config;
|
||||
$scope.Features = Features;
|
||||
$scope.notificationService = NotificationService;
|
||||
$scope.searchingAllowed = false;
|
||||
$scope.showBuildDialogCounter = 0;
|
||||
|
||||
// Monitor any user changes and place the current user into the scope.
|
||||
UserService.updateUserIn($scope, userUpdated);
|
||||
StateService.updateStateIn($scope, function(state) {
|
||||
$scope.inReadOnlyMode = state.inReadOnlyMode;
|
||||
});
|
||||
|
||||
$scope.currentPageContext = {};
|
||||
|
||||
$rootScope.$watch('currentPage.scope.viewuser', function(u) {
|
||||
$scope.currentPageContext['viewuser'] = u;
|
||||
});
|
||||
|
||||
$rootScope.$watch('currentPage.scope.organization', function(o) {
|
||||
$scope.currentPageContext['organization'] = o;
|
||||
});
|
||||
|
||||
$rootScope.$watch('currentPage.scope.repository', function(r) {
|
||||
$scope.currentPageContext['repository'] = r;
|
||||
});
|
||||
|
||||
$scope.signout = function() {
|
||||
ApiService.logout().then(function() {
|
||||
UserService.load();
|
||||
$location.path('/');
|
||||
});
|
||||
};
|
||||
|
||||
$scope.getEnterpriseLogo = function() {
|
||||
return Config.getEnterpriseLogo();
|
||||
};
|
||||
|
||||
$scope.getNamespace = function(context) {
|
||||
if (!context) { return null; }
|
||||
|
||||
if (context.repository && context.repository.namespace) {
|
||||
return context.repository.namespace;
|
||||
}
|
||||
|
||||
if (context.organization && context.organization.name) {
|
||||
return context.organization.name;
|
||||
}
|
||||
|
||||
if (context.viewuser && context.viewuser.username) {
|
||||
return context.viewuser.username;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
$scope.canAdmin = function(namespace) {
|
||||
if (!namespace) { return false; }
|
||||
return UserService.isNamespaceAdmin(namespace);
|
||||
};
|
||||
|
||||
$scope.isOrganization = function(namespace) {
|
||||
if (!namespace) { return false; }
|
||||
return UserService.isOrganization(namespace);
|
||||
};
|
||||
|
||||
$scope.startBuild = function(context) {
|
||||
$scope.showBuildDialogCounter++;
|
||||
};
|
||||
|
||||
$scope.handleBuildStarted = function(build, context) {
|
||||
$location.url('/repository/' + context.repository.namespace + '/' + context.repository.name + '/build/' + build.id);
|
||||
};
|
||||
|
||||
$scope.handleRobotCreated = function(created, context) {
|
||||
var namespace = $scope.getNamespace(context);
|
||||
if (UserService.isOrganization(namespace)) {
|
||||
$location.url('/organization/' + namespace + '?tab=robots&showRobot=' + created.name);
|
||||
} else {
|
||||
$location.url('/user/' + namespace + '?tab=robots&showRobot=' + created.name);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.handleTeamCreated = function(created, context) {
|
||||
var namespace = $scope.getNamespace(context);
|
||||
$location.url('/organization/' + namespace + '/teams/' + created.name);
|
||||
};
|
||||
|
||||
$scope.askCreateRobot = function(context) {
|
||||
var namespace = $scope.getNamespace(context);
|
||||
if (!namespace || !UserService.isNamespaceAdmin(namespace)) { return; }
|
||||
|
||||
$scope.createRobotInfo = {
|
||||
'namespace': namespace
|
||||
};
|
||||
};
|
||||
|
||||
$scope.askCreateTeam = function(context) {
|
||||
var namespace = $scope.getNamespace(context);
|
||||
if (!namespace || !UserService.isNamespaceAdmin(namespace)) { return; }
|
||||
|
||||
$scope.createTeamInfo = {
|
||||
'namespace': namespace
|
||||
};
|
||||
};
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
Some files were not shown because too many files have changed in this diff Show more
Reference in a new issue