Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Invite users to an org or space #3377

Merged
merged 58 commits into from
Feb 19, 2019
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
8c86ef7
Merge remote-tracking branch 'origin/v2-master' into invite-users
nwmac Jan 25, 2019
366869b
WIP
nwmac Jan 25, 2019
04ad893
WIP - Add stepper with new step, routing and list button (tbd)
richard-cox Jan 25, 2019
b59bb53
WIP - Added stacked input & mostly fleshed out
richard-cox Jan 29, 2019
46ef39a
WIP - Flesh out step 1 with instructions, special case of space level
richard-cox Jan 29, 2019
28ea51c
Add email library
nwmac Jan 30, 2019
93cd3ed
v2/v3 api docs updates
richard-cox Jan 25, 2019
9c2b5dc
wire in perms, cleared many todos
richard-cox Jan 30, 2019
0bbc74b
Merge remote-tracking branch 'origin/invite-users' into invite-users-2
richard-cox Jan 31, 2019
d79cb13
WIP backend
nwmac Jan 31, 2019
292a79d
Merge remote-tracking branch 'origin/invite-users' into invite-users-2
richard-cox Jan 31, 2019
30b7396
Wire in request
richard-cox Jan 31, 2019
70f002f
Remove other steps, fix & tidy up
richard-cox Feb 1, 2019
66546a8
Fix dupe entries
richard-cox Feb 1, 2019
3602acf
Return + focus
richard-cox Feb 1, 2019
8a3d171
Tweaks
richard-cox Feb 4, 2019
1da601c
Tweaks
richard-cox Feb 4, 2019
a95f61e
Fix tests
richard-cox Feb 4, 2019
b9c1454
Merge remote-tracking branch 'origin/v2-master' into invite-users
richard-cox Feb 4, 2019
8585502
CC and lint fixes
richard-cox Feb 4, 2019
e9d53d9
Basic docs, test fixes
richard-cox Feb 4, 2019
4c909f7
MD lint fixes
richard-cox Feb 5, 2019
aece246
Only show invite configure button if cf admin, tweak message for non-…
richard-cox Feb 5, 2019
bc27492
Fix `other` suite e2e tests
richard-cox Feb 5, 2019
887aab0
Improve wait for users table
richard-cox Feb 5, 2019
42599f6
Fix backend test
richard-cox Feb 5, 2019
2944bc6
Fix unit test, allow more time for e2e loading users
richard-cox Feb 5, 2019
ad8eb90
Unit test tweak
richard-cox Feb 6, 2019
fa1855c
Doc fix, tweak for unit tests
richard-cox Feb 6, 2019
66219a7
To Revert - e2e debug
richard-cox Feb 6, 2019
adcc443
Revert "To Revert - e2e debug"
richard-cox Feb 7, 2019
8ceae43
Fix forever loading users list on systems with no smpt config
richard-cox Feb 7, 2019
0afc77e
CC Fixes
richard-cox Feb 7, 2019
501b77c
Add close option to config invite client snack bar
richard-cox Feb 7, 2019
e3fab3b
Add confirmation and error snackbar to disable user invite
richard-cox Feb 7, 2019
8551357
Check scopes and use token for creating user in CF
nwmac Feb 8, 2019
6cd885b
Merge remote-tracking branch 'origin/v2-master' into invite-users
nwmac Feb 12, 2019
93eead1
Update user invite to Echo V3 API
nwmac Feb 12, 2019
ca17c21
Unit test fix
nwmac Feb 12, 2019
086dcfe
Fix front-end unit test
nwmac Feb 12, 2019
9f72548
Add permission check
nwmac Feb 13, 2019
0b2ba30
Three changes
richard-cox Feb 12, 2019
cbf358f
Move invite users button to above user tables
richard-cox Feb 13, 2019
69c06a7
Fix doc readme cc warnings
richard-cox Feb 13, 2019
d442ee3
Minor tidy-ups
nwmac Feb 14, 2019
3fef4f6
Merge branch 'v2-master' into invite-users
KlapTrap Feb 15, 2019
40058aa
Fix imports
KlapTrap Feb 15, 2019
283b5a0
Update docs
KlapTrap Feb 15, 2019
159a5ef
Update the docs guide
KlapTrap Feb 18, 2019
db04bb3
Update invite users text
KlapTrap Feb 18, 2019
ecea0ad
Minor change to the invite table header
KlapTrap Feb 18, 2019
af6e698
Small text change
KlapTrap Feb 18, 2019
a55a88a
Minor text change
KlapTrap Feb 18, 2019
b108d71
Merge remote-tracking branch 'origin/v2-master' into invite-users
richard-cox Feb 18, 2019
73793bc
Merge fixes
richard-cox Feb 18, 2019
93d8719
Update fetch all users max pagination
richard-cox Feb 18, 2019
2090f70
CC Fixed and copy change
KlapTrap Feb 18, 2019
656e3ff
CC Fix
KlapTrap Feb 18, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Wire in request
  • Loading branch information
