true if Keycloak is present.
+ *
+ * @returns {boolean}
+ */
+ isKeycloakPresent(): boolean {
+ return this.cheKeycloak.isPresent();
+ }
+
+ /**
+ * Opens user profile in new browser page.
+ */
+ gotoProfile(): void {
+ const url = this.cheKeycloak.getProfileUrl();
+ this.$window.open(url);
+ }
+
+ /**
+ * Logout.
+ */
+ logout(): void {
+ this.cheKeycloak.logout();
+ }
+
}
diff --git a/dashboard/src/app/navbar/navbar.directive.ts b/dashboard/src/app/navbar/navbar.directive.ts
index 0baa7dbb76..338666e395 100644
--- a/dashboard/src/app/navbar/navbar.directive.ts
+++ b/dashboard/src/app/navbar/navbar.directive.ts
@@ -30,7 +30,7 @@ export class CheNavBar {
this.replace = false;
this.templateUrl = 'app/navbar/navbar.html';
this.controller = 'CheNavBarController';
- this.controllerAs = 'navbarCtrl';
+ this.controllerAs = 'navbarController';
}
}
diff --git a/dashboard/src/app/navbar/navbar.html b/dashboard/src/app/navbar/navbar.html
index a4b4922d1f..b98ad2d8f9 100644
--- a/dashboard/src/app/navbar/navbar.html
+++ b/dashboard/src/app/navbar/navbar.html
@@ -12,8 +12,9 @@
-->
true if allowed
+ */
+ isUserAllowedTo(value: string): boolean {
+ if (value === this.organizationActions.UPDATE && this.isPersonalOrganization()) {
+ return false;
+ }
+ return this.allowedUserActions ? this.allowedUserActions.indexOf(value) >= 0 : false;
+ }
+
+ /**
+ * Checks for personal.
+ *
+ * @returns {boolean} true if personal
+ */
+ isPersonalOrganization(): boolean {
+ let user = this.cheUser.getUser();
+ return this.organization && user && this.organization.qualifiedName === user.name;
+ }
+
+ /**
+ * Checks for root.
+ *
+ * @returns {boolean} true if root
+ */
+ isRootOrganization(): boolean {
+ return this.organization && !this.organization.parent;
+ }
+
+ /**
+ * Returns whether current user can change organization resource limits.
+ *
+ * @returns {boolean} true if can change resource limits
+ */
+ canChangeResourceLimits(): boolean {
+ if (this.isRootOrganization()) {
+ return this.chePermissions.getUserServices().hasAdminUserService;
+ }
+ return this.organizationsPermissionService.isUserAllowedTo(this.organizationActions.MANAGE_RESOURCES, this.organization.parent);
+ }
+
+ /**
+ * Check if the name is unique.
+ * @param name
+ * @returns {boolean}
+ */
+ isUniqueName(name: string): boolean {
+ let currentOrganizationName = this.organization.name;
+ let organizations = this.cheOrganization.getOrganizations();
+ let account = '';
+ let parentId = this.organization.parent;
+ if (parentId) {
+ let parent = this.cheOrganization.getOrganizationById(parentId);
+ if (parent && parent.qualifiedName) {
+ account = parent.qualifiedName + '/';
+ }
+ }
+ if (organizations.length && currentOrganizationName !== name) {
+ for (let i = 0; i < organizations.length; i++) {
+ if (organizations[i].qualifiedName === account + name) {
+ return false;
+ }
+ }
+ return true;
+ } else {
+ return true;
+ }
+ }
+
+ /**
+ * Fetches defined organization's limits (workspace, runtime, RAM caps, etc).
+ */
+ fetchLimits(): void {
+ this.isLoading = true;
+ this.cheResourcesDistribution.fetchOrganizationResources(this.organization.id).then(() => {
+ this.isLoading = false;
+ this.processResources();
+ }, (error: any) => {
+ this.isLoading = false;
+ this.limits = {};
+ this.limitsCopy = angular.copy(this.limits);
+ });
+ }
+
+ /**
+ * Process resources limits.
+ */
+ processResources(): void {
+ let ramLimit = this.cheResourcesDistribution.getOrganizationResourceByType(this.organization.id, this.resourceLimits.RAM);
+ let workspaceLimit = this.cheResourcesDistribution.getOrganizationResourceByType(this.organization.id, this.resourceLimits.WORKSPACE);
+ let runtimeLimit = this.cheResourcesDistribution.getOrganizationResourceByType(this.organization.id, this.resourceLimits.RUNTIME);
+
+ this.limits = {};
+ this.limits.workspaceCap = workspaceLimit ? workspaceLimit.amount : undefined;
+ this.limits.runtimeCap = runtimeLimit ? runtimeLimit.amount : undefined;
+ this.limits.ramCap = ramLimit ? ramLimit.amount / 1024 : undefined;
+ this.limitsCopy = angular.copy(this.limits);
+ }
+
+
+ /**
+ * Fetches total resources of the organization (workspace, runtime, RAM caps, etc).
+ */
+ fetchTotalResources(): void {
+ this.isLoading = true;
+ this.cheResourcesDistribution.fetchTotalOrganizationResources(this.organization.id).then(() => {
+ this.isLoading = false;
+ this.processTotalResources();
+ }, (error: any) => {
+ this.isLoading = false;
+ this.limits = {};
+ this.limitsCopy = angular.copy(this.limits);
+ });
+ }
+
+ /**
+ * Process organization's total resources.
+ */
+ processTotalResources(): void {
+ let ram = this.cheResourcesDistribution.getOrganizationTotalResourceByType(this.organization.id, this.resourceLimits.RAM);
+ let workspace = this.cheResourcesDistribution.getOrganizationTotalResourceByType(this.organization.id, this.resourceLimits.WORKSPACE);
+ let runtime = this.cheResourcesDistribution.getOrganizationTotalResourceByType(this.organization.id, this.resourceLimits.RUNTIME);
+
+ this.totalResources = {};
+ this.totalResources.workspaceCap = (workspace && workspace.amount !== -1) ? workspace.amount : undefined;
+ this.totalResources.runtimeCap = (runtime && runtime.amount !== -1) ? runtime.amount : undefined;
+ this.totalResources.ramCap = (ram && ram.amount !== -1) ? ram.amount / 1024 : undefined;
+ this.totalResourcesCopy = angular.copy(this.totalResources);
+ }
+
+ /**
+ * Confirms and performs organization's deletion.
+ */
+ deleteOrganization(): void {
+ let promise = this.confirmDialogService.showConfirmDialog('Delete organization',
+ 'Would you like to delete organization \'' + this.organization.name + '\'?', 'Delete');
+
+ promise.then(() => {
+ let promise = this.cheOrganization.deleteOrganization(this.organization.id);
+ promise.then(() => {
+ this.$location.path('/organizations');
+
+ }, (error: any) => {
+ this.cheNotification.showError(error.data.message !== null ? error.data.message : 'Team deletion failed.');
+ });
+ });
+ }
+
+ /**
+ * Update organization's details.
+ *
+ */
+ updateOrganizationName(): void {
+ if (this.newName && this.organization && this.newName !== this.organization.name) {
+ this.organization.name = this.newName;
+ this.cheOrganization.updateOrganization(this.organization).then((organization: che.IOrganization) => {
+ this.cheOrganization.fetchOrganizations().then(() => {
+ this.$location.path('/organization/' + organization.qualifiedName);
+ });
+ }, (error: any) => {
+ this.cheNotification.showError((error.data && error.data.message !== null) ? error.data.message : 'Rename organization failed.');
+ });
+ }
+ }
+
+ /**
+ * Update resource limits.
+ */
+ updateLimits(): void {
+ if (!this.organization || !this.limits || angular.equals(this.limits, this.limitsCopy)) {
+ return;
+ }
+ let resources = angular.copy(this.cheResourcesDistribution.getOrganizationResources(this.organization.id));
+
+ let resourcesToRemove = [this.resourceLimits.TIMEOUT];
+ if (this.limits.ramCap !== null && this.limits.ramCap !== undefined) {
+ resources = this.cheResourcesDistribution.setOrganizationResourceLimitByType(resources, this.resourceLimits.RAM, (this.limits.ramCap * 1024).toString());
+ } else {
+ resourcesToRemove.push(this.resourceLimits.RAM);
+ }
+ if (this.limits.workspaceCap !== null && this.limits.workspaceCap !== undefined) {
+ resources = this.cheResourcesDistribution.setOrganizationResourceLimitByType(resources, this.resourceLimits.WORKSPACE, this.limits.workspaceCap);
+ } else {
+ resourcesToRemove.push(this.resourceLimits.WORKSPACE);
+ }
+ if (this.limits.runtimeCap !== null && this.limits.runtimeCap !== undefined) {
+ resources = this.cheResourcesDistribution.setOrganizationResourceLimitByType(resources, this.resourceLimits.RUNTIME, this.limits.runtimeCap);
+ } else {
+ resourcesToRemove.push(this.resourceLimits.RUNTIME);
+ }
+ // if the timeout resource will be send in this case - it will set the timeout for the current organization, and the updating timeout of
+ // parent organization will not affect the current organization, so to avoid this - remove timeout resource if present:
+ this.lodash.remove(resources, (resource: any) => {
+ return resourcesToRemove.indexOf(resource.type) >= 0;
+ });
+
+ this.isLoading = true;
+ this.cheResourcesDistribution.distributeResources(this.organization.id, resources).then(() => {
+ this.fetchLimits();
+ }, (error: any) => {
+ let errorMessage = 'Failed to set update organization CAPs.';
+ this.cheNotification.showError((error.data && error.data.message !== null) ? errorMessage + 'Reason: ' + error.data.message : errorMessage);
+ this.fetchLimits();
+ });
+ }
+
+ /**
+ * Update resource limits.
+ */
+ updateTotalResources(): void {
+ if (!this.organization || !this.totalResources || angular.equals(this.totalResources, this.totalResourcesCopy)) {
+ return;
+ }
+ let resources = angular.copy(this.cheResourcesDistribution.getTotalOrganizationResources(this.organization.id));
+
+ let resourcesToRemove = [this.resourceLimits.TIMEOUT];
+ if (this.totalResources.ramCap !== null && this.totalResources.ramCap !== undefined) {
+ resources = this.cheResourcesDistribution.setOrganizationResourceLimitByType(resources, this.resourceLimits.RAM, (this.totalResources.ramCap * 1024).toString());
+ } else {
+ resources = this.cheResourcesDistribution.setOrganizationResourceLimitByType(resources, this.resourceLimits.RAM, '-1');
+ }
+ if (this.totalResources.workspaceCap !== null && this.totalResources.workspaceCap !== undefined) {
+ resources = this.cheResourcesDistribution.setOrganizationResourceLimitByType(resources, this.resourceLimits.WORKSPACE, this.totalResources.workspaceCap);
+ } else {
+ resources = this.cheResourcesDistribution.setOrganizationResourceLimitByType(resources, this.resourceLimits.WORKSPACE, '-1');
+ }
+ if (this.totalResources.runtimeCap !== null && this.totalResources.runtimeCap !== undefined) {
+ resources = this.cheResourcesDistribution.setOrganizationResourceLimitByType(resources, this.resourceLimits.RUNTIME, this.totalResources.runtimeCap);
+ } else {
+ resources = this.cheResourcesDistribution.setOrganizationResourceLimitByType(resources, this.resourceLimits.RUNTIME, '-1');
+ }
+ // if the timeout resource will be send in this case - it will set the timeout for the current organization, and the updating timeout of
+ // parent organization will not affect the current organization, so to avoid this - remove timeout resource if present:
+ this.lodash.remove(resources, (resource: any) => {
+ return resourcesToRemove.indexOf(resource.type) >= 0;
+ });
+
+ this.isLoading = true;
+
+ this.cheResourcesDistribution.updateTotalResources(this.organization.id, resources).then(() => {
+ this.fetchTotalResources();
+ }, (error: any) => {
+ let errorMessage = 'Failed to update organization CAPs.';
+ this.cheNotification.showError((error.data && error.data.message !== null) ? errorMessage + 'Reason: ' + error.data.message : errorMessage);
+ this.fetchTotalResources();
+ });
+ }
+
+ /**
+ * Returns whether save button is disabled.
+ *
+ * @return {boolean}
+ */
+ isSaveButtonDisabled(): boolean {
+ return !this.organizationForm || this.organizationForm.$invalid;
+ }
+
+ /**
+ * Returns true if "Save" button should be visible
+ *
+ * @return {boolean}
+ */
+ isSaveButtonVisible(): boolean {
+ return (this.selectedTabIndex === Tab.Settings && !this.isLoading) && (!angular.equals(this.organization.name, this.newName) ||
+ !angular.equals(this.limits, this.limitsCopy) || !angular.equals(this.totalResources, this.totalResourcesCopy));
+ }
+
+ /**
+ * Returns back button link and title.
+ *
+ * @returns {any} back button link
+ */
+ getBackButtonLink(): any {
+ if (this.organization && this.organization.parent) {
+ let parent = this.organization.qualifiedName.replace(/\/[^\/]+$/, '');
+ return {link: '#/organization/' + parent, title: parent};
+ } else {
+ return {link: '#/organizations', title: 'Organizations'};
+ }
+ }
+
+ updateOrganization(): void {
+ this.updateOrganizationName();
+ this.updateLimits();
+ this.updateTotalResources();
+ }
+
+ cancelChanges(): void {
+ this.newName = angular.copy(this.organization.name);
+ this.limits = angular.copy(this.limitsCopy);
+ this.totalResources = angular.copy(this.totalResourcesCopy);
+ }
+}
diff --git a/dashboard/src/app/organizations/organization-details/organization-details.html b/dashboard/src/app/organizations/organization-details/organization-details.html
new file mode 100644
index 0000000000..0de6b14ef2
--- /dev/null
+++ b/dashboard/src/app/organizations/organization-details/organization-details.html
@@ -0,0 +1,201 @@
+null if add new member is needed. (Comes from outside)
+ */
+ private member: che.IMember;
+ /**
+ * Role to be used, may be null if role is needed to be set. (Comes from outside)
+ */
+ private role: string;
+ /**
+ * Choosen role for user.
+ */
+ private newRole: string;
+ /**
+ * Dialog window title.
+ */
+ private title: string;
+ /**
+ * Title of operation button (Save or Add)
+ */
+ private buttonTitle: string;
+ /**
+ * Email validation error message.
+ */
+ private emailError: string;
+ /**
+ * todo
+ */
+ private organizationRoles: che.resource.ICheOrganizationRoles;
+
+ /**
+ * Default constructor that is using resource
+ * @ngInject for Dependency injection
+ */
+ constructor($q: ng.IQService, $mdDialog: angular.material.IDialogService, cheUser: any, cheOrganization: che.api.ICheOrganization, lodash: any, resourcesService: che.service.IResourcesService) {
+ this.$mdDialog = $mdDialog;
+ this.cheUser = cheUser;
+ this.cheOrganization = cheOrganization;
+ this.$q = $q;
+ this.lodash = lodash;
+ this.organizationRoles = resourcesService.getOrganizationRoles();
+
+ this.isProcessing = false;
+
+ this.emails = [];
+ this.members.forEach((member: che.IMember) => {
+ this.emails.push(member.email);
+ });
+
+ // role is set, need to add only user with this role:
+ if (this.role) {
+ this.email = '';
+ this.title = 'Add new ' + this.organizationRoles[this.role].title.toLowerCase();
+ this.buttonTitle = 'Add';
+ return;
+ }
+
+ this.roles = this.organizationRoles.getRoles();
+ if (this.member) {
+ this.title = 'Edit ' + this.member.name + ' roles';
+ this.buttonTitle = 'Save';
+ this.email = this.member.email;
+ let roles = cheOrganization.getRolesFromActions(this.member.permissions.actions);
+ this.newRole = (roles && roles.length > 0) ? roles[0].name : this.organizationRoles.MEMBER.name;
+ } else {
+ this.email = '';
+ this.title = 'Invite member to collaborate';
+ this.buttonTitle = 'Add';
+ this.newRole = this.organizationRoles.MEMBER.name;
+ }
+ }
+
+ /**
+ * Returns title of specified role.
+ *
+ * @param {string} roleName
+ * @returns {string}
+ */
+ getRoleTitle(roleName: string): string {
+ return this.organizationRoles[roleName].title;
+ }
+
+ /**
+ * Returns description of specified role.
+ *
+ * @param {string} roleName
+ * @returns {string}
+ */
+ getRoleDescription(roleName: string): string {
+ return this.organizationRoles[roleName].description;
+ }
+
+ /**
+ * Hides the add member dialog.
+ */
+ hide(): void {
+ this.$mdDialog.hide();
+ }
+
+ /**
+ * Checks whether entered email valid and is unique.
+ *
+ * @param value value with email(s) to check
+ * @returns {boolean} true if pointed email(s) are valid and not in the list yet
+ */
+ isValidEmail(value: string): boolean {
+ let emails = value.replace(/\s*,?\s+/g, ',').split(',');
+ for (let i = 0; i < emails.length; i++) {
+ // email is valid
+ let email = emails[i];
+ let emailRe = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i;
+ if (!emailRe.test(email)) {
+ this.emailError = `"${email}" is invalid email address.`;
+ return false;
+ }
+
+ // user has not been invited yet
+ if (this.emails.indexOf(email) >= 0) {
+ this.emailError = `User with email ${email} is already invited.`;
+ return false;
+ }
+
+ // user is a member of parent organization
+ if (this.parentOrganizationId && this.parentOrganizationMembers.indexOf(email) === -1) {
+ this.emailError = 'User with this email is not a member of parent organization.';
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Adds new member.
+ */
+ addMembers(): void {
+ let userRoleName = this.role ? this.role : this.newRole;
+ let emails = this.email.replace(/\s*,?\s+/g, ',').split(',');
+ // form the list of emails without duplicates and empty values:
+ let resultEmails = emails.reduce((array: Arraytrue if all users in list are checked.
+ * @returns {boolean}
+ */
+ isAllUsersSelected(): boolean {
+ return this.isAllSelected;
+ }
+
+ /**
+ * Returns true if all users in list are not checked.
+ * @returns {boolean}
+ */
+ isNoUsersSelected(): boolean {
+ return this.isNoSelected;
+ }
+
+ /**
+ * Make all users in list selected.
+ */
+ selectAllUsers(): void {
+ this.availableUsers.forEach((user: IOrganizationMember) => {
+ this.userSelectedStatus[user.id] = true;
+ });
+ }
+
+ /**
+ * Make all users in list deselected.
+ */
+ deselectAllUsers(): void {
+ this.availableUsers.forEach((user: IOrganizationMember) => {
+ this.userSelectedStatus[user.id] = false;
+ });
+ }
+
+ /**
+ * Change bulk selection value.
+ */
+ changeBulkSelection(): void {
+ if (this.isBulkChecked) {
+ this.deselectAllUsers();
+ this.isBulkChecked = false;
+ } else {
+ this.selectAllUsers();
+ this.isBulkChecked = true;
+ }
+ this.updateSelectedStatus();
+ }
+
+ /**
+ * Set selected status for user.
+ *
+ * @param {string} id user ID
+ * @param {boolean} isSelected
+ */
+ setSelectedStatus(id: string, isSelected: boolean) {
+ this.userSelectedStatus[id] = isSelected;
+ this.updateSelectedStatus();
+ }
+
+ /**
+ * Update members selected status.
+ */
+ updateSelectedStatus(): void {
+ this.isNoSelected = true;
+ this.isAllSelected = true;
+
+ Object.keys(this.userSelectedStatus).forEach((key: string) => {
+ if (this.userSelectedStatus[key]) {
+ this.isNoSelected = false;
+ } else {
+ this.isAllSelected = false;
+ }
+ });
+
+ if (this.isNoSelected) {
+ this.isBulkChecked = false;
+ return;
+ }
+
+ this.isBulkChecked = (this.isAllSelected && Object.keys(this.userSelectedStatus).length === this.availableUsers.length);
+ }
+}
+
diff --git a/dashboard/src/app/organizations/organization-details/organization-select-members-dialog/organization-select-members-dialog.html b/dashboard/src/app/organizations/organization-details/organization-select-members-dialog/organization-select-members-dialog.html
new file mode 100644
index 0000000000..ffc78fd65d
--- /dev/null
+++ b/dashboard/src/app/organizations/organization-details/organization-select-members-dialog/organization-select-members-dialog.html
@@ -0,0 +1,57 @@
+true if allowed
+ */
+ isUserAllowedTo(action: string, organizationId: string): boolean {
+ if (!organizationId || !action) {
+ return false;
+ }
+ let permissions = this.chePermissions.getOrganizationPermissions(organizationId);
+ if (!permissions) {
+ this.fetchPermissions(organizationId);
+ return false;
+ }
+ return !angular.isUndefined(permissions.find((permission: che.IPermissions) => {
+ return permission.userId === this.userId && permission.actions.indexOf(action.toString()) !== -1;
+ }));
+ }
+}
diff --git a/dashboard/src/app/organizations/organizations.controller.ts b/dashboard/src/app/organizations/organizations.controller.ts
new file mode 100644
index 0000000000..5d37710d5a
--- /dev/null
+++ b/dashboard/src/app/organizations/organizations.controller.ts
@@ -0,0 +1,122 @@
+/*
+ * Copyright (c) 2015-2017 Red Hat, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Red Hat, Inc. - initial API and implementation
+ */
+'use strict';
+
+const MAX_ITEMS = 12;
+
+/**
+ * @ngdoc controller
+ * @name organizations.controller:OrganizationsController
+ * @description This class is handling the controller for organizations
+ * @author Oleksii Orel
+ */
+export class OrganizationsController {
+ /**
+ * Promises service.
+ */
+ private $q: ng.IQService;
+ /**
+ * Organization API interaction.
+ */
+ private cheOrganization: che.api.ICheOrganization;
+ /**
+ * Service for displaying notifications.
+ */
+ private cheNotification: any;
+ /**
+ * Loading state of the page.
+ */
+ private isInfoLoading: boolean;
+ /**
+ * List of organizations.
+ */
+ private organizations: Arraytrue if all teams in list are checked.
+ *
+ * @returns {boolean}
+ */
+ isAllTeamsSelected(): boolean {
+ return this.isAllSelected;
+ }
+
+ /**
+ * Returns true if all teams in list are not checked.
+ *
+ * @returns {boolean}
+ */
+ isNoTeamsSelected(): boolean {
+ return this.isNoSelected;
+ }
+
+ /**
+ * Make all teams in list selected.
+ */
+ selectAllTeams(): void {
+ this.teams.forEach((team: any) => {
+ this.teamsSelectedStatus[team.id] = true;
+ });
+ }
+
+ /**
+ * Make all teams in list deselected.
+ */
+ deselectAllTeams(): void {
+ this.teams.forEach((team: any) => {
+ this.teamsSelectedStatus[team.id] = false;
+ });
+ }
+
+ /**
+ * Change bulk selection value.
+ */
+ changeBulkSelection(): void {
+ if (this.isBulkChecked) {
+ this.deselectAllTeams();
+ this.isBulkChecked = false;
+ } else {
+ this.selectAllTeams();
+ this.isBulkChecked = true;
+ }
+ this.updateSelectedStatus();
+ }
+
+ /**
+ * Update teams selected status.
+ */
+ updateSelectedStatus(): void {
+ this.isNoSelected = true;
+ this.isAllSelected = true;
+
+ Object.keys(this.teamsSelectedStatus).forEach((key: string) => {
+ if (this.teamsSelectedStatus[key]) {
+ this.isNoSelected = false;
+ } else {
+ this.isAllSelected = false;
+ }
+ });
+
+ if (this.isNoSelected) {
+ this.isBulkChecked = false;
+ return;
+ }
+
+ if (this.isAllSelected) {
+ this.isBulkChecked = true;
+ }
+ }
+
+ /**
+ * Redirects to new team creation page.
+ */
+ createNewTeam(): void {
+ this.$location.path('/team/create');
+ }
+
+ /**
+ * Delete all selected teams.
+ */
+ removeTeams(): void {
+ let teamsSelectedStatusKeys = Object.keys(this.teamsSelectedStatus);
+ let checkedTeamsKeys = [];
+
+ if (!teamsSelectedStatusKeys.length) {
+ this.cheNotification.showError('No such team.');
+ return;
+ }
+
+ teamsSelectedStatusKeys.forEach((key: any) => {
+ if (this.teamsSelectedStatus[key] === true) {
+ checkedTeamsKeys.push(key);
+ }
+ });
+
+ if (!checkedTeamsKeys.length) {
+ this.cheNotification.showError('No such team.');
+ return;
+ }
+
+ let confirmationPromise = this.showDeleteTeamsConfirmation(checkedTeamsKeys.length);
+ confirmationPromise.then(() => {
+ this.isLoading = true;
+ let promises = [];
+
+ checkedTeamsKeys.forEach((teamId: string) => {
+ this.teamsSelectedStatus[teamId] = false;
+
+ let promise = this.cheTeam.deleteTeam(teamId).then(() => {
+ //
+ }, (error: any) => {
+ this.cheNotification.showError(error && error.data && error.data.message ? error.data.message : 'Failed to delete team ' + teamId + '.');
+ });
+
+ promises.push(promise);
+ });
+
+ this.$q.all(promises).finally(() => {
+ this.fetchTeams();
+ this.updateSelectedStatus();
+ });
+ });
+ }
+
+ /**
+ * Show confirmation popup before teams deletion.
+ *
+ * @param numberToDelete number of teams to be deleted
+ * @returns {*}
+ */
+ showDeleteTeamsConfirmation(numberToDelete: number): ng.IPromisenull if add new member is needed. (Comes from outside)
+ */
+ private member: any;
+ /**
+ * Role to be used, may be null if role is needed to be set. (Comes from outside)
+ */
+ private role: any;
+ /**
+ * Choosen role for user.
+ */
+ private newRole: any;
+ /**
+ * Dialog window title.
+ */
+ private title: string;
+ /**
+ * Title of operation button (Save or Add)
+ */
+ private buttonTitle: string;
+ /**
+ * Email validation error message.
+ */
+ private emailError: string;
+
+ /**
+ * Default constructor that is using resource
+ * @ngInject for Dependency injection
+ */
+ constructor($mdDialog: angular.material.IDialogService, cheTeam: che.api.ICheTeam, cheUser: any, $q: ng.IQService, lodash: any) {
+ this.$mdDialog = $mdDialog;
+ this.cheTeam = cheTeam;
+ this.cheUser = cheUser;
+ this.$q = $q;
+ this.lodash = lodash;
+
+ this.isProcessing = false;
+
+ this.emails = [];
+ this.members.forEach((member: any) => {
+ this.emails.push(member.email);
+ });
+
+ // role is set, need to add only user with this role:
+ if (this.role) {
+ this.email = '';
+ this.title = 'Add new ' + this.role.title.toLowerCase();
+ this.buttonTitle = 'Add';
+ return;
+ }
+
+ this.roles = CheTeamRoles.getValues();
+ if (this.member) {
+ this.title = 'Edit ' + this.member.name + ' roles';
+ this.buttonTitle = 'Save';
+ this.email = this.member.email;
+ let roles = this.cheTeam.getRolesFromActions(this.member.permissions.actions);
+ this.newRole = (roles && roles.length > 0) ? angular.toJson(roles[0]) : angular.toJson(CheTeamRoles.TEAM_MEMBER);
+ } else {
+ this.email = '';
+ this.title = 'Invite member to collaborate';
+ this.buttonTitle = 'Add';
+ this.newRole = angular.toJson(CheTeamRoles.TEAM_MEMBER);
+ }
+ }
+
+ /**
+ * Hides the add member dialog.
+ */
+ hide(): void {
+ this.$mdDialog.hide();
+ }
+
+ /**
+ * Checks whether entered email valid and is unique.
+ *
+ * @param value value with email(s) to check
+ * @returns {boolean} true if pointed email(s) are valid and not in the list yet
+ */
+ isValidEmail(value: string): boolean {
+ // return this.emails.indexOf(email) < 0;
+ let emails = value.replace(/ /g, ',').split(',');
+ for (let i = 0; i < emails.length; i++) {
+ let email = emails[i];
+ let emailRe = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i;
+ if (!emailRe.test(email)) {
+ this.emailError = email + ' is invalid email address.';
+ return false;
+ }
+
+ if (this.emails.indexOf(email) >= 0) {
+ this.emailError = 'User with email ' + email + ' is already invited.';
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Adds new member.
+ */
+ addMembers(): void {
+ let userRoles = this.role ? [this.role] : [angular.fromJson(this.newRole)];
+
+
+ let emails = this.email.replace(/ /g, ',').split(',');
+ // form the list of emails without duplicates and empty values:
+ let resultEmails = emails.reduce((array: Arraytrue if allowed
+ */
+ isUserAllowedTo(value: string): boolean {
+ return this.allowedUserActions ? this.allowedUserActions.indexOf(value) >= 0 : false;
+ }
+
+ /**
+ * Returns whether current user can change team resource limits.
+ *
+ * @returns {boolean} true if can change resource limits
+ */
+ canChangeResourceLimits(): boolean {
+ return (this.cheTeam.getPersonalAccount() && this.team) ? this.cheTeam.getPersonalAccount().id === this.team.parent : false;
+ }
+
+ /**
+ * Returns whether current user can leave team (owner of the team is not allowed to leave it).
+ *
+ * @returns {boolean} true if can leave team
+ */
+ canLeaveTeam(): boolean {
+ return (this.cheTeam.getPersonalAccount() && this.team) ? this.cheTeam.getPersonalAccount().id !== this.team.parent : false;
+ }
+
+ /**
+ * Fetches defined team's limits (workspace, runtime, RAM caps, etc).
+ */
+ fetchLimits(): void {
+ this.isLoading = true;
+ this.cheResourcesDistribution.fetchOrganizationResources(this.team.id).then(() => {
+ this.isLoading = false;
+ this.processResources();
+ }, (error: any) => {
+ this.isLoading = false;
+ if (!error) {
+ return;
+ }
+ if (error.status === 304) {
+ this.processResources();
+ } else if (error.status === 404) {
+ this.limits = {};
+ this.limitsCopy = angular.copy(this.limits);
+ }
+ });
+ }
+
+ /**
+ * Process resources limits.
+ */
+ processResources(): void {
+ let ramLimit = this.cheResourcesDistribution.getOrganizationResourceByType(this.team.id, this.resourceLimits.RAM);
+ let workspaceLimit = this.cheResourcesDistribution.getOrganizationResourceByType(this.team.id, this.resourceLimits.WORKSPACE);
+ let runtimeLimit = this.cheResourcesDistribution.getOrganizationResourceByType(this.team.id, this.resourceLimits.RUNTIME);
+
+ this.limits = {};
+ this.limits.workspaceCap = workspaceLimit ? workspaceLimit.amount : undefined;
+ this.limits.runtimeCap = runtimeLimit ? runtimeLimit.amount : undefined;
+ this.limits.ramCap = ramLimit ? ramLimit.amount / 1024 : undefined;
+ this.limitsCopy = angular.copy(this.limits);
+ }
+
+ /**
+ * Confirms and performs team's deletion.
+ *
+ * @param event
+ */
+ deleteTeam(event: MouseEvent): void {
+ let promise = this.confirmDialogService.showConfirmDialog('Delete team',
+ 'Would you like to delete team \'' + this.team.name + '\'?', 'Delete');
+
+ promise.then(() => {
+ let promise = this.cheTeam.deleteTeam(this.team.id);
+ promise.then(() => {
+ this.$location.path('/');
+ this.cheTeam.fetchTeams();
+ }, (error: any) => {
+ this.cheNotification.showError(error.data.message !== null ? error.data.message : 'Team deletion failed.');
+ });
+ });
+ }
+
+ /**
+ * Confirms and performs removing user from current team.
+ *
+ */
+ leaveTeam(): void {
+ let promise = this.confirmDialogService.showConfirmDialog('Leave team',
+ 'Would you like to leave team \'' + this.team.name + '\'?', 'Leave');
+
+ promise.then(() => {
+ let promise = this.chePermissions.removeOrganizationPermissions(this.team.id, this.cheUser.getUser().id);
+ promise.then(() => {
+ this.$location.path('/');
+ this.cheTeam.fetchTeams();
+ }, (error: any) => {
+ this.cheNotification.showError(error.data.message !== null ? error.data.message : 'Leave team failed.');
+ });
+ });
+ }
+
+ /**
+ * Update team's details.
+ *
+ */
+ updateTeamName(): void {
+ if (this.newName && this.team && this.newName !== this.team.name) {
+ this.team.name = this.newName;
+ this.cheTeam.updateTeam(this.team).then((team: any) => {
+ this.cheTeam.fetchTeams().then(() => {
+ this.$location.path('/team/' + team.qualifiedName);
+ });
+ }, (error: any) => {
+ this.cheNotification.showError((error.data && error.data.message !== null) ? error.data.message : 'Rename team failed.');
+ });
+ }
+ }
+
+ /**
+ * Update resource limits.
+ *
+ */
+ updateLimits(): void {
+ if (!this.team || !this.limits || angular.equals(this.limits, this.limitsCopy)) {
+ return;
+ }
+
+ let resources = angular.copy(this.cheResourcesDistribution.getOrganizationResources(this.team.id));
+
+ let resourcesToRemove = [this.resourceLimits.TIMEOUT];
+
+ if (this.limits.ramCap !== null && this.limits.ramCap !== undefined) {
+ resources = this.cheResourcesDistribution.setOrganizationResourceLimitByType(resources, this.resourceLimits.RAM, (this.limits.ramCap * 1024).toString());
+ } else {
+ resourcesToRemove.push(this.resourceLimits.RAM);
+ }
+
+ if (this.limits.workspaceCap !== null && this.limits.workspaceCap !== undefined) {
+ resources = this.cheResourcesDistribution.setOrganizationResourceLimitByType(resources, this.resourceLimits.WORKSPACE, this.limits.workspaceCap);
+ } else {
+ resourcesToRemove.push(this.resourceLimits.WORKSPACE);
+ }
+
+ if (this.limits.runtimeCap !== null && this.limits.runtimeCap !== undefined) {
+ resources = this.cheResourcesDistribution.setOrganizationResourceLimitByType(resources, this.resourceLimits.RUNTIME, this.limits.runtimeCap);
+ } else {
+ resourcesToRemove.push(this.resourceLimits.RUNTIME);
+ }
+
+ // if the timeout resource will be send in this case - it will set the timeout for the current team, and the updating timeout of
+ // parent team will not affect the current team, so to avoid this - remove timeout resource if present:
+ this.lodash.remove(resources, (resource: any) => {
+ return resourcesToRemove.indexOf(resource.type) >= 0;
+ });
+
+
+ this.isLoading = true;
+ this.cheResourcesDistribution.distributeResources(this.team.id, resources).then(() => {
+ this.fetchLimits();
+ }, (error: any) => {
+ let errorMessage = 'Failed to set update team CAPs.';
+ this.cheNotification.showError((error.data && error.data.message !== null) ? errorMessage + 'Reason: ' + error.data.message : errorMessage);
+ this.fetchLimits();
+ });
+ }
+
+ /**
+ * Returns whether save button is disabled.
+ *
+ * @return {boolean}
+ */
+ isSaveButtonDisabled(): boolean {
+ return this.teamForm && this.teamForm.$invalid;
+ }
+
+ /**
+ * Returns true if "Save" button should be visible
+ *
+ * @return {boolean}
+ */
+ isSaveButtonVisible(): boolean {
+ return (this.selectedTabIndex === Tab.Settings && !this.isLoading) && (!angular.equals(this.team.name, this.newName) ||
+ !angular.equals(this.limits, this.limitsCopy));
+ }
+
+ updateTeam(): void {
+ this.updateTeamName();
+ this.updateLimits();
+ }
+
+ cancelChanges(): void {
+ this.newName = angular.copy(this.team.name);
+ this.limits = angular.copy(this.limitsCopy);
+ }
+}
diff --git a/dashboard/src/app/teams/team-details/team-details.html b/dashboard/src/app/teams/team-details/team-details.html
new file mode 100644
index 0000000000..cb12dd07a3
--- /dev/null
+++ b/dashboard/src/app/teams/team-details/team-details.html
@@ -0,0 +1,179 @@
+true need to refresh members
+ * @param fetchInvitations if true need to refresh invitations
+ * @return {IPromisetrue if specified member is team owner.
+ *
+ * @param {che.IMember> member a team member
+ * @return {boolean}
+ */
+ memberIsOwner(member: che.IMember): boolean {
+ return member.userId === this.owner.id;
+ }
+
+ /**
+ * Fetches the list of team members.
+ *
+ * @return {IPromisetrue if all members in list are checked.
+ * @returns {boolean}
+ */
+ isAllMembersSelected(): boolean {
+ return this.isAllSelected;
+ }
+
+ /**
+ * Returns true if all members in list are not checked.
+ * @returns {boolean}
+ */
+ isNoMemberSelected(): boolean {
+ return this.isNoSelected;
+ }
+
+ /**
+ * Make all members in list selected.
+ */
+ selectAllMembers(): void {
+ this.members.forEach((member: any) => {
+ this.membersSelectedStatus[member.userId] = true;
+ });
+ }
+
+ /**
+ * Make all members in list deselected.
+ */
+ deselectAllMembers(): void {
+ this.members.forEach((member: any) => {
+ this.membersSelectedStatus[member.userId] = false;
+ });
+ }
+
+ /**
+ * Change bulk selection value.
+ */
+ changeBulkSelection(): void {
+ if (this.isBulkChecked) {
+ this.deselectAllMembers();
+ this.isBulkChecked = false;
+ } else {
+ this.selectAllMembers();
+ this.isBulkChecked = true;
+ }
+ this.updateSelectedStatus();
+ }
+
+ /**
+ * Update members selected status.
+ */
+ updateSelectedStatus(): void {
+ this.isNoSelected = true;
+ this.isAllSelected = true;
+
+ Object.keys(this.membersSelectedStatus).forEach((key: string) => {
+ if (this.membersSelectedStatus[key]) {
+ this.isNoSelected = false;
+ } else {
+ this.isAllSelected = false;
+ }
+ });
+
+ if (this.isNoSelected) {
+ this.isBulkChecked = false;
+ return;
+ }
+
+ this.isBulkChecked = (this.isAllSelected && Object.keys(this.membersSelectedStatus).length === this.members.length);
+ }
+}
diff --git a/dashboard/src/app/workspaces/share-workspace/add-members/add-members.html b/dashboard/src/app/workspaces/share-workspace/add-members/add-members.html
new file mode 100644
index 0000000000..45a6b8a860
--- /dev/null
+++ b/dashboard/src/app/workspaces/share-workspace/add-members/add-members.html
@@ -0,0 +1,63 @@
+null if not found
+ */
+ getOrganizationById(id: string): che.IOrganization {
+ return this.organizationsByIdMap.get(id);
+ }
+
+ /**
+ * Returns organization by it's name.
+ *
+ * @param name {string} organization's name
+ * @returns {any} organization or null if not found
+ */
+ getOrganizationByName(name: string): che.IOrganization {
+ return this.organizationByNameMap.get(name);
+ }
+
+ /**
+ * Creates new organization with pointed name.
+ *
+ * @param name the name of the organization to be created
+ * @param parentId {string} the id of the parent organization
+ * @returns {ng.IPromisetrue if subset
+ * @private
+ */
+ _checkIsSubset(role: any, roles: Array