Merge pull request #1455 from coreos-inc/better-dialogs

Better dialogs
This commit is contained in:
josephschorr 2016-05-16 18:27:03 -04:00
commit ee557c944a
32 changed files with 702 additions and 233 deletions

View file

@ -1527,13 +1527,13 @@ a:focus {
-webkit-filter: grayscale(100%);
}
.co-dialog .co-tab-content {
padding: 16px;
padding-bottom: 30px;
}
.co-dialog .co-tab-content h3 {
margin-top: 0px;
margin-bottom: 0px;
}
@ -1574,3 +1574,9 @@ a:focus {
height: auto;
}
}
.co-modal-body-scrollable {
overflow-y: auto;
overflow-x: hidden;
max-height: 400px;
}

View file

@ -0,0 +1,53 @@
.create-entity-dialog-element .modal-body {
min-height: 300px;
}
.create-entity-dialog-element form {
padding: 10px;
}
.create-entity-dialog-element .help-text {
color: #aaa;
margin-top: 10px;
}
.create-entity-dialog-element h4 .fa {
margin-left: 4px;
margin-right: 4px;
}
.create-entity-dialog-element label {
margin-top: 4px;
}
.create-entity-dialog-element .co-table {
margin-top: 20px;
}
.create-entity-dialog-element .fa-hdd-o {
margin-right: 4px;
vertical-align: middle;
}
.create-entity-dialog-element .co-filter-box {
display: block;
float: right;
margin-bottom: 20px;
}
.create-entity-dialog-element .co-filter-box .filter-message {
left: -180px;
top: 4px;
}
.create-entity-dialog-element .co-filter-box input {
width: 100%;
padding-top: 2px;
padding-bottom: 2px;
height: 28px;
}
.create-entity-dialog-element label .avatar {
vertical-align: text-bottom;
margin-left: 4px;
}

View file