richard-cox committed Jan 31, 2019
commit 30b7396e904511e0b28537aade29444186574012
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Injectable } from '@angular/core';
import { MatSnackBar } from '@angular/material';
import { Store } from '@ngrx/store';
import { combineLatest, Observable, of as observableOf } from 'rxjs';
import { catchError, map, switchMap } from 'rxjs/operators';
import { catchError, filter, map, switchMap } from 'rxjs/operators';

import { environment } from '../../../../environments/environment';
import { CurrentUserPermissions } from '../../../core/current-user-permissions.config';
Expand All @@ -20,6 +20,36 @@ export interface UserInviteResponse {
errorMessage?: string;
}

export interface UserInviteSendUaaResponse {
failed_invites: UserInviteSendUaaSectionResponse[];
new_invites: UserInviteSendUaaSectionResponse[];
}

export interface UserInviteSendUaaSectionResponse {
email: string;
errorCode: string;
errorMessage: string;
inviteLink: string;
success: boolean;
userid: string;
}

export interface UserInviteSendResponse extends UserInviteResponse, UserInviteSendUaaResponse {
}

export enum UserInviteSendSpaceRoles {
developer = 'developer',
auditor = 'auditor',
manager = 'manager'
}

interface UserInviteSendRequest {
org: string;
space: string;
spaceRoles: { [spaceRole: string]: boolean };
emails: string[];
}