@ -7,6 +7,7 @@
overflow: hidden;
text-align: center;
height: 42px;
pointer-events: none;
}
@keyframes flow-down-up {

View file

@ -2,14 +2,26 @@
width: 90px;
position: relative;
text-align: left;
padding: 4px;
padding-left: 10px;
}
.role-group.small .btn {
padding: 2px;
padding-left: 10px;
}
.new-role-group .btn .caret {
position: absolute;
top: 15px;
top: 13px;
right: 7px;
}
.role-group.small .btn .caret {
top: 11px;
}
.new-role-group .role-help-text {
font-size: 12px;
color: #ccc;

View file

@ -0,0 +1,98 @@
<div class="create-entity-dialog-element">
<div class="modal fade co-dialog wider">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" ng-click="hide()" aria-hidden="true">&times;</button>
<h4 class="modal-title" ng-show="view == 'enterName' || view == 'creating'">
<i class="fa {{ entityIcon }}"></i>
Create {{ entityTitle }}
</h4>
<h4 class="modal-title" ng-show="view == 'addperms' || view == 'addingperms'">
Add permissions for <i class="fa {{ entityIcon }}"></i> {{ entity.name }}
</h4>
</div> <!-- /.model-header -->
<div class="modal-body" ng-show="view == 'creating' || view == 'addingperms'">
<div class="cor-loader"></div>
</div>
<div class="modal-body co-modal-body-scrollable" ng-show="view == 'addperms'">
<span class="co-filter-box">
<span class="filter-message" ng-if="options.filter">
Showing {{ orderedRepositories.entries.length }} of {{ repositories.length }} repositories
</span>
<input class="form-control" type="text" ng-model="options.filter" placeholder="Filter Repositories...">
</span>
<label>
Select repositories in
<span class="avatar" size="16" data="namespace.avatar"></span>
{{ info.namespace }}:
</label>
<table class="co-table" style="margin-bottom: 210px;">
<thead>
<td class="checkbox-col"></td>
<td ng-class="TableService.tablePredicateClass('name', options.predicate, options.reverse)">
<a ng-click="TableService.orderBy('name', options)">Repository Name</a>
</td>
<td>Permission</td>
<td ng-class="TableService.tablePredicateClass('last_modified_datetime', options.predicate, options.reverse)">
<a ng-click="TableService.orderBy('last_modified_datetime', options)">Last Updated</a>
</td>
</thead>
<tr class="co-checkable-row"
ng-repeat="repo in orderedRepositories.visibleEntries"
ng-class="checkedRepos.isChecked(repo, checkedRepos.checked) ? 'checked' : ''"
bindonce>
<td>
<span class="cor-checkable-item" controller="checkedRepos" item="repo"></span>
</td>
<td>
<i class="fa fa-hdd-o"></i>
<span bo-text="repo.name"></span>
</td>
<td>
<span class="role-group small" current-role="repo.permission"
roles="repoRolesOrNone"
role-changed="setRole(role, repo)"></span>
</td>
<td>
<span ng-if="repo.last_modified">
{{ repo.last_modified * 1000 | amCalendar }}
</span>
<span class="empty" ng-if="!repo.last_modified">(Empty Repository)</span>
</td>
</tr>
</table>
<div class="empty" ng-if="!orderedRepositories.entries.length"
style="margin-top: 20px;">
<div class="empty-primary-msg">No matching repositories found.</div>
<div class="empty-secondary-msg">Try expanding your filtering terms.</div>
</div>
</div>
<div class="modal-body" ng-show="view == 'enterName'">
<form name="enterNameForm" ng-submit="createEntity()">
<label>Provide a name for your new {{ entityTitle }}:</label>
<input type="text" class="form-control" ng-model="entityName" ng-pattern="entityNameRegexObj" required>
<div class="help-text">
Choose a name to inform your teammates
about this {{ entityTitle }}. Must match {{ entityNameRegex }}.
</div>
</form>
</div> <!-- /.modal-body -->
<div class="modal-footer" ng-show="view == 'addperms'">
<button type="button" class="btn btn-primary" ng-click="addPermissions()"
ng-show="checkedRepos.checked.length">Add permissions</button>
<button type="button" class="btn btn-default" ng-click="hide()">Close</button>
</div> <!-- /.footer-body -->
<div class="modal-footer" ng-show="view == 'enterName'">
<button type="button" class="btn btn-primary" ng-click="createEntity()"
ng-disabled="enterNameForm.$invalid">Create {{ entityTitle }}</button>
<button type="button" class="btn btn-default" ng-click="hide()">Cancel</button>
</div> <!-- /.footer-body -->
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,9 @@
<div class="create-robot-dialog-element">
<div ng-if="info">
<div class="create-entity-dialog" info="info" entity-title="robot account"
entity-kind="user"
entity-icon="ci-robot" entity-name-regex="{{ ROBOT_PATTERN }}"
entity-create-requested="createRobot(name, callback)"
entity-create-completed="robotFinished(entity)"></div>
</div>
</div>

View file

@ -0,0 +1,9 @@
<div class="create-team-dialog-element">
<div ng-if="info">
<div class="create-entity-dialog" info="info" entity-title="team"
entity-kind="team"
entity-icon="fa-group" entity-name-regex="{{ TEAM_PATTERN }}"
entity-create-requested="createTeam(name, callback)"
entity-create-completed="teamFinished(entity)"></div>
</div>
</div>

View file

@ -14,12 +14,12 @@
</li>
<li role="presentation" ng-show="includeTeams && isOrganization && !lazyLoading && isAdmin">
<a role="menuitem" class="new-action" tabindex="-1" ng-click="createTeam()">
<a role="menuitem" class="new-action" tabindex="-1" ng-click="askCreateTeam()">
<i class="fa fa-group"></i> Create team
</a>
</li>
<li role="presentation" ng-show="includeRobots && !lazyLoading && isAdmin">
<a role="menuitem" class="new-action" tabindex="-1" ng-click="createRobot()">
<a role="menuitem" class="new-action" tabindex="-1" ng-click="askCreateRobot()">
<i class="fa ci-robot"></i>
Create robot account
</a>
@ -70,4 +70,8 @@
</li>
</ul>
</div>
<div class="create-team-dialog" info="createTeamInfo"
team-created="handleTeamCreated(team)"></div>
<div class="create-robot-dialog" info="createRobotInfo"
robot-created="handleRobotCreated(robot)"></div>
</span>

View file

@ -38,6 +38,7 @@
<td class="first-col">Pull Credentials:</td>
<td>
<div class="entity-search" namespace="repository.namespace"
for-repository="repository"
placeholder="'Choose Pull Credentials'"
allowed-entities="['robot']"
clear-value="clearCounter"

View file

@ -85,12 +85,12 @@
Namespace {{ getNamespace(currentPageContext) }}
</li>
<li ng-if="isOrganization(getNamespace(currentPageContext)) && canAdmin(getNamespace(currentPageContext))">
<a ng-click="createTeam(currentPageContext)">
<a ng-click="askCreateTeam(currentPageContext)">
<i class="fa fa-group"></i> New Team
</a>
</li>
<li ng-if="canAdmin(getNamespace(currentPageContext))">
<a ng-click="createRobot(currentPageContext)">
<a ng-click="askCreateRobot(currentPageContext)">
<i class="fa ci-robot"></i> New Robot Account
</a>
</li>
@ -207,6 +207,14 @@
</ul>
</div>
<div class="create-robot-dialog" info="createRobotInfo"
robot-created="handleRobotCreated(robot, currentPageContext)">
</div>
<div class="create-team-dialog" info="createTeamInfo"
team-created="handleTeamCreated(team, currentPageContext)">
</div>
<div class="dockerfile-build-dialog"
show-now="showBuildDialogCounter"
repository="currentPageContext.repository"

View file

@ -30,7 +30,7 @@
</thead>
<tbody>
<tr ng-repeat="repository in orderedRepositories">
<tr ng-repeat="repository in orderedRepositories.entries">
<td class="repo-name-icon">
<span class="avatar" size="24" data="getAvatarData(repository.namespace)"></span>
<a href="/repository/{{ repository.namespace }}/{{ repository.name }}">

View file

@ -61,7 +61,7 @@
<span class="co-filter-box">
<span class="page-controls" total-count="tags.length" current-page="options.page" page-size="tagsPerPage"></span>
<input class="form-control" type="text" ng-model="options.tagFilter" placeholder="Filter Tags...">
<input class="form-control" type="text" ng-model="options.filter" placeholder="Filter Tags...">
</span>
</div>

View file

@ -106,6 +106,8 @@
<tr class="add-row" ng-if-media="'(min-width: 768px)'">
<td id="add-entity-permission" class="admin-search">
<span class="entity-search" namespace="repository.namespace"
for-repository="repository"
skip-permissions="true"
placeholder="'Select a ' + (repository.is_organization ? 'team or ' : '') + 'user...'"
current-entity="addPermissionInfo.entity"></span>
</td>
@ -125,6 +127,8 @@
<!-- Mobile add permissions -->
<div class="mobile-add-row" ng-if-media="'(max-width: 767px)'">
<span class="entity-search" namespace="repository.namespace"
for-repository="repository"
skip-permissions="true"
placeholder="'Select a ' + (repository.is_organization ? 'team or ' : '') + 'user...'"
current-entity="addPermissionInfo.entity"
pull-right="true"></span>

View file

@ -4,12 +4,9 @@
<div ng-show="!loading">
<div class="manager-header" header-title="Robot Accounts">
<span class="popup-input-button" pattern="ROBOT_PATTERN"
placeholder="'Robot Account Name'"
submitted="createRobot(value)"
ng-show="isEnabled">
<i class="fa fa-plus"></i> Create Robot Account
</span>
<button class="btn btn-primary" ng-click="askCreateRobot()" ng-show="isEnabled">
<i class="fa fa-plus" style="margin-right: 4px;"></i> Create Robot Account
</button>
</div>
<div class="section-description-header">
@ -134,5 +131,6 @@
</table>
</div>
<div class="create-robot-dialog" info="createRobotInfo" robot-created="robotCreated()"></div>
<div class="robot-credentials-dialog" info="robotDisplayInfo"></div>
</div>

View file

@ -13,23 +13,17 @@
</button>
</div>
</div>
<span class="popup-input-button visible-xs" ng-if="!showingMembers"
pattern="TEAM_PATTERN" placeholder="'Team Name'"
submitted="createTeam(value)" ng-show="organization.is_admin">
<i class="fa fa-plus" style="margin-right: 6px;"></i> Create New Team
</span>
</div>
<!-- Teams List -->
<div ng-show="!showingMembers">
<div class="row" style="margin-left: 0px; margin-right: 0px;">
<span class="popup-input-button hidden-xs"
pattern="TEAM_PATTERN" placeholder="'Team Name'"
submitted="createTeam(value)" ng-show="organization.is_admin"
style="margin-bottom: 10px;">
<i class="fa fa-plus" style="margin-right: 6px;"></i> Create New Team
</span>
<button class="btn btn-primary hidden-xs"
ng-show="organization.is_admin"
style="margin-bottom: 10px; float: right;"
ng-click="askCreateTeam()">
<i class="fa fa-plus" style="margin-right: 4px;"></i> Create New Team
</button>
</div>
<div class="row hidden-xs">
@ -155,6 +149,8 @@
</table>
</div>
<div class="create-team-dialog" info="createTeamInfo" team-created="handleTeamCreated(team)"></div>
<!-- Remove member confirm -->
<div class="cor-confirm-dialog"
dialog-context="removeMemberInfo"

View file

@ -18,9 +18,7 @@ angular.module('quay').directive('repoPanelTags', function () {
'getImages': '&getImages'
},
controller: function($scope, $element, $filter, $location, ApiService, UIService, VulnerabilityService) {
var orderBy = $filter('orderBy');
controller: function($scope, $element, $filter, $location, ApiService, UIService, VulnerabilityService, TableService) {
$scope.maxTrackCount = 5;
$scope.checkedTags = UIService.createCheckStateController([], 'name');
@ -45,32 +43,26 @@ angular.module('quay').directive('repoPanelTags', function () {
var setTagState = function() {
if (!$scope.repository || !$scope.selectedTags) { return; }
var tags = [];
// Build a list of all the tags, with extending information.
var allTags = [];
// Build a list of tags and filtered tags.
for (var tag in $scope.repository.tags) {
if (!$scope.repository.tags.hasOwnProperty(tag)) { continue; }
var tagData = $scope.repository.tags[tag];
var tagInfo = $.extend(tagData, {
'name': tag,
'last_modified_datetime': (new Date(tagData.last_modified || 0)).valueOf() * (-1)
'last_modified_datetime': TableService.getReversedTimestamp(tagData.last_modified)
});
allTags.push(tagInfo);
if (!$scope.options.tagFilter || tag.indexOf($scope.options.tagFilter) >= 0 ||
tagInfo.image_id.indexOf($scope.options.tagFilter) >= 0) {
tags.push(tagInfo);
}
}
// Sort the tags by the predicate and the reverse, and map the information.
var imageIDs = [];
var ordered = orderBy(tags, $scope.options.predicate, $scope.options.reverse);
var checked = [];
var ordered = TableService.buildOrderedItems(allTags, $scope.options,
['name'], ['last_modified_datetime', 'size']).entries;
var checked = [];
var imageMap = {};
var imageIndexMap = {};
for (var i = 0; i < ordered.length; ++i) {
@ -175,7 +167,7 @@ angular.module('quay').directive('repoPanelTags', function () {
$scope.$watch('options.predicate', setTagState);
$scope.$watch('options.reverse', setTagState);
$scope.$watch('options.tagFilter', setTagState);
$scope.$watch('options.filter', setTagState);
$scope.$watch('options.page', function(page) {
if (page != null && $scope.checkedTags) {

View file

@ -0,0 +1,202 @@
/**
* 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',
'entityCreateRequested': '&entityCreateRequested',
'entityCreateCompleted': '&entityCreateCompleted'
},
controller: function($scope, $element, ApiService, UIService, TableService, RolesService, UserService) {
$scope.TableService = TableService;
$scope.options = {
'predicate': 'last_modified_datetime',
'reverse': false,
'filter': ''
};
var handleRepoCheckChange = function() {
$scope.repositories.forEach(function(repo) {
if ($scope.checkedRepos.isChecked(repo)) {
if (repo['permission'] == 'none') {
repo['permission'] = 'read';
}
} else {
repo['permission'] = 'none';
}
});
};
$scope.$on('$destroy', function() {
if ($scope.inBody) {
document.body.removeChild($element[0]);
}
});
$scope.setRole = function(role, repo) {
repo['permission'] = role;
if (role == 'none') {
$scope.checkedRepos.uncheckItem(repo);
} else {
$scope.checkedRepos.checkItem(repo);
}
};
$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.entity = 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 setRepoState = function() {
if (!$scope.repositories) {
return;
}
$scope.orderedRepositories = TableService.buildOrderedItems($scope.repositories, $scope.options,
['name', 'permission'],
['last_modified_datetime']);
};
var entityCreateCallback = function(entity) {
if (!entity || $scope.info.skip_permissions) {
$scope.entity = entity;
$scope.hide();
return;
}
// Load the repositories under the entity's namespace.
var params = {
'namespace': $scope.info.namespace,
'last_modified': true
};
ApiService.listRepos(null, params).then(function(resp) {
$scope.view = 'addperms';
$scope.entity = entity;
var repos = [];
resp['repositories'].forEach(function(repo) {
repos.push({
'namespace': repo.namespace,
'name': repo.name,
'last_modified': repo.last_modified,
'last_modified_datetime': TableService.getReversedTimestamp(repo.last_modified),
'permission': 'none'
});
});
if (repos.length == 0) {
$scope.hide();
return;
}
$scope.repositories = repos;
$scope.checkedRepos = UIService.createCheckStateController($scope.repositories, 'name');
$scope.checkedRepos.listen(handleRepoCheckChange);
if ($scope.info.repository) {
repos.forEach(function(repo) {
if (repo['namespace'] == $scope.info.repository.namespace &&
repo['name'] == $scope.info.repository.name) {
$scope.checkedRepos.checkItem(repo);
$scope.options.filter = $scope.info.repository.name;
}
});
}
setRepoState();
}, ApiService.errorDisplay('Could not load repositories'));
};
$scope.addPermissions = function() {
$scope.view = 'addingperms';
var repos = $scope.checkedRepos.checked;
var counter = 0;
var addPerm = function() {
if (counter >= repos.length) {
$scope.hide();
return;
}
var repo = repos[counter];
RolesService.setRepositoryRole(repo, repo.permission, $scope.entityKind, $scope.entity.name,
function(status) {
if (status) {
counter++;
addPerm();
} else {
$scope.hide();
}
});
};
addPerm();
};
$scope.createEntity = function() {
$scope.view = 'creating';
$scope.entityCreateRequested({
'name': $scope.entityName,
'callback': entityCreateCallback
});
};
$scope.$watch('options.predicate', setRepoState);
$scope.$watch('options.reverse', setRepoState);
$scope.$watch('options.filter', setRepoState);
$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;
});

View file

@ -0,0 +1,43 @@
/**
* 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) {
$scope.ROBOT_PATTERN = ROBOT_PATTERN;
$scope.robotFinished = function(robot) {
$scope.robotCreated({'robot': robot});
};
$scope.createRobot = function(name, callback) {
var organization = $scope.info.namespace;
if (!UserService.isOrganization(organization)) {
organization = null;
}
var params = {
'robot_shortname': name
};
var errorDisplay = ApiService.errorDisplay('Cannot create robot account', function() {
callback(null);
});
ApiService.createRobot(organization, null, params).then(function(resp) {
callback(resp);
}, errorDisplay);
};
}
};
return directiveDefinitionObject;
});

View file

@ -0,0 +1,44 @@
/**
* 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) {
$scope.TEAM_PATTERN = 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) {
callback(resp);
}, errorDisplay);
};
}
};
return directiveDefinitionObject;
});

View file

@ -22,6 +22,12 @@ angular.module('quay').directive('credentialsDialog', function () {
$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;
@ -36,6 +42,11 @@ angular.module('quay').directive('credentialsDialog', function () {
$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) {

View file

@ -18,6 +18,8 @@ angular.module('quay').directive('entitySearch', function () {
scope: {
'namespace': '=namespace',
'placeholder': '=placeholder',
'forRepository': '=forRepository',
'skipPermissions': '=skipPermissions',
// Default: ['user', 'team', 'robot']
'allowedEntities': '=allowedEntities',
@ -41,7 +43,7 @@ angular.module('quay').directive('entitySearch', function () {
// True if the menu should pull right.
'pullRight': '@pullRight'
},
controller: function($rootScope, $scope, $element, Restangular, UserService, ApiService, UtilService, Config, CreateService) {
controller: function($rootScope, $scope, $element, Restangular, UserService, ApiService, UtilService, Config) {
$scope.lazyLoading = true;
$scope.teams = null;
@ -55,6 +57,8 @@ angular.module('quay').directive('entitySearch', function () {
$scope.includeOrgs = false;
$scope.currentEntityInternal = $scope.currentEntity;
$scope.createRobotInfo = null;
$scope.createTeamInfo = null;
$scope.Config = Config;
@ -91,18 +95,30 @@ angular.module('quay').directive('entitySearch', function () {
}
};
$scope.createTeam = function() {
CreateService.askCreateTeam($scope.namespace, function(created) {
$scope.setEntity(created.name, 'team', false, created.avatar);
$scope.teams[created.name] = created;
});
$scope.askCreateTeam = function() {
$scope.createTeamInfo = {
'namespace': $scope.namespace,
'repository': $scope.forRepository,
'skip_permissions': $scope.skipPermissions
};
};
$scope.createRobot = function() {
CreateService.askCreateRobot($scope.namespace, function(created) {
$scope.setEntity(created.name, 'user', true, created.avatar);
$scope.robots.push(created);
});
$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);
$scope.teams[created.name] = created;
};
$scope.handleRobotCreated = function(created) {
$scope.setEntity(created.name, 'user', true, created.avatar);
$scope.robots.push(created);
};
$scope.setEntity = function(name, kind, is_robot, avatar) {

View file

@ -13,7 +13,7 @@ angular.module('quay').directive('headerBar', function () {
scope: {
},
controller: function($rootScope, $scope, $element, $location, $timeout, hotkeys, UserService,
PlanService, ApiService, NotificationService, Config, CreateService, Features,
PlanService, ApiService, NotificationService, Config, Features,
DocumentationService, ExternalLoginService) {
$scope.externalSigninUrl = ExternalLoginService.getSingleSigninUrl();
@ -268,24 +268,36 @@ angular.module('quay').directive('headerBar', function () {
$location.url('/repository/' + context.repository.namespace + '/' + context.repository.name + '/build/' + build.id);
};
$scope.createRobot = function(context) {
$scope.handleRobotCreated = function(created, context) {
var namespace = $scope.getNamespace(context);
CreateService.askCreateRobot(namespace, function(created) {
if (UserService.isOrganization(namespace)) {
$location.url('/organization/' + namespace + '?tab=robots&showRobot=' + created.name);
} else {
$location.url('/user/' + namespace + '?tab=robots&showRobot=' + created.name);
}
});
if (UserService.isOrganization(namespace)) {
$location.url('/organization/' + namespace + '?tab=robots&showRobot=' + created.name);
} else {
$location.url('/user/' + namespace + '?tab=robots&showRobot=' + created.name);
}
};
$scope.createTeam = function(context) {
$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; }
CreateService.askCreateTeam(namespace, function(created) {
$location.url('/organization/' + namespace + '/teams/' + created.name);
});
$scope.createRobotInfo = {
'namespace': namespace
};
};
$scope.askCreateTeam = function(context) {
var namespace = $scope.getNamespace(context);
if (!namespace || !UserService.isNamespaceAdmin(namespace)) { return; }
$scope.createTeamInfo = {
'namespace': namespace
};
};
}
};

View file

@ -13,30 +13,22 @@ angular.module('quay').directive('repoListTable', function () {
'namespaces': '=namespaces',
'starToggled': '&starToggled'
},
controller: function($scope, $element, $filter) {
var orderBy = $filter('orderBy');
controller: function($scope, $element, $filter, TableService) {
$scope.repositories = null;
$scope.orderedRepositories = [];
$scope.maxPopularity = 0;
$scope.options = {
'predicate': 'popularity',
'reverse': true
'reverse': false,
'filter': null
};
var buildOrderedRepositories = function() {
if (!$scope.repositories) { return; }
var modifier = $scope.options.reverse ? '-' : '';
var fields = [modifier + $scope.options.predicate];
// Secondary ordering by full name.
if ($scope.options.predicate != 'full_name') {
fields.push('full_name');
}
var ordered = orderBy($scope.repositories, fields, false);
$scope.orderedRepositories = ordered;
$scope.orderedRepositories = TableService.buildOrderedItems($scope.repositories, $scope.options,
[], ['last_modified_datetime', 'popularity'])
};
$scope.tablePredicateClass = function(name, predicate, reverse) {
@ -92,7 +84,7 @@ angular.module('quay').directive('repoListTable', function () {
(resource.value || []).forEach(function(repository) {
var repositoryInfo = $.extend(repository, {
'full_name': repository.namespace + '/' + repository.name,
'last_modified_datetime': (new Date(repository.last_modified || 0)).valueOf() * (-1)
'last_modified_datetime': TableService.getReversedTimestamp(repository.last_modified),
});
$scope.repositories.push(repositoryInfo);

View file

@ -28,7 +28,7 @@ angular.module('quay').directive('repositoryPermissionsTable', function () {
'repository': '=repository',
'isEnabled': '=isEnabled'
},
controller: function($scope, $element, ApiService, Restangular, UtilService, RolesService) {
controller: function($scope, $element, ApiService, RolesService) {
$scope.permissionResources = {'team': {}, 'user': {}};
$scope.permissionCache = {};
$scope.permissions = {};
@ -69,13 +69,6 @@ angular.module('quay').directive('repositoryPermissionsTable', function () {
loadAllPermissions();
var getPermissionEndpoint = function(entityName, kind) {
var namespace = $scope.repository.namespace;
var name = $scope.repository.name;
var url = UtilService.getRestUrl('repository', namespace, name, 'permissions', kind, entityName);
return Restangular.one(url);
};
$scope.buildEntityForPermission = function(permission, kind) {
var key = permission.name + ':' + kind;
if ($scope.permissionCache[key]) {
@ -146,51 +139,36 @@ angular.module('quay').directive('repositoryPermissionsTable', function () {
};
$scope.deleteRole = function(entityName, kind) {
var errorHandler = ApiService.errorDisplay('Cannot change permission', function(resp) {
if (resp.status == 409) {
return 'Cannot change permission as you do not have the authority';
RolesService.deleteRepositoryRole($scope.repository, kind, entityName, function(status) {
if (status) {
delete $scope.permissions[kind][entityName];
}
});
var endpoint = getPermissionEndpoint(entityName, kind);
endpoint.customDELETE().then(function() {
delete $scope.permissions[kind][entityName];
}, errorHandler);
};
$scope.addRole = function(entityName, role, kind, opt_callback) {
var permission = {
'role': role,
};
var errorHandler = ApiService.errorDisplay('Cannot change permission', function() {
opt_callback && opt_callback(false);
RolesService.setRepositoryRole($scope.repository, role, kind, entityName, function(status, result) {
$scope.addPermissionInfo = {
'role': readRole
};
if (status) {
$scope.permissions[kind][entityName] = result;
}
opt_callback && opt_callback(status);
});
var endpoint = getPermissionEndpoint(entityName, kind);
endpoint.customPUT(permission).then(function(result) {
$scope.permissions[kind][entityName] = result;
$scope.addPermissionInfo = {
'role': readRole
};
opt_callback && opt_callback(true)
}, errorHandler);
};
$scope.setRole = function(role, entityName, kind) {
var errorDisplay = ApiService.errorDisplay(function(resp) {
$scope.permissions[kind][entityName] = {'role': currentRole};
var currentRole = $scope.permissions[kind][entityName].role;
RolesService.setRepositoryRole($scope.repository, role, kind, entityName, function(status) {
if (status) {
$scope.permissions[kind][entityName]['role'] = role;
} else {
$scope.permissions[kind][entityName]['role'] = currentRole;
}
});
var permission = $scope.permissions[kind][entityName];
var currentRole = permission.role;
permission.role = role;
var endpoint = getPermissionEndpoint(entityName, kind);
endpoint.customPUT(permission).then(function() {}, errorDisplay);
};
}
};

View file

@ -13,15 +13,14 @@ angular.module('quay').directive('robotsManager', function () {
'user': '=user',
'isEnabled': '=isEnabled'
},
controller: function($scope, $element, ApiService, $routeParams, $location, CreateService,
Config, $rootScope) {
$scope.ROBOT_PATTERN = ROBOT_PATTERN;
controller: function($scope, $element, ApiService, $routeParams, $location, Config, $rootScope) {
$scope.robots = null;
$scope.loading = false;
$scope.Config = Config;
$scope.feedback = null;
$scope.robotDisplayInfo = null;
$scope.createRobotInfo = null;
// Listen for route changes and update the tabs accordingly.
var locationListener = $rootScope.$on('$routeUpdate', function(){
@ -96,22 +95,10 @@ angular.module('quay').directive('robotsManager', function () {
return name.substr(0, plus);
};
$scope.createRobot = function(name) {
if (!name) { return; }
CreateService.createRobotAccount(ApiService, !!$scope.organization, $scope.organization ? $scope.organization.name : '', name,
function(created) {
created.teams = [];
created.repositories = [];
$scope.robots.push(created);
$scope.feedback = {
'kind': 'success',
'message': 'Robot account {robot} was created',
'data': {
'robot': name
}
};
});
$scope.askCreateRobot = function() {
$scope.createRobotInfo = {
'namespace': $scope.organization ? $scope.organization.name : $scope.user.username
};
};
$scope.deleteRobot = function(info) {
@ -131,7 +118,6 @@ angular.module('quay').directive('robotsManager', function () {
}, ApiService.errorDisplay('Cannot delete robot account'));
};
$scope.askDeleteRobot = function(info) {
bootbox.confirm('Are you sure you want to delete robot ' + info.name + '?', function(resp) {
if (resp) {
@ -140,6 +126,10 @@ angular.module('quay').directive('robotsManager', function () {
});
};
$scope.robotCreated = function() {
update();
};
var update = function() {
if (!$scope.user && !$scope.organization) { return; }
if ($scope.loading || !$scope.isEnabled) { return; }

View file

@ -12,8 +12,7 @@ angular.module('quay').directive('teamsManager', function () {
'organization': '=organization',
'isEnabled': '=isEnabled'
},
controller: function($scope, $element, ApiService, CreateService, $timeout, UserService) {
$scope.TEAM_PATTERN = TEAM_PATTERN;
controller: function($scope, $element, ApiService, $timeout, UserService) {
$scope.teamRoles = [
{ 'id': 'member', 'title': 'Member', 'kind': 'default' },
{ 'id': 'creator', 'title': 'Creator', 'kind': 'success' },
@ -27,6 +26,7 @@ angular.module('quay').directive('teamsManager', function () {
$scope.showingMembers = false;
$scope.fullMemberList = null;
$scope.feedback = null;
$scope.createTeamInfo = null;
var loadTeamMembers = function() {
if (!$scope.organization || !$scope.isEnabled) { return; }
@ -107,35 +107,27 @@ angular.module('quay').directive('teamsManager', function () {
}, errorHandler);
};
$scope.createTeam = function(teamname) {
if (!teamname) {
return;
}
$scope.askCreateTeam = function(teamname) {
$scope.createTeamInfo = {
'namespace': $scope.organization.name
};
};
if ($scope.organization.teams[teamname]) {
$('#team-' + teamname).removeClass('highlight');
setTimeout(function() {
$('#team-' + teamname).addClass('highlight');
}, 10);
return;
}
$scope.handleTeamCreated = function(created) {
var teamname = created.name;
$scope.organization.teams[teamname] = created;
$scope.members[teamname] = {};
$scope.members[teamname].members = [];
$scope.organization.ordered_teams.push(teamname);
$scope.orderedTeams.push(created);
var orgname = $scope.organization.name;
CreateService.createOrganizationTeam(ApiService, orgname, teamname, function(created) {
$scope.organization.teams[teamname] = created;
$scope.members[teamname] = {};
$scope.members[teamname].members = [];
$scope.organization.ordered_teams.push(teamname);
$scope.orderedTeams.push(created);
$scope.feedback = {
'kind': 'success',
'message': 'Team {team} created',
'data': {
'team': teamname
}
};
});
$scope.feedback = {
'kind': 'success',
'message': 'Team {team} created',
'data': {
'team': teamname
}
};
};
$scope.askDeleteTeam = function(teamname) {

View file

@ -41,6 +41,7 @@
var loadUser = function() {
$scope.userResource = ApiService.getUserInformationAsResource({'username': username}).get(function(user) {
$scope.context.viewuser = user;
$scope.viewuser = user;
// Load the repositories.
$timeout(function() {

View file

@ -1,61 +0,0 @@
/**
* Service which exposes various methods for creating entities on the backend.
*/
angular.module('quay').factory('CreateService', ['ApiService', 'UserService', function(ApiService, UserService) {
var createService = {};
createService.createRobotAccount = function(ApiService, is_org, orgname, name, callback) {
ApiService.createRobot(is_org ? orgname : null, null, {'robot_shortname': name})
.then(callback, ApiService.errorDisplay('Cannot create robot account'));
};
createService.createOrganizationTeam = function(ApiService, orgname, teamname, callback) {
var data = {
'name': teamname,
'role': 'member'
};
var params = {
'orgname': orgname,
'teamname': teamname
};
ApiService.updateOrganizationTeam(data, params)
.then(callback, ApiService.errorDisplay('Cannot create team'));
};
createService.askCreateRobot = function(namespace, callback) {
if (!namespace || !UserService.isNamespaceAdmin(namespace)) { return; }
var isorg = UserService.isOrganization(namespace);
bootbox.prompt('Enter the name of the new robot account', function(robotname) {
if (!robotname) { return; }
var regex = new RegExp(ROBOT_PATTERN);
if (!regex.test(robotname)) {
bootbox.alert('Invalid robot account name');
return;
}
createService.createRobotAccount(ApiService, isorg, namespace, robotname, callback);
});
};
createService.askCreateTeam = function(namespace, callback) {
if (!namespace || !UserService.isNamespaceAdmin(namespace)) { return; }
bootbox.prompt('Enter the name of the new team', function(teamname) {
if (!teamname) { return; }
var regex = new RegExp(TEAM_PATTERN);
if (!regex.test(teamname)) {
bootbox.alert('Invalid team name');
return;
}
createService.createOrganizationTeam(ApiService, namespace, teamname, callback);
});
};
return createService;
}]);

View file

@ -1,20 +1,57 @@
/**
* Service which defines the various role groups.
*/
angular.module('quay').factory('RolesService', [function() {
angular.module('quay').factory('RolesService', ['UtilService', 'Restangular', 'ApiService', function(UtilService, Restangular, ApiService) {
var roleService = {};
roleService.repoRoles = [
roleService.repoRolesOrNone = [
{ 'id': 'none', 'title': 'None', 'kind': 'default', 'description': 'No permissions on the repository' },
{ 'id': 'read', 'title': 'Read', 'kind': 'success', 'description': 'Can view and pull from the repository' },
{ 'id': 'write', 'title': 'Write', 'kind': 'success', 'description': 'Can view, pull and push to the repository' },
{ 'id': 'admin', 'title': 'Admin', 'kind': 'primary', 'description': 'Full admin access, pull and push on the repository' }
];
roleService.repoRoles = roleService.repoRolesOrNone.slice(1);
roleService.teamRoles = [
{ 'id': 'member', 'title': 'Member', 'kind': 'default', 'description': 'Inherits all permissions of the team' },
{ 'id': 'creator', 'title': 'Creator', 'kind': 'success', 'description': 'Member and can create new repositories' },
{ 'id': 'admin', 'title': 'Admin', 'kind': 'primary', 'description': 'Full admin access to the organization' }
];
var getPermissionEndpoint = function(repository, entityName, kind) {
var namespace = repository.namespace;
var name = repository.name;
var url = UtilService.getRestUrl('repository', namespace, name, 'permissions', kind, entityName);
return Restangular.one(url);
};
roleService.deleteRepositoryRole = function(repository, entityKind, entityName, callback) {
var errorDisplay = ApiService.errorDisplay('Cannot change permission', function(resp) {
callback(false);
});
var endpoint = getPermissionEndpoint(repository, entityName, kind);
endpoint.customDELETE().then(function() {
callback(true);
}, errorHandler);
};
roleService.setRepositoryRole = function(repository, role, entityKind, entityName, callback) {
var errorDisplay = ApiService.errorDisplay('Cannot change permission', function(resp) {
callback(false);
});
var permission = {
'role': role
};
var endpoint = getPermissionEndpoint(repository, entityName, entityKind);
endpoint.customPUT(permission).then(function(resp) {
callback(true, resp);
}, errorDisplay);
};
return roleService;
}]);

View file

@ -22,6 +22,14 @@ angular.module('quay').factory('TableService', ['AngularViewArray', function(Ang
options.predicate = predicate;
};
tableService.getReversedTimestamp = function(datetime) {
if (!datetime) {
return -Number.MAX_VALUE;
}
return (new Date(datetime)).valueOf() * (-1);
};
tableService.buildOrderedItems = function(items, options, filterFields, numericFields, opt_extrafilter) {
var orderedItems = AngularViewArray.create();

View file

@ -82,7 +82,7 @@ angular.module('quay').factory('UIService', ['$timeout', '$rootScope', '$locatio
CheckStateController.prototype.rebuildCheckedList_ = function() {
var that = this;
this.checked = [];
this.items.forEach(function(item) {
this.allItems_.forEach(function(item) {
if (that.allCheckedMap_[item[that.itemKey_]]) {
that.checked.push(item);
}

View file

@ -134,6 +134,19 @@ function(ApiService, CookieService, $rootScope, Config) {
return !!org;
};
userService.getNamespace = function(namespace) {
var org = userService.getOrganization(namespace);
if (org) {
return org;
}
if (namespace == userResponse.username) {
return userResponse;
}
return null;
};
userService.currentUser = function() {
return userResponse;
};