@Injectable()
export class UserInviteService {

Expand All @@ -36,13 +66,10 @@ export class UserInviteService {
// TODO: RC Should all users be allowed to configure?
// waitForCFPermissions(this.store, this.activeRouteCfOrgSpace.cfGuid).pipe(
// map(cf => cf.global.isAdmin) <--- for admin
// this.configured$ = cfEndpointService.endpoint$.pipe(
// tap((c) => console.log(1, c)),
// filter(v => !!v.entity && !!v.entity.metadata),
// tap(() => console.log(2)),
// map(v => v.entity && v.entity.metadata['userInviteAllowed'] === 'true'), // TODO: add to typing
// );
this.configured$ = observableOf(true); // TODO: RC remove
this.configured$ = cfEndpointService.endpoint$.pipe(
filter(v => !!v.entity && !!v.entity.metadata),
map(v => v.entity && v.entity.metadata['userInviteAllowed'] === 'true'), // TODO: add to typing
);
}

configure(cfGUID: string, clientID: string, clientSecret: string): Observable<UserInviteResponse> {
Expand Down Expand Up @@ -121,4 +148,30 @@ export class UserInviteService {
map(([configured, canChangeRoles]) => !!configured && !!canChangeRoles)
);
}

invite(cfGuid: string, orgGuid: string, spaceGuid: string, spaceRole: UserInviteSendSpaceRoles, emails: string[]):
Observable<UserInviteSendResponse> {
const users: UserInviteSendRequest = {
org: orgGuid,
space: spaceGuid,
spaceRoles: {
[spaceRole]: true
},
emails
};
return this.http.post(`/pp/${proxyAPIVersion}/invite/send/${cfGuid}`, users).pipe(
map((response: UserInviteSendUaaResponse) => ({
error: false,
...response
})),
catchError(err => observableOf({
error: true,
errorMessage: err && err.error && err.error.error ?
err.error.error :
`Failed to either create ${emails.length === 1 ? 'user' : 'users'} or add basic roles`,
failed_invites: [],
new_invites: []
}))
);
}
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,28 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { BehaviorSubject, Observable, of as observableOf } from 'rxjs';
import { map } from 'rxjs/operators';
import { Store } from '@ngrx/store';
import { BehaviorSubject, combineLatest, Observable, of as observableOf } from 'rxjs';
import { map, tap } from 'rxjs/operators';

import { IOrganization, ISpace } from '../../../../../core/cf-api.types';
import { EntityServiceFactory } from '../../../../../core/entity-service-factory.service';
import {
StackedInputActionResult,
} from '../../../../../shared/components/stacked-input-actions/stacked-input-action/stacked-input-action.component';
import {
StackedInputActionsState,
StackedInputActionsUpdate,
} from '../../../../../shared/components/stacked-input-actions/stacked-input-actions.component';
import { StepOnNextFunction } from '../../../../../shared/components/stepper/step/step.component';
import { GetOrganization } from '../../../../../store/actions/organization.actions';
import { GetSpace } from '../../../../../store/actions/space.actions';
import { UsersRolesSetUsers } from '../../../../../store/actions/users-roles.actions';
import { AppState } from '../../../../../store/app-state';
import { entityFactory, organizationSchemaKey, spaceSchemaKey } from '../../../../../store/helpers/entity-factory';
import { APIResource } from '../../../../../store/types/api.types';
import { SpaceUserRoleNames } from '../../../../../store/types/user.types';
import { CfUser, SpaceUserRoleNames } from '../../../../../store/types/user.types';
import { UserRoleLabels } from '../../../../../store/types/users-roles.types';
import { ActiveRouteCfOrgSpace } from '../../../cf-page.types';
import { UserInviteSendSpaceRoles, UserInviteService } from '../../../user-invites/user-invite.service';

@Component({
selector: 'app-invite-users-create',
Expand All @@ -27,27 +35,31 @@ export class InviteUsersCreateComponent implements OnInit, OnDestroy {
public stepValid = new BehaviorSubject<boolean>(false);
public state = new BehaviorSubject<StackedInputActionsState[]>([]);
public orgName$: Observable<string>;
public org$: Observable<APIResource<IOrganization>>;
public spaceName$: Observable<string>;
public space$: Observable<APIResource<ISpace>>;
public isSpace = false;
public spaceRole: SpaceUserRoleNames = SpaceUserRoleNames.AUDITOR;
public spaceRoles: { label: string, value: SpaceUserRoleNames }[] = [];
public spaceRole: UserInviteSendSpaceRoles = UserInviteSendSpaceRoles.auditor;
public spaceRoles: { label: string, value: UserInviteSendSpaceRoles }[] = [];
private users: StackedInputActionsUpdate;

constructor(
private store: Store<AppState>,
private activeRouteCfOrgSpace: ActiveRouteCfOrgSpace,
private entityServiceFactory: EntityServiceFactory,
private userInviteService: UserInviteService
) {
this.valid$ = this.stepValid.asObservable();
this.spaceRoles.push(
{
label: UserRoleLabels.space.short[SpaceUserRoleNames.AUDITOR],
value: SpaceUserRoleNames.AUDITOR,
value: UserInviteSendSpaceRoles.auditor,
}, {
label: UserRoleLabels.space.short[SpaceUserRoleNames.DEVELOPER],
value: SpaceUserRoleNames.DEVELOPER,
value: UserInviteSendSpaceRoles.developer,
}, {
label: UserRoleLabels.space.short[SpaceUserRoleNames.MANAGER],
value: SpaceUserRoleNames.MANAGER,
value: UserInviteSendSpaceRoles.manager,
});
}

Expand All @@ -58,21 +70,27 @@ export class InviteUsersCreateComponent implements OnInit, OnDestroy {

ngOnInit() {
this.isSpace = !!this.activeRouteCfOrgSpace.spaceGuid;
this.orgName$ = this.entityServiceFactory.create<APIResource<IOrganization>>(
this.org$ = this.entityServiceFactory.create<APIResource<IOrganization>>(
organizationSchemaKey,
entityFactory(organizationSchemaKey),
this.activeRouteCfOrgSpace.orgGuid,
new GetOrganization(this.activeRouteCfOrgSpace.orgGuid, this.activeRouteCfOrgSpace.cfGuid, [], false)
).waitForEntity$.pipe(
map(entity => entity.entity.entity.name)
map(entity => entity.entity)
);
this.orgName$ = this.org$.pipe(
map(entity => entity.entity.name)
);
this.spaceName$ = this.isSpace ? this.entityServiceFactory.create<APIResource<ISpace>>(
this.space$ = this.isSpace ? this.entityServiceFactory.create<APIResource<ISpace>>(
spaceSchemaKey,
entityFactory(spaceSchemaKey),
this.activeRouteCfOrgSpace.spaceGuid,
new GetSpace(this.activeRouteCfOrgSpace.spaceGuid, this.activeRouteCfOrgSpace.cfGuid, [], false)
).waitForEntity$.pipe(
map(entity => entity.entity.entity.name)
map(entity => entity.entity)
) : observableOf(null);
this.spaceName$ = this.isSpace ? this.space$.pipe(
map(entity => entity.entity.name)
) : observableOf(null);
}

Expand Down Expand Up @@ -130,12 +148,98 @@ export class InviteUsersCreateComponent implements OnInit, OnDestroy {
// }
}

onNext = () => {
onNext: StepOnNextFunction = () => {
// this.store.dispatch(new UsersRolesSetUsers(this.cfUserService.activeRouteCfOrgSpace.cfGuid, [user.entity]));
// TODO: RC wire in invite service create user function to here. See `users` and `state` for data from and to components
// TODO: RC wire in response to use this.store.dispatch(new UsersRolesSetUsers(this.activeRouteCfOrgSpace.cfGuid, users));
// TODO: RC what happens if some pass and some fail? Do we re-attempt the passed/automatically remove inputs/ignore on retry?
return observableOf({ success: false });

// Mark all as processing
const processingState: StackedInputActionsState[] = [];
this.users.values.forEach(() => {
processingState.push({
key: 'I dont know',
result: StackedInputActionResult.PROCESSING,
});
});
this.state.next(processingState);
// this.users.values = ['sdfdsfdsfdsf', 'test@test.com'];

// Kick off the invites
return combineLatest(
this.userInviteService.invite(
this.activeRouteCfOrgSpace.cfGuid,
this.activeRouteCfOrgSpace.orgGuid,
this.activeRouteCfOrgSpace.spaceGuid,
this.spaceRole,
this.users.values),
this.org$,
this.space$
).pipe(
tap(([res, org, space]) => {
if (!res.error && res.failed_invites.length === 0) {
// TODO: RC check for existing user
const newUsers: CfUser[] = res.new_invites.map(invite => ({
organizations: [org],
managed_organizations: [],
billing_managed_organizations: [],
audited_organizations: [],
admin: false,
spaces: space && this.spaceRole === UserInviteSendSpaceRoles.developer ? [space] : [],
managed_spaces: space && this.spaceRole === UserInviteSendSpaceRoles.manager ? [space] : [],
audited_spaces: space && this.spaceRole === UserInviteSendSpaceRoles.auditor ? [space] : [],
cfGuid: this.activeRouteCfOrgSpace.cfGuid,
guid: invite.userid,
username: invite.email,
active: true, // TODO: RC CHECK
spaces_url: '',
organizations_url: '',
managed_organizations_url: '',
billing_managed_organizations_url: '',
audited_organizations_url: '',
managed_spaces_url: '',
audited_spaces_url: '',
default_space_guid: '',
}));

this.store.dispatch(new UsersRolesSetUsers(this.activeRouteCfOrgSpace.cfGuid, newUsers));

} else if (res.failed_invites.length > 0) {
// Push failures back into components
const newState: StackedInputActionsState[] = [];
this.users.values.forEach(email => {
// Update failed users
const failed = res.failed_invites.find(invite => invite.email === email);
if (failed) {
newState.push({
key: 'I dont know',
result: StackedInputActionResult.FAILED,
message: failed.errorMessage
});
return;
}
// Update succeeded users
const succeeded = res.new_invites.find(invite => invite.email === email);
if (succeeded) {
newState.push({
key: 'I dont know',
result: StackedInputActionResult.SUCCEEDED,
});
return;
}
// Can't find user for unknown reason, set to failed to can try again
newState.push({
key: 'I dont know',
result: StackedInputActionResult.FAILED,
message: 'No response for user found'
});
});
this.state.next(newState);
}
}),
map(([res, org, space]) => ({
success: !res.error,
message: res.errorMessage
})),
);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
<app-step title="Create Users" [onLeave]="create.onLeave" [onEnter]="create.onEnter" [onNext]="create.onNext" [valid]="create.valid$ | async" [nextButtonText]="'Create'">
<app-invite-users-create #create></app-invite-users-create>
</app-step>
<!-- //TODO: RC Only show next steps if we can get all user roles -->
<app-step title="Update Roles" [blocked]="modify.cfRolesService.loading$ | async" [onLeave]="modify.onLeave" [onEnter]="modify.onEnter" [onNext]="modify.onNext" [valid]="modify.valid$ | async">
<!-- // TODO: RC Only show next steps if we can get all user roles -->
<!-- // TODO: RC update t'other use of blocked -->
<app-step title="Update Roles" [blocked]="modify.blocked$ | async" [onLeave]="modify.onLeave" [onEnter]="modify.onEnter" [onNext]="modify.onNext" [valid]="modify.valid$ | async">
<app-manage-users-modify #modify></app-manage-users-modify>
</app-step>
<app-step title="Confirm Roles" [onEnter]="confirm.onEnter" [canClose]="!applyStarted" [disablePrevious]="applyStarted" [destructiveStep]="!applyStarted" [onNext]="startApply" [finishButtonText]="applyStarted ? 'Close' : 'Apply'">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,34 @@ import { UsersRolesExecuteChanges } from '../../../../store/actions/users-roles.
import { AppState } from '../../../../store/app-state';
import { ActiveRouteCfOrgSpace } from '../../cf-page.types';
import { getActiveRouteCfOrgSpaceProvider } from '../../cf.helpers';
import { CloudFoundryEndpointService } from '../../services/cloud-foundry-endpoint.service';
import { UserInviteService } from '../../user-invites/user-invite.service';
import { CfRolesService } from '../manage-users/cf-roles.service';

@Component({
selector: 'app-invite-users',
templateUrl: './invite-users.component.html',
styleUrls: ['./invite-users.component.scss'],
providers: [
getActiveRouteCfOrgSpaceProvider
getActiveRouteCfOrgSpaceProvider,
UserInviteService,
CfUserService,
CfRolesService,
CloudFoundryEndpointService
]
})
export class InviteUsersComponent implements OnInit {

defaultCancelUrl: string;
applyStarted = true;
applyStarted = false;

constructor(
private store: Store<AppState>,
private activeRouteCfOrgSpace: ActiveRouteCfOrgSpace,
private cfUserService: CfUserService,
private route: ActivatedRoute) {
private cfUserService: CfUserService, // TODO: RC remove?
private route: ActivatedRoute// TODO: RC remove?
) {
this.defaultCancelUrl = this.createReturnUrl(activeRouteCfOrgSpace);

}

ngOnInit() {
Expand Down
Loading