1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-17 08:43:33 +00:00

[EC-15] Members Grid (#4097)

* [EC-623] Introduce shared organization module and search input component

* [EC-623] Add search input story

* [EC-15] Introduce Members module

- Add members module and members routing module
- Move members only components into the members module and folder
- Remove members only components from LooseComponents module
- Update organization routing module to lazy load members module

* [EC-15] Enable ToggleGroup component to support generic values

Using a generic type for the ToggleGroup allows using both Strings and Enums as values without causing Typescript compiler warning/errors.

* [EC-15] Force no bottom margin for Toggle button label

* [EC-15] Update Members page header

- Use bit-toggle for member status filter
- Update bit-toggle Accepted button to say Needs Confirmation
- Use bit-search-input
- Update search placeholder text
- Update invite member button style and text
- Import ToggleGroupModule into ShareModule

* [EC-15] Update members table

- Use the CL bit-table component
- Add new table headings
- Replace cog options menu with bit-menu component
- Add placeholder for groups/collection badges

* [EC-15] Specify default generic type for ToggleGroup

* [EC-15] Modify getOrganizationUsers() in Api service

- Optionally allow the Api service to fetch org user groups and/or collections
- Will eventually be moved to an organization user service, but kept here for now

* [EC-15] Update member view to fetch groups/collections for users

- Use the new Api service functionality
- Fetch the organization's list of groups and decrypted collection for rendering their names in the table

* [EC-15] Refresh table after editing user groups

* [EC-15] Move new members dialog into members module

* [EC-15] Show "All" in collections column for users with AccessAll flag

* [EC-15] Update copy after talking with design/product
This commit is contained in:
Shane Melton
2022-12-14 13:23:33 -08:00
committed by GitHub
parent 443b3e0367
commit 913af652c5
41 changed files with 584 additions and 380 deletions

View File

@@ -20,6 +20,7 @@ import { ListResponse } from "@bitwarden/common/models/response/list.response";
import { OrganizationUserUserDetailsResponse } from "@bitwarden/common/models/response/organization-user.response"; import { OrganizationUserUserDetailsResponse } from "@bitwarden/common/models/response/organization-user.response";
import { ProviderUserUserDetailsResponse } from "@bitwarden/common/models/response/provider/provider-user.response"; import { ProviderUserUserDetailsResponse } from "@bitwarden/common/models/response/provider/provider-user.response";
import { OrganizationUserView } from "../organizations/core/views/organization-user.view";
import { UserConfirmComponent } from "../organizations/manage/user-confirm.component"; import { UserConfirmComponent } from "../organizations/manage/user-confirm.component";
type StatusType = OrganizationUserStatusType | ProviderUserStatusType; type StatusType = OrganizationUserStatusType | ProviderUserStatusType;
@@ -28,7 +29,7 @@ const MaxCheckedCount = 500;
@Directive() @Directive()
export abstract class BasePeopleComponent< export abstract class BasePeopleComponent<
UserType extends ProviderUserUserDetailsResponse | OrganizationUserUserDetailsResponse UserType extends ProviderUserUserDetailsResponse | OrganizationUserView
> { > {
@ViewChild("confirmTemplate", { read: ViewContainerRef, static: true }) @ViewChild("confirmTemplate", { read: ViewContainerRef, static: true })
confirmModalRef: ViewContainerRef; confirmModalRef: ViewContainerRef;
@@ -110,7 +111,7 @@ export abstract class BasePeopleComponent<
) {} ) {}
abstract edit(user: UserType): void; abstract edit(user: UserType): void;
abstract getUsers(): Promise<ListResponse<UserType>>; abstract getUsers(): Promise<ListResponse<UserType> | UserType[]>;
abstract deleteUser(id: string): Promise<void>; abstract deleteUser(id: string): Promise<void>;
abstract revokeUser(id: string): Promise<void>; abstract revokeUser(id: string): Promise<void>;
abstract restoreUser(id: string): Promise<void>; abstract restoreUser(id: string): Promise<void>;
@@ -125,9 +126,14 @@ export abstract class BasePeopleComponent<
this.statusMap.set(status, []); this.statusMap.set(status, []);
} }
if (response instanceof ListResponse) {
this.allUsers = response.data != null && response.data.length > 0 ? response.data : []; this.allUsers = response.data != null && response.data.length > 0 ? response.data : [];
} else if (Array.isArray(response)) {
this.allUsers = response;
}
this.allUsers.sort( this.allUsers.sort(
Utils.getSortFunction<ProviderUserUserDetailsResponse | OrganizationUserUserDetailsResponse>( Utils.getSortFunction<ProviderUserUserDetailsResponse | OrganizationUserView>(
this.i18nService, this.i18nService,
"email" "email"
) )

View File

@@ -0,0 +1,40 @@
import { OrganizationUserStatusType } from "@bitwarden/common/enums/organizationUserStatusType";
import { OrganizationUserType } from "@bitwarden/common/enums/organizationUserType";
import { PermissionsApi } from "@bitwarden/common/models/api/permissions.api";
import { OrganizationUserUserDetailsResponse } from "@bitwarden/common/models/response/organization-user.response";
import { CollectionAccessSelectionView } from "./collection-access-selection.view";
export class OrganizationUserView {
id: string;
userId: string;
type: OrganizationUserType;
status: OrganizationUserStatusType;
accessAll: boolean;
permissions: PermissionsApi;
resetPasswordEnrolled: boolean;
name: string;
email: string;
twoFactorEnabled: boolean;
usesKeyConnector: boolean;
collections: CollectionAccessSelectionView[] = [];
groups: string[] = [];
groupNames: string[] = [];
collectionNames: string[] = [];
static fromResponse(response: OrganizationUserUserDetailsResponse): OrganizationUserView {
const view = Object.assign(new OrganizationUserView(), response) as OrganizationUserView;
if (response.collections != undefined) {
view.collections = response.collections.map((c) => new CollectionAccessSelectionView(c));
}
if (response.groups != undefined) {
view.groups = response.groups;
}
return view;
}
}

View File

@@ -4,10 +4,9 @@ import { NgModule } from "@angular/core";
import { SharedModule } from "../../shared"; import { SharedModule } from "../../shared";
import { EntityUsersComponent } from "./entity-users.component"; import { EntityUsersComponent } from "./entity-users.component";
import { UserDialogModule } from "./member-dialog";
@NgModule({ @NgModule({
imports: [SharedModule, ScrollingModule, UserDialogModule], imports: [SharedModule, ScrollingModule],
declarations: [EntityUsersComponent], declarations: [EntityUsersComponent],
exports: [EntityUsersComponent], exports: [EntityUsersComponent],
}) })

View File

@@ -1,290 +0,0 @@
<div class="container page-content">
<div
class="-tw-mt-2 tw-mb-2 tw-flex tw-flex-wrap tw-border-0 tw-border-b tw-border-solid tw-border-secondary-300 tw-pb-2.5"
>
<h1 class="tw-mt-2 tw-mb-0 tw-grow tw-pr-3">{{ "members" | i18n }}</h1>
<div class="tw-mt-2 tw-flex tw-justify-start tw-space-x-3">
<div class="btn-group btn-group-sm" role="group">
<button
type="button"
class="btn btn-outline-secondary"
[ngClass]="{ active: status == null }"
(click)="filter(null)"
>
{{ "all" | i18n }}
<span bitBadge badgeType="info" *ngIf="allCount">{{ allCount }}</span>
</button>
<button
type="button"
class="btn btn-outline-secondary"
[ngClass]="{ active: status == userStatusType.Invited }"
(click)="filter(userStatusType.Invited)"
>
{{ "invited" | i18n }}
<span bitBadge badgeType="info" *ngIf="invitedCount">{{ invitedCount }}</span>
</button>
<button
type="button"
class="btn btn-outline-secondary"
[ngClass]="{ active: status == userStatusType.Accepted }"
(click)="filter(userStatusType.Accepted)"
>
{{ "accepted" | i18n }}
<span bitBadge badgeType="warning" *ngIf="acceptedCount">{{ acceptedCount }}</span>
</button>
<button
type="button"
class="btn btn-outline-secondary"
[ngClass]="{ active: status == userStatusType.Revoked }"
(click)="filter(userStatusType.Revoked)"
>
{{ "revoked" | i18n }}
<span bitBadge badgeType="info" *ngIf="revokedCount">{{ revokedCount }}</span>
</button>
</div>
<div class="tw-w-44">
<label class="sr-only" for="search">{{ "search" | i18n }}</label>
<input
type="search"
class="form-control form-control-sm"
id="search"
placeholder="{{ 'search' | i18n }}"
[(ngModel)]="searchText"
/>
</div>
<div class="dropdown" appListDropdown>
<button
class="btn btn-sm btn-outline-secondary dropdown-toggle"
type="button"
id="bulkActionsButton"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"
appA11yTitle="{{ 'options' | i18n }}"
>
<i class="bwi bwi-cog" aria-hidden="true"></i>
</button>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="bulkActionsButton">
<button class="dropdown-item" appStopClick (click)="bulkReinvite()">
<i class="bwi bwi-fw bwi-envelope" aria-hidden="true"></i>
{{ "reinviteSelected" | i18n }}
</button>
<button
class="dropdown-item text-success"
appStopClick
(click)="bulkConfirm()"
*ngIf="showBulkConfirmUsers"
>
<i class="bwi bwi-fw bwi-check" aria-hidden="true"></i>
{{ "confirmSelected" | i18n }}
</button>
<button class="dropdown-item" appStopClick (click)="bulkRestore()">
<i class="bwi bwi-fw bwi-plus-circle" aria-hidden="true"></i>
{{ "restoreAccess" | i18n }}
</button>
<button class="dropdown-item" appStopClick (click)="bulkRevoke()">
<i class="bwi bwi-fw bwi-minus-circle" aria-hidden="true"></i>
{{ "revokeAccess" | i18n }}
</button>
<button class="dropdown-item text-danger" appStopClick (click)="bulkRemove()">
<i class="bwi bwi-fw bwi-close" aria-hidden="true"></i>
{{ "remove" | i18n }}
</button>
<div class="dropdown-divider"></div>
<button class="dropdown-item" appStopClick (click)="selectAll(true)">
<i class="bwi bwi-fw bwi-check-square" aria-hidden="true"></i>
{{ "selectAll" | i18n }}
</button>
<button class="dropdown-item" appStopClick (click)="selectAll(false)">
<i class="bwi bwi-fw bwi-minus-square" aria-hidden="true"></i>
{{ "unselectAll" | i18n }}
</button>
</div>
</div>
<button type="button" class="btn btn-sm btn-outline-primary" (click)="invite()">
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
{{ "inviteUser" | i18n }}
</button>
</div>
</div>
<ng-container *ngIf="loading">
<i
class="bwi bwi-spinner bwi-spin text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</ng-container>
<ng-container
*ngIf="
!loading &&
(isPaging() ? pagedUsers : (users | search: searchText:'name':'email':'id')) as searchedUsers
"
>
<p *ngIf="!searchedUsers.length">{{ "noUsersInList" | i18n }}</p>
<ng-container *ngIf="searchedUsers.length">
<app-callout
type="info"
title="{{ 'confirmUsers' | i18n }}"
icon="bwi bwi-check-circle"
*ngIf="showConfirmUsers"
>
{{ "usersNeedConfirmed" | i18n }}
</app-callout>
<table
class="table table-hover table-list"
infiniteScroll
[infiniteScrollDistance]="1"
[infiniteScrollDisabled]="!isPaging()"
(scrolled)="loadMore()"
>
<tbody>
<tr *ngFor="let u of searchedUsers">
<td (click)="checkUser(u)" class="table-list-checkbox">
<input type="checkbox" [(ngModel)]="u.checked" appStopProp />
</td>
<td width="30">
<bit-avatar [text]="u | userName" [id]="u.userId" size="small"></bit-avatar>
</td>
<td>
<a href="#" appStopClick (click)="edit(u)">{{ u.email }}</a>
<span bitBadge badgeType="secondary" *ngIf="u.status === userStatusType.Invited">{{
"invited" | i18n
}}</span>
<span bitBadge badgeType="warning" *ngIf="u.status === userStatusType.Accepted">{{
"accepted" | i18n
}}</span>
<span bitBadge badgeType="secondary" *ngIf="u.status === userStatusType.Revoked">{{
"revoked" | i18n
}}</span>
<small class="text-muted d-block" *ngIf="u.name">{{ u.name }}</small>
</td>
<td>
<ng-container *ngIf="u.twoFactorEnabled">
<i
class="bwi bwi-lock"
title="{{ 'userUsingTwoStep' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "userUsingTwoStep" | i18n }}</span>
</ng-container>
<ng-container *ngIf="showEnrolledStatus(u)">
<i
class="bwi bwi-key"
title="{{ 'enrolledPasswordReset' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "enrolledPasswordReset" | i18n }}</span>
</ng-container>
</td>
<td>
<span *ngIf="u.type === userType.Owner">{{ "owner" | i18n }}</span>
<span *ngIf="u.type === userType.Admin">{{ "admin" | i18n }}</span>
<span *ngIf="u.type === userType.Manager">{{ "manager" | i18n }}</span>
<span *ngIf="u.type === userType.User">{{ "user" | i18n }}</span>
<span *ngIf="u.type === userType.Custom">{{ "custom" | i18n }}</span>
</td>
<td class="table-list-options">
<div class="dropdown" appListDropdown>
<button
class="btn btn-outline-secondary dropdown-toggle"
type="button"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"
appA11yTitle="{{ 'options' | i18n }}"
>
<i class="bwi bwi-cog bwi-lg" aria-hidden="true"></i>
</button>
<div class="dropdown-menu dropdown-menu-right">
<a
class="dropdown-item"
href="#"
appStopClick
(click)="reinvite(u)"
*ngIf="u.status === userStatusType.Invited"
>
<i class="bwi bwi-fw bwi-envelope" aria-hidden="true"></i>
{{ "resendInvitation" | i18n }}
</a>
<a
class="dropdown-item text-success"
href="#"
appStopClick
(click)="confirm(u)"
*ngIf="u.status === userStatusType.Accepted"
>
<i class="bwi bwi-fw bwi-check" aria-hidden="true"></i>
{{ "confirm" | i18n }}
</a>
<a
class="dropdown-item"
href="#"
appStopClick
(click)="groups(u)"
*ngIf="accessGroups"
>
<i class="bwi bwi-fw bwi-sitemap" aria-hidden="true"></i>
{{ "groups" | i18n }}
</a>
<a
class="dropdown-item"
href="#"
appStopClick
(click)="events(u)"
*ngIf="accessEvents && u.status === userStatusType.Confirmed"
>
<i class="bwi bwi-fw bwi-file-text" aria-hidden="true"></i>
{{ "eventLogs" | i18n }}
</a>
<a
class="dropdown-item"
href="#"
appStopClick
(click)="resetPassword(u)"
*ngIf="allowResetPassword(u)"
>
<i class="bwi bwi-fw bwi-key" aria-hidden="true"></i>
{{ "resetPassword" | i18n }}
</a>
<a
class="dropdown-item"
href="#"
appStopClick
(click)="restore(u)"
*ngIf="u.status === userStatusType.Revoked"
>
<i class="bwi bwi-fw bwi-plus-circle" aria-hidden="true"></i>
{{ "restoreAccess" | i18n }}
</a>
<a
class="dropdown-item"
href="#"
appStopClick
(click)="revoke(u)"
*ngIf="u.status !== userStatusType.Revoked"
>
<i class="bwi bwi-fw bwi-minus-circle" aria-hidden="true"></i>
{{ "revokeAccess" | i18n }}
</a>
<a class="dropdown-item text-danger" href="#" appStopClick (click)="remove(u)">
<i class="bwi bwi-fw bwi-close" aria-hidden="true"></i>
{{ "remove" | i18n }}
</a>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</ng-container>
</ng-container>
</div>
<ng-template #addEdit></ng-template>
<ng-template #groupsTemplate></ng-template>
<ng-template #eventsTemplate></ng-template>
<ng-template #confirmTemplate></ng-template>
<ng-template #resetPasswordTemplate></ng-template>
<ng-template #bulkStatusTemplate></ng-template>
<ng-template #bulkConfirmTemplate></ng-template>
<ng-template #bulkRemoveTemplate></ng-template>

View File

@@ -1,6 +1,6 @@
import { NgModule } from "@angular/core"; import { NgModule } from "@angular/core";
import { SharedModule } from "../../../shared/shared.module"; import { SharedModule } from "../../../../shared/shared.module";
import { MemberDialogComponent } from "./member-dialog.component"; import { MemberDialogComponent } from "./member-dialog.component";
import { NestedCheckboxComponent } from "./nested-checkbox.component"; import { NestedCheckboxComponent } from "./nested-checkbox.component";

View File

@@ -7,7 +7,7 @@ import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUti
import { Utils } from "@bitwarden/common/misc/utils"; import { Utils } from "@bitwarden/common/misc/utils";
import { OrganizationUserUpdateGroupsRequest } from "@bitwarden/common/models/request/organization-user-update-groups.request"; import { OrganizationUserUpdateGroupsRequest } from "@bitwarden/common/models/request/organization-user-update-groups.request";
import { GroupService, GroupView } from "../core"; import { GroupService, GroupView } from "../../core";
@Component({ @Component({
selector: "app-user-groups", selector: "app-user-groups",

View File

@@ -0,0 +1 @@
export * from "./members.module";

View File

@@ -0,0 +1,26 @@
import { NgModule } from "@angular/core";
import { RouterModule, Routes } from "@angular/router";
import { canAccessMembersTab } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { OrganizationPermissionsGuard } from "../guards/org-permissions.guard";
import { PeopleComponent } from "./people.component";
const routes: Routes = [
{
path: "",
component: PeopleComponent,
canActivate: [OrganizationPermissionsGuard],
data: {
titleId: "members",
organizationPermissions: canAccessMembersTab,
},
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class MembersRoutingModule {}

View File

@@ -0,0 +1,39 @@
import { ComponentFactoryResolver, NgModule } from "@angular/core";
import { ModalService } from "@bitwarden/angular/services/modal.service";
import { LooseComponentsModule } from "../../shared";
import { SharedOrganizationModule } from "../shared";
import { BulkConfirmComponent } from "./components/bulk/bulk-confirm.component";
import { BulkRemoveComponent } from "./components/bulk/bulk-remove.component";
import { BulkRestoreRevokeComponent } from "./components/bulk/bulk-restore-revoke.component";
import { BulkStatusComponent } from "./components/bulk/bulk-status.component";
import { UserDialogModule } from "./components/member-dialog";
import { ResetPasswordComponent } from "./components/reset-password.component";
import { UserGroupsComponent } from "./components/user-groups.component";
import { MembersRoutingModule } from "./members-routing.module";
import { PeopleComponent } from "./people.component";
@NgModule({
imports: [
SharedOrganizationModule,
LooseComponentsModule,
MembersRoutingModule,
UserDialogModule,
],
declarations: [
BulkConfirmComponent,
BulkRemoveComponent,
BulkRestoreRevokeComponent,
BulkStatusComponent,
PeopleComponent,
ResetPasswordComponent,
UserGroupsComponent,
],
})
export class MembersModule {
constructor(modalService: ModalService, componentFactoryResolver: ComponentFactoryResolver) {
modalService.registerComponentFactoryResolver(UserGroupsComponent, componentFactoryResolver);
}
}

View File

@@ -0,0 +1,305 @@
<div class="container page-content">
<div class="tw-mb-4 tw-flex tw-flex-col tw-space-y-4">
<h1>{{ "members" | i18n }}</h1>
<div class="tw-flex tw-items-center tw-justify-end tw-space-x-3">
<bit-toggle-group
[selected]="status"
(selectedChange)="filter($event)"
[attr.aria-label]="'memberStatusFilter' | i18n"
>
<bit-toggle [value]="null">
{{ "all" | i18n }} <span bitBadge badgeType="info" *ngIf="allCount">{{ allCount }}</span>
</bit-toggle>
<bit-toggle [value]="userStatusType.Invited">
{{ "invited" | i18n }}
<span bitBadge badgeType="info" *ngIf="invitedCount">{{ invitedCount }}</span>
</bit-toggle>
<bit-toggle [value]="userStatusType.Accepted">
{{ "needsConfirmation" | i18n }}
<span bitBadge badgeType="info" *ngIf="acceptedCount">{{ acceptedCount }}</span>
</bit-toggle>
<bit-toggle [value]="userStatusType.Revoked">
{{ "revoked" | i18n }}
<span bitBadge badgeType="info" *ngIf="revokedCount">{{ revokedCount }}</span>
</bit-toggle>
</bit-toggle-group>
<app-search-input
class="tw-grow"
[(ngModel)]="searchText"
[placeholder]="'searchMembers' | i18n"
>
</app-search-input>
<button type="button" bitButton buttonType="primary" (click)="invite()">
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
{{ "inviteMember" | i18n }}
</button>
</div>
</div>
<ng-container *ngIf="loading">
<i
class="bwi bwi-spinner bwi-spin text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</ng-container>
<ng-container
*ngIf="
!loading &&
(isPaging() ? pagedUsers : (users | search: searchText:'name':'email':'id')) as searchedUsers
"
>
<p *ngIf="!searchedUsers.length">{{ "noMembersInList" | i18n }}</p>
<ng-container *ngIf="searchedUsers.length">
<app-callout
type="info"
title="{{ 'confirmUsers' | i18n }}"
icon="bwi bwi-check-circle"
*ngIf="showConfirmUsers"
>
{{ "usersNeedConfirmed" | i18n }}
</app-callout>
<bit-table
infinite-scroll
[infiniteScrollDistance]="1"
[infiniteScrollDisabled]="!isPaging()"
(scrolled)="loadMore()"
>
<ng-container header>
<tr>
<th bitCell class="tw-w-20">
<input
type="checkbox"
class="tw-mr-1"
(change)="selectAll($any($event.target).checked)"
id="selectAll"
/>
<label class="tw-mb-0 !tw-font-bold !tw-text-muted" for="selectAll">{{
"all" | i18n
}}</label>
</th>
<th bitCell>{{ "name" | i18n }}</th>
<th bitCell>{{ (accessGroups ? "groups" : "collections") | i18n }}</th>
<th bitCell>{{ "role" | i18n }}</th>
<th bitCell>{{ "policies" | i18n }}</th>
<th bitCell class="tw-w-10">
<button
[bitMenuTriggerFor]="headerMenu"
type="button"
bitIconButton="bwi-ellipsis-v"
size="small"
appA11yTitle="{{ 'options' | i18n }}"
></button>
<bit-menu #headerMenu>
<button type="button" bitMenuItem (click)="bulkReinvite()">
<i class="bwi bwi-fw bwi-envelope" aria-hidden="true"></i>
{{ "reinviteSelected" | i18n }}
</button>
<button
type="button"
bitMenuItem
(click)="bulkConfirm()"
*ngIf="showBulkConfirmUsers"
>
<span class="tw-text-success">
<i class="bwi bwi-fw bwi-check" aria-hidden="true"></i>
{{ "confirmSelected" | i18n }}
</span>
</button>
<button type="button" bitMenuItem (click)="bulkRestore()">
<i class="bwi bwi-fw bwi-plus-circle" aria-hidden="true"></i>
{{ "restoreAccess" | i18n }}
</button>
<button type="button" bitMenuItem (click)="bulkRevoke()">
<i class="bwi bwi-fw bwi-minus-circle" aria-hidden="true"></i>
{{ "revokeAccess" | i18n }}
</button>
<button type="button" bitMenuItem (click)="bulkRemove()">
<span class="tw-text-danger">
<i aria-hidden="true" class="bwi bwi-close"></i>
{{ "remove" | i18n }}
</span>
</button>
</bit-menu>
</th>
</tr>
</ng-container>
<ng-container body>
<tr bitRow *ngFor="let u of searchedUsers" alignContent="middle">
<td bitCell (click)="checkUser(u)">
<input type="checkbox" [(ngModel)]="u.checked" />
</td>
<td bitCell (click)="edit(u)" class="tw-cursor-pointer">
<div class="tw-flex tw-items-center">
<bit-avatar
size="small"
[text]="u | userName"
[id]="u.userId"
class="tw-mr-3"
></bit-avatar>
<div class="tw-flex tw-flex-col">
<div class="tw-text-sm tw-font-bold tw-text-primary-500">
{{ u.name ?? u.email }}
<span
bitBadge
badgeType="secondary"
*ngIf="u.status === userStatusType.Invited"
>{{ "invited" | i18n }}</span
>
<span
bitBadge
badgeType="warning"
*ngIf="u.status === userStatusType.Accepted"
>{{ "needsConfirmation" | i18n }}</span
>
<span
bitBadge
badgeType="secondary"
*ngIf="u.status === userStatusType.Revoked"
>{{ "revoked" | i18n }}</span
>
</div>
<div class="tw-text-xs tw-text-muted" *ngIf="u.name">
{{ u.email }}
</div>
</div>
</div>
</td>
<td bitCell (click)="edit(u)" class="tw-cursor-pointer">
<bit-badge-list
*ngIf="accessGroups || !u.accessAll"
[items]="accessGroups ? u.groupNames : u.collectionNames"
[maxItems]="2"
badgeType="secondary"
></bit-badge-list>
<span *ngIf="!accessGroups && u.accessAll">{{ "all" | i18n }}</span>
</td>
<td bitCell (click)="edit(u)" class="tw-cursor-pointer">
{{ u.type | userType }}
</td>
<td bitCell>
<ng-container *ngIf="u.twoFactorEnabled">
<i
class="bwi bwi-lock"
title="{{ 'userUsingTwoStep' | i18n }}"
aria-hidden="true"
></i>
<span class="tw-sr-only">{{ "userUsingTwoStep" | i18n }}</span>
</ng-container>
<ng-container *ngIf="showEnrolledStatus(u)">
<i
class="bwi bwi-key"
title="{{ 'enrolledPasswordReset' | i18n }}"
aria-hidden="true"
></i>
<span class="tw-sr-only">{{ "enrolledPasswordReset" | i18n }}</span>
</ng-container>
</td>
<td bitCell>
<button
[bitMenuTriggerFor]="rowMenu"
type="button"
bitIconButton="bwi-ellipsis-v"
size="small"
appA11yTitle="{{ 'options' | i18n }}"
></button>
<bit-menu #rowMenu>
<button
type="button"
bitMenuItem
(click)="reinvite(u)"
*ngIf="u.status === userStatusType.Invited"
>
<i aria-hidden="true" class="bwi bwi-envelope"></i>
{{ "resendInvitation" | i18n }}
</button>
<button
type="button"
bitMenuItem
(click)="confirm(u)"
*ngIf="u.status === userStatusType.Accepted"
>
<span class="tw-text-success">
<i aria-hidden="true" class="bwi bwi-check"></i> {{ "confirm" | i18n }}
</span>
</button>
<bit-menu-divider
*ngIf="
u.status === userStatusType.Accepted || u.status === userStatusType.Invited
"
></bit-menu-divider>
<button type="button" bitMenuItem (click)="edit(u)">
<i aria-hidden="true" class="bwi bwi-user"></i> {{ "memberRole" | i18n }}
</button>
<button type="button" bitMenuItem (click)="groups(u)" *ngIf="accessGroups">
<i aria-hidden="true" class="bwi bwi-users"></i> {{ "groups" | i18n }}
</button>
<button type="button" bitMenuItem (click)="edit(u)">
<i aria-hidden="true" class="bwi bwi-collection"></i> {{ "collections" | i18n }}
</button>
<bit-menu-divider></bit-menu-divider>
<button
type="button"
bitMenuItem
(click)="events(u)"
*ngIf="accessEvents && u.status === userStatusType.Confirmed"
>
<i aria-hidden="true" class="bwi bwi-file-text"></i> {{ "eventLogs" | i18n }}
</button>
<button
type="button"
bitMenuItem
(click)="resetPassword(u)"
*ngIf="allowResetPassword(u)"
>
<i aria-hidden="true" class="bwi bwi-key"></i> {{ "resetPassword" | i18n }}
</button>
<button
type="button"
bitMenuItem
(click)="restore(u)"
*ngIf="u.status === userStatusType.Revoked"
>
<i aria-hidden="true" class="bwi bwi-plus-circle"></i>
{{ "restoreAccess" | i18n }}
</button>
<button
type="button"
bitMenuItem
(click)="revoke(u)"
*ngIf="u.status !== userStatusType.Revoked"
>
<i aria-hidden="true" class="bwi bwi-minus-circle"></i>
{{ "revokeAccess" | i18n }}
</button>
<button type="button" bitMenuItem (click)="remove(u)">
<span class="tw-text-danger">
<i aria-hidden="true" class="bwi bwi-close"></i> {{ "remove" | i18n }}
</span>
</button>
</bit-menu>
</td>
</tr>
</ng-container>
</bit-table>
</ng-container>
</ng-container>
<ng-template #addEdit></ng-template>
<ng-template #groupsTemplate></ng-template>
<ng-template #eventsTemplate></ng-template>
<ng-template #confirmTemplate></ng-template>
<ng-template #resetPasswordTemplate></ng-template>
<ng-template #bulkStatusTemplate></ng-template>
<ng-template #bulkConfirmTemplate></ng-template>
<ng-template #bulkRemoveTemplate></ng-template>
</div>

View File

@@ -6,6 +6,7 @@ import { SearchPipe } from "@bitwarden/angular/pipes/search.pipe";
import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe"; import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe";
import { ModalService } from "@bitwarden/angular/services/modal.service"; import { ModalService } from "@bitwarden/angular/services/modal.service";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { CollectionService } from "@bitwarden/common/abstractions/collection.service";
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service"; import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service"; import { LogService } from "@bitwarden/common/abstractions/log.service";
@@ -20,31 +21,36 @@ import { ValidationService } from "@bitwarden/common/abstractions/validation.ser
import { OrganizationUserStatusType } from "@bitwarden/common/enums/organizationUserStatusType"; import { OrganizationUserStatusType } from "@bitwarden/common/enums/organizationUserStatusType";
import { OrganizationUserType } from "@bitwarden/common/enums/organizationUserType"; import { OrganizationUserType } from "@bitwarden/common/enums/organizationUserType";
import { PolicyType } from "@bitwarden/common/enums/policyType"; import { PolicyType } from "@bitwarden/common/enums/policyType";
import { CollectionData } from "@bitwarden/common/models/data/collection.data";
import { Collection } from "@bitwarden/common/models/domain/collection";
import { OrganizationKeysRequest } from "@bitwarden/common/models/request/organization-keys.request"; import { OrganizationKeysRequest } from "@bitwarden/common/models/request/organization-keys.request";
import { OrganizationUserBulkRequest } from "@bitwarden/common/models/request/organization-user-bulk.request"; import { OrganizationUserBulkRequest } from "@bitwarden/common/models/request/organization-user-bulk.request";
import { OrganizationUserConfirmRequest } from "@bitwarden/common/models/request/organization-user-confirm.request"; import { OrganizationUserConfirmRequest } from "@bitwarden/common/models/request/organization-user-confirm.request";
import { CollectionDetailsResponse } from "@bitwarden/common/models/response/collection.response";
import { ListResponse } from "@bitwarden/common/models/response/list.response"; import { ListResponse } from "@bitwarden/common/models/response/list.response";
import { OrganizationUserBulkResponse } from "@bitwarden/common/models/response/organization-user-bulk.response"; import { OrganizationUserBulkResponse } from "@bitwarden/common/models/response/organization-user-bulk.response";
import { OrganizationUserUserDetailsResponse } from "@bitwarden/common/models/response/organization-user.response"; import { OrganizationUserUserDetailsResponse } from "@bitwarden/common/models/response/organization-user.response";
import { DialogService } from "@bitwarden/components"; import { DialogService } from "@bitwarden/components";
import { BasePeopleComponent } from "../../common/base.people.component"; import { BasePeopleComponent } from "../../common/base.people.component";
import { GroupService } from "../core";
import { OrganizationUserView } from "../core/views/organization-user.view";
import { EntityEventsComponent } from "../manage/entity-events.component";
import { BulkConfirmComponent } from "./bulk/bulk-confirm.component"; import { BulkConfirmComponent } from "./components/bulk/bulk-confirm.component";
import { BulkRemoveComponent } from "./bulk/bulk-remove.component"; import { BulkRemoveComponent } from "./components/bulk/bulk-remove.component";
import { BulkRestoreRevokeComponent } from "./bulk/bulk-restore-revoke.component"; import { BulkRestoreRevokeComponent } from "./components/bulk/bulk-restore-revoke.component";
import { BulkStatusComponent } from "./bulk/bulk-status.component"; import { BulkStatusComponent } from "./components/bulk/bulk-status.component";
import { EntityEventsComponent } from "./entity-events.component"; import { MemberDialogResult, openUserAddEditDialog } from "./components/member-dialog";
import { openUserAddEditDialog, MemberDialogResult } from "./member-dialog/member-dialog.component"; import { ResetPasswordComponent } from "./components/reset-password.component";
import { ResetPasswordComponent } from "./reset-password.component"; import { UserGroupsComponent } from "./components/user-groups.component";
import { UserGroupsComponent } from "./user-groups.component";
@Component({ @Component({
selector: "app-org-people", selector: "app-org-people",
templateUrl: "people.component.html", templateUrl: "people.component.html",
}) })
export class PeopleComponent export class PeopleComponent
extends BasePeopleComponent<OrganizationUserUserDetailsResponse> extends BasePeopleComponent<OrganizationUserView>
implements OnInit, OnDestroy implements OnInit, OnDestroy
{ {
@ViewChild("groupsTemplate", { read: ViewContainerRef, static: true }) @ViewChild("groupsTemplate", { read: ViewContainerRef, static: true })
@@ -94,7 +100,9 @@ export class PeopleComponent
stateService: StateService, stateService: StateService,
private organizationService: OrganizationService, private organizationService: OrganizationService,
private organizationApiService: OrganizationApiServiceAbstraction, private organizationApiService: OrganizationApiServiceAbstraction,
private dialogService: DialogService private dialogService: DialogService,
private groupService: GroupService,
private collectionService: CollectionService
) { ) {
super( super(
apiService, apiService,
@@ -167,12 +175,68 @@ export class PeopleComponent
} }
async load() { async load() {
super.load();
await super.load(); await super.load();
} }
getUsers(): Promise<ListResponse<OrganizationUserUserDetailsResponse>> { async getUsers(): Promise<OrganizationUserView[]> {
return this.apiService.getOrganizationUsers(this.organizationId); let groupsPromise: Promise<Map<string, string>>;
let collectionsPromise: Promise<Map<string, string>>;
// We don't need both groups and collections for the table, so only load one
const userPromise = this.apiService.getOrganizationUsers(this.organizationId, {
includeGroups: this.accessGroups,
includeCollections: !this.accessGroups,
});
// Depending on which column is displayed, we need to load the group/collection names
if (this.accessGroups) {
groupsPromise = this.getGroupNameMap();
} else {
collectionsPromise = this.getCollectionNameMap();
}
const [usersResponse, groupNamesMap, collectionNamesMap] = await Promise.all([
userPromise,
groupsPromise,
collectionsPromise,
]);
return usersResponse.data?.map<OrganizationUserView>((r) => {
const userView = OrganizationUserView.fromResponse(r);
userView.groupNames = userView.groups
.map((g) => groupNamesMap.get(g))
.sort(this.i18nService.collator?.compare);
userView.collectionNames = userView.collections
.map((c) => collectionNamesMap.get(c.id))
.sort(this.i18nService.collator?.compare);
return userView;
});
}
async getGroupNameMap(): Promise<Map<string, string>> {
const groups = await this.groupService.getAll(this.organizationId);
const groupNameMap = new Map<string, string>();
groups.forEach((g) => groupNameMap.set(g.id, g.name));
return groupNameMap;
}
/**
* Retrieve a map of all collection IDs <-> names for the organization.
*/
async getCollectionNameMap() {
const collectionMap = new Map<string, string>();
const response = await this.apiService.getCollections(this.organizationId);
const collections = response.data.map(
(r) => new Collection(new CollectionData(r as CollectionDetailsResponse))
);
const decryptedCollections = await this.collectionService.decryptMany(collections);
decryptedCollections.forEach((c) => collectionMap.set(c.id, c.name));
return collectionMap;
} }
deleteUser(id: string): Promise<void> { deleteUser(id: string): Promise<void> {
@@ -191,10 +255,7 @@ export class PeopleComponent
return this.apiService.postOrganizationUserReinvite(this.organizationId, id); return this.apiService.postOrganizationUserReinvite(this.organizationId, id);
} }
async confirmUser( async confirmUser(user: OrganizationUserView, publicKey: Uint8Array): Promise<void> {
user: OrganizationUserUserDetailsResponse,
publicKey: Uint8Array
): Promise<void> {
const orgKey = await this.cryptoService.getOrgKey(this.organizationId); const orgKey = await this.cryptoService.getOrgKey(this.organizationId);
const key = await this.cryptoService.rsaEncrypt(orgKey.key, publicKey.buffer); const key = await this.cryptoService.rsaEncrypt(orgKey.key, publicKey.buffer);
const request = new OrganizationUserConfirmRequest(); const request = new OrganizationUserConfirmRequest();
@@ -202,7 +263,7 @@ export class PeopleComponent
await this.apiService.postOrganizationUserConfirm(this.organizationId, user.id, request); await this.apiService.postOrganizationUserConfirm(this.organizationId, user.id, request);
} }
allowResetPassword(orgUser: OrganizationUserUserDetailsResponse): boolean { allowResetPassword(orgUser: OrganizationUserView): boolean {
// Hierarchy check // Hierarchy check
let callingUserHasPermission = false; let callingUserHasPermission = false;
@@ -240,7 +301,7 @@ export class PeopleComponent
); );
} }
async edit(user: OrganizationUserUserDetailsResponse) { async edit(user: OrganizationUserView) {
const dialog = openUserAddEditDialog(this.dialogService, { const dialog = openUserAddEditDialog(this.dialogService, {
data: { data: {
name: this.userNamePipe.transform(user), name: this.userNamePipe.transform(user),
@@ -274,6 +335,7 @@ export class PeopleComponent
// eslint-disable-next-line rxjs-angular/prefer-takeuntil // eslint-disable-next-line rxjs-angular/prefer-takeuntil
comp.onSavedUser.subscribe(() => { comp.onSavedUser.subscribe(() => {
modal.close(); modal.close();
this.load();
}); });
} }
); );
@@ -376,7 +438,7 @@ export class PeopleComponent
await this.load(); await this.load();
} }
async events(user: OrganizationUserUserDetailsResponse) { async events(user: OrganizationUserView) {
await this.modalService.openViewRef(EntityEventsComponent, this.eventsModalRef, (comp) => { await this.modalService.openViewRef(EntityEventsComponent, this.eventsModalRef, (comp) => {
comp.name = this.userNamePipe.transform(user); comp.name = this.userNamePipe.transform(user);
comp.organizationId = this.organizationId; comp.organizationId = this.organizationId;
@@ -386,7 +448,7 @@ export class PeopleComponent
}); });
} }
async resetPassword(user: OrganizationUserUserDetailsResponse) { async resetPassword(user: OrganizationUserView) {
const [modal] = await this.modalService.openViewRef( const [modal] = await this.modalService.openViewRef(
ResetPasswordComponent, ResetPasswordComponent,
this.resetPasswordModalRef, this.resetPasswordModalRef,
@@ -405,7 +467,7 @@ export class PeopleComponent
); );
} }
protected async removeUserConfirmationDialog(user: OrganizationUserUserDetailsResponse) { protected async removeUserConfirmationDialog(user: OrganizationUserView) {
const warningMessage = user.usesKeyConnector const warningMessage = user.usesKeyConnector
? this.i18nService.t("removeUserConfirmationKeyConnector") ? this.i18nService.t("removeUserConfirmationKeyConnector")
: this.i18nService.t("removeOrgUserConfirmation"); : this.i18nService.t("removeOrgUserConfirmation");
@@ -420,8 +482,8 @@ export class PeopleComponent
} }
private async showBulkStatus( private async showBulkStatus(
users: OrganizationUserUserDetailsResponse[], users: OrganizationUserView[],
filteredUsers: OrganizationUserUserDetailsResponse[], filteredUsers: OrganizationUserView[],
request: Promise<ListResponse<OrganizationUserBulkResponse>>, request: Promise<ListResponse<OrganizationUserBulkResponse>>,
successfullMessage: string successfullMessage: string
) { ) {

View File

@@ -3,9 +3,8 @@ import { RouterModule, Routes } from "@angular/router";
import { AuthGuard } from "@bitwarden/angular/guards/auth.guard"; import { AuthGuard } from "@bitwarden/angular/guards/auth.guard";
import { import {
canAccessOrgAdmin,
canAccessGroupsTab, canAccessGroupsTab,
canAccessMembersTab, canAccessOrgAdmin,
} from "@bitwarden/common/abstractions/organization/organization.service.abstraction"; } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { OrganizationPermissionsGuard } from "./guards/org-permissions.guard"; import { OrganizationPermissionsGuard } from "./guards/org-permissions.guard";
@@ -13,7 +12,6 @@ import { OrganizationLayoutComponent } from "./layouts/organization-layout.compo
import { CollectionsComponent } from "./manage/collections.component"; import { CollectionsComponent } from "./manage/collections.component";
import { GroupsComponent } from "./manage/groups.component"; import { GroupsComponent } from "./manage/groups.component";
import { ManageComponent } from "./manage/manage.component"; import { ManageComponent } from "./manage/manage.component";
import { PeopleComponent } from "./manage/people.component";
import { VaultModule } from "./vault/vault.module"; import { VaultModule } from "./vault/vault.module";
const routes: Routes = [ const routes: Routes = [
@@ -36,12 +34,7 @@ const routes: Routes = [
}, },
{ {
path: "members", path: "members",
component: PeopleComponent, loadChildren: () => import("./members").then((m) => m.MembersModule),
canActivate: [OrganizationPermissionsGuard],
data: {
titleId: "members",
organizationPermissions: canAccessMembersTab,
},
}, },
{ {
path: "groups", path: "groups",

View File

@@ -3,12 +3,11 @@ import { NgModule } from "@angular/core";
import { CoreOrganizationModule } from "./core"; import { CoreOrganizationModule } from "./core";
import { GroupAddEditComponent } from "./manage/group-add-edit.component"; import { GroupAddEditComponent } from "./manage/group-add-edit.component";
import { GroupsComponent } from "./manage/groups.component"; import { GroupsComponent } from "./manage/groups.component";
import { UserGroupsComponent } from "./manage/user-groups.component";
import { OrganizationsRoutingModule } from "./organization-routing.module"; import { OrganizationsRoutingModule } from "./organization-routing.module";
import { SharedOrganizationModule } from "./shared"; import { SharedOrganizationModule } from "./shared";
@NgModule({ @NgModule({
imports: [SharedOrganizationModule, CoreOrganizationModule, OrganizationsRoutingModule], imports: [SharedOrganizationModule, CoreOrganizationModule, OrganizationsRoutingModule],
declarations: [GroupsComponent, GroupAddEditComponent, UserGroupsComponent], declarations: [GroupsComponent, GroupAddEditComponent],
}) })
export class OrganizationModule {} export class OrganizationModule {}

View File

@@ -9,6 +9,6 @@ import { SearchInputComponent } from "./components/search-input/search-input.com
@NgModule({ @NgModule({
imports: [SharedModule, CollectionDialogModule, AccessSelectorModule], imports: [SharedModule, CollectionDialogModule, AccessSelectorModule],
declarations: [SearchInputComponent], declarations: [SearchInputComponent],
exports: [SharedModule, CollectionDialogModule, AccessSelectorModule], exports: [SharedModule, CollectionDialogModule, AccessSelectorModule, SearchInputComponent],
}) })
export class SharedOrganizationModule {} export class SharedOrganizationModule {}

View File

@@ -27,16 +27,10 @@ import { NavbarComponent } from "../layouts/navbar.component";
import { UserLayoutComponent } from "../layouts/user-layout.component"; import { UserLayoutComponent } from "../layouts/user-layout.component";
import { OrganizationCreateModule } from "../organizations/create/organization-create.module"; import { OrganizationCreateModule } from "../organizations/create/organization-create.module";
import { OrganizationLayoutComponent } from "../organizations/layouts/organization-layout.component"; import { OrganizationLayoutComponent } from "../organizations/layouts/organization-layout.component";
import { BulkConfirmComponent as OrgBulkConfirmComponent } from "../organizations/manage/bulk/bulk-confirm.component";
import { BulkRemoveComponent as OrgBulkRemoveComponent } from "../organizations/manage/bulk/bulk-remove.component";
import { BulkRestoreRevokeComponent as OrgBulkRestoreRevokeComponent } from "../organizations/manage/bulk/bulk-restore-revoke.component";
import { BulkStatusComponent as OrgBulkStatusComponent } from "../organizations/manage/bulk/bulk-status.component";
import { CollectionsComponent as OrgManageCollectionsComponent } from "../organizations/manage/collections.component"; import { CollectionsComponent as OrgManageCollectionsComponent } from "../organizations/manage/collections.component";
import { EntityEventsComponent as OrgEntityEventsComponent } from "../organizations/manage/entity-events.component"; import { EntityEventsComponent as OrgEntityEventsComponent } from "../organizations/manage/entity-events.component";
import { EventsComponent as OrgEventsComponent } from "../organizations/manage/events.component"; import { EventsComponent as OrgEventsComponent } from "../organizations/manage/events.component";
import { ManageComponent as OrgManageComponent } from "../organizations/manage/manage.component"; import { ManageComponent as OrgManageComponent } from "../organizations/manage/manage.component";
import { PeopleComponent as OrgPeopleComponent } from "../organizations/manage/people.component";
import { ResetPasswordComponent as OrgResetPasswordComponent } from "../organizations/manage/reset-password.component";
import { UserConfirmComponent as OrgUserConfirmComponent } from "../organizations/manage/user-confirm.component"; import { UserConfirmComponent as OrgUserConfirmComponent } from "../organizations/manage/user-confirm.component";
import { AcceptFamilySponsorshipComponent } from "../organizations/sponsorships/accept-family-sponsorship.component"; import { AcceptFamilySponsorshipComponent } from "../organizations/sponsorships/accept-family-sponsorship.component";
import { FamiliesForEnterpriseSetupComponent } from "../organizations/sponsorships/families-for-enterprise-setup.component"; import { FamiliesForEnterpriseSetupComponent } from "../organizations/sponsorships/families-for-enterprise-setup.component";
@@ -172,10 +166,6 @@ import { SharedModule } from "./shared.module";
OrganizationLayoutComponent, OrganizationLayoutComponent,
OrganizationPlansComponent, OrganizationPlansComponent,
OrgAttachmentsComponent, OrgAttachmentsComponent,
OrgBulkConfirmComponent,
OrgBulkRestoreRevokeComponent,
OrgBulkRemoveComponent,
OrgBulkStatusComponent,
OrgCollectionsComponent, OrgCollectionsComponent,
OrgEntityEventsComponent, OrgEntityEventsComponent,
OrgEventsComponent, OrgEventsComponent,
@@ -183,8 +173,6 @@ import { SharedModule } from "./shared.module";
OrgInactiveTwoFactorReportComponent, OrgInactiveTwoFactorReportComponent,
OrgManageCollectionsComponent, OrgManageCollectionsComponent,
OrgManageComponent, OrgManageComponent,
OrgPeopleComponent,
OrgResetPasswordComponent,
OrgReusedPasswordsReportComponent, OrgReusedPasswordsReportComponent,
OrgToolsComponent, OrgToolsComponent,
OrgUnsecuredWebsitesReportComponent, OrgUnsecuredWebsitesReportComponent,
@@ -289,10 +277,6 @@ import { SharedModule } from "./shared.module";
OrganizationLayoutComponent, OrganizationLayoutComponent,
OrganizationPlansComponent, OrganizationPlansComponent,
OrgAttachmentsComponent, OrgAttachmentsComponent,
OrgBulkConfirmComponent,
OrgBulkRestoreRevokeComponent,
OrgBulkRemoveComponent,
OrgBulkStatusComponent,
OrgCollectionsComponent, OrgCollectionsComponent,
OrgEntityEventsComponent, OrgEntityEventsComponent,
OrgEventsComponent, OrgEventsComponent,
@@ -300,8 +284,6 @@ import { SharedModule } from "./shared.module";
OrgInactiveTwoFactorReportComponent, OrgInactiveTwoFactorReportComponent,
OrgManageCollectionsComponent, OrgManageCollectionsComponent,
OrgManageComponent, OrgManageComponent,
OrgPeopleComponent,
OrgResetPasswordComponent,
OrgReusedPasswordsReportComponent, OrgReusedPasswordsReportComponent,
OrgToolsComponent, OrgToolsComponent,
OrgUnsecuredWebsitesReportComponent, OrgUnsecuredWebsitesReportComponent,

View File

@@ -24,6 +24,7 @@ import {
NavigationModule, NavigationModule,
TableModule, TableModule,
TabsModule, TabsModule,
ToggleGroupModule,
} from "@bitwarden/components"; } from "@bitwarden/components";
// Register the locales for the application // Register the locales for the application
@@ -59,6 +60,7 @@ import "./locales";
FormFieldModule, FormFieldModule,
IconButtonModule, IconButtonModule,
IconModule, IconModule,
LinkModule,
MenuModule, MenuModule,
NavigationModule, NavigationModule,
TableModule, TableModule,
@@ -91,6 +93,7 @@ import "./locales";
MenuModule, MenuModule,
NavigationModule, NavigationModule,
TableModule, TableModule,
ToggleGroupModule,
LinkModule, LinkModule,
TabsModule, TabsModule,

View File

@@ -302,6 +302,9 @@
"searchOrganization": { "searchOrganization": {
"message": "Search organization" "message": "Search organization"
}, },
"searchMembers": {
"message": "Search members"
},
"allItems": { "allItems": {
"message": "All items" "message": "All items"
}, },
@@ -729,6 +732,9 @@
"noUsersInList": { "noUsersInList": {
"message": "There are no users to list." "message": "There are no users to list."
}, },
"noMembersInList": {
"message": "There are no members to list."
},
"noEventsInList": { "noEventsInList": {
"message": "There are no events to list." "message": "There are no events to list."
}, },
@@ -2437,9 +2443,6 @@
"editMember": { "editMember": {
"message": "Edit member" "message": "Edit member"
}, },
"inviteMember": {
"message": "Invite member"
},
"inviteUserDesc": { "inviteUserDesc": {
"message": "Invite a new user to your organization by entering their Bitwarden account email address below. If they do not have a Bitwarden account already, they will be prompted to create a new account." "message": "Invite a new user to your organization by entering their Bitwarden account email address below. If they do not have a Bitwarden account already, they will be prompted to create a new account."
}, },
@@ -2969,10 +2972,10 @@
} }
}, },
"confirmUsers": { "confirmUsers": {
"message": "Confirm users" "message": "Confirm members"
}, },
"usersNeedConfirmed": { "usersNeedConfirmed": {
"message": "You have users that have accepted their invitation, but still need to be confirmed. Users will not have access to the organization until they are confirmed." "message": "You have members that have accepted their invitation, but still need to be confirmed. Members will not have access to the organization until they are confirmed."
}, },
"startDate": { "startDate": {
"message": "Start date" "message": "Start date"
@@ -5883,19 +5886,25 @@
"selectGroupsAndMembers": { "selectGroupsAndMembers": {
"message": "Select groups and members" "message": "Select groups and members"
}, },
"selectMembers": {
"message": "Select members"
},
"userPermissionOverrideHelper": { "userPermissionOverrideHelper": {
"message": "Permissions set for a member will replace permissions set by that member's group" "message": "Permissions set for a member will replace permissions set by that member's group"
}, },
"noMembersOrGroupsAdded": { "noMembersOrGroupsAdded": {
"message": "No members or groups added" "message": "No members or groups added"
}, },
"noMembersAdded": {
"message": "No members added"
},
"deleted": { "deleted": {
"message": "Deleted" "message": "Deleted"
},
"memberStatusFilter": {
"message": "Member status filter"
},
"inviteMember": {
"message": "Invite member"
},
"needsConfirmation": {
"message": "Needs confirmation"
},
"memberRole": {
"message": "Member role"
} }
} }

View File

@@ -3,12 +3,12 @@ import { Component, Input } from "@angular/core";
import { ProviderUserStatusType } from "@bitwarden/common/enums/providerUserStatusType"; import { ProviderUserStatusType } from "@bitwarden/common/enums/providerUserStatusType";
import { ProviderUserBulkConfirmRequest } from "@bitwarden/common/models/request/provider/provider-user-bulk-confirm.request"; import { ProviderUserBulkConfirmRequest } from "@bitwarden/common/models/request/provider/provider-user-bulk-confirm.request";
import { ProviderUserBulkRequest } from "@bitwarden/common/models/request/provider/provider-user-bulk.request"; import { ProviderUserBulkRequest } from "@bitwarden/common/models/request/provider/provider-user-bulk.request";
import { BulkConfirmComponent as OrganizationBulkConfirmComponent } from "@bitwarden/web-vault/app/organizations/manage/bulk/bulk-confirm.component"; import { BulkConfirmComponent as OrganizationBulkConfirmComponent } from "@bitwarden/web-vault/app/organizations/members/components/bulk/bulk-confirm.component";
import { BulkUserDetails } from "@bitwarden/web-vault/app/organizations/manage/bulk/bulk-status.component"; import { BulkUserDetails } from "@bitwarden/web-vault/app/organizations/members/components/bulk/bulk-status.component";
@Component({ @Component({
templateUrl: templateUrl:
"../../../../../../../apps/web/src/app/organizations/manage/bulk/bulk-confirm.component.html", "../../../../../../../apps/web/src/app/organizations/members/components/bulk/bulk-confirm.component.html",
}) })
export class BulkConfirmComponent extends OrganizationBulkConfirmComponent { export class BulkConfirmComponent extends OrganizationBulkConfirmComponent {
@Input() providerId: string; @Input() providerId: string;

View File

@@ -1,11 +1,11 @@
import { Component, Input } from "@angular/core"; import { Component, Input } from "@angular/core";
import { ProviderUserBulkRequest } from "@bitwarden/common/models/request/provider/provider-user-bulk.request"; import { ProviderUserBulkRequest } from "@bitwarden/common/models/request/provider/provider-user-bulk.request";
import { BulkRemoveComponent as OrganizationBulkRemoveComponent } from "@bitwarden/web-vault/app/organizations/manage/bulk/bulk-remove.component"; import { BulkRemoveComponent as OrganizationBulkRemoveComponent } from "@bitwarden/web-vault/app/organizations/members/components/bulk/bulk-remove.component";
@Component({ @Component({
templateUrl: templateUrl:
"../../../../../../../apps/web/src/app/organizations/manage/bulk/bulk-remove.component.html", "../../../../../../../apps/web/src/app/organizations/members/components/bulk/bulk-remove.component.html",
}) })
export class BulkRemoveComponent extends OrganizationBulkRemoveComponent { export class BulkRemoveComponent extends OrganizationBulkRemoveComponent {
@Input() providerId: string; @Input() providerId: string;

View File

@@ -22,8 +22,8 @@ import { ListResponse } from "@bitwarden/common/models/response/list.response";
import { ProviderUserBulkResponse } from "@bitwarden/common/models/response/provider/provider-user-bulk.response"; import { ProviderUserBulkResponse } from "@bitwarden/common/models/response/provider/provider-user-bulk.response";
import { ProviderUserUserDetailsResponse } from "@bitwarden/common/models/response/provider/provider-user.response"; import { ProviderUserUserDetailsResponse } from "@bitwarden/common/models/response/provider/provider-user.response";
import { BasePeopleComponent } from "@bitwarden/web-vault/app/common/base.people.component"; import { BasePeopleComponent } from "@bitwarden/web-vault/app/common/base.people.component";
import { BulkStatusComponent } from "@bitwarden/web-vault/app/organizations/manage/bulk/bulk-status.component";
import { EntityEventsComponent } from "@bitwarden/web-vault/app/organizations/manage/entity-events.component"; import { EntityEventsComponent } from "@bitwarden/web-vault/app/organizations/manage/entity-events.component";
import { BulkStatusComponent } from "@bitwarden/web-vault/app/organizations/members/components/bulk/bulk-status.component";
import { BulkConfirmComponent } from "./bulk/bulk-confirm.component"; import { BulkConfirmComponent } from "./bulk/bulk-confirm.component";
import { BulkRemoveComponent } from "./bulk/bulk-remove.component"; import { BulkRemoveComponent } from "./bulk/bulk-remove.component";

View File

@@ -354,7 +354,11 @@ export abstract class ApiService {
) => Promise<OrganizationUserDetailsResponse>; ) => Promise<OrganizationUserDetailsResponse>;
getOrganizationUserGroups: (organizationId: string, id: string) => Promise<string[]>; getOrganizationUserGroups: (organizationId: string, id: string) => Promise<string[]>;
getOrganizationUsers: ( getOrganizationUsers: (
organizationId: string organizationId: string,
options?: {
includeCollections?: boolean;
includeGroups?: boolean;
}
) => Promise<ListResponse<OrganizationUserUserDetailsResponse>>; ) => Promise<ListResponse<OrganizationUserUserDetailsResponse>>;
getOrganizationUserResetPasswordDetails: ( getOrganizationUserResetPasswordDetails: (
organizationId: string, organizationId: string,

View File

@@ -32,6 +32,8 @@ export class OrganizationUserUserDetailsResponse extends OrganizationUserRespons
email: string; email: string;
twoFactorEnabled: boolean; twoFactorEnabled: boolean;
usesKeyConnector: boolean; usesKeyConnector: boolean;
collections: SelectionReadOnlyResponse[] = [];
groups: string[] = [];
constructor(response: any) { constructor(response: any) {
super(response); super(response);
@@ -39,6 +41,14 @@ export class OrganizationUserUserDetailsResponse extends OrganizationUserRespons
this.email = this.getResponseProperty("Email"); this.email = this.getResponseProperty("Email");
this.twoFactorEnabled = this.getResponseProperty("TwoFactorEnabled"); this.twoFactorEnabled = this.getResponseProperty("TwoFactorEnabled");
this.usesKeyConnector = this.getResponseProperty("UsesKeyConnector") ?? false; this.usesKeyConnector = this.getResponseProperty("UsesKeyConnector") ?? false;
const collections = this.getResponseProperty("Collections");
if (collections != null) {
this.collections = collections.map((c: any) => new SelectionReadOnlyResponse(c));
}
const groups = this.getResponseProperty("Groups");
if (groups != null) {
this.groups = groups;
}
} }
} }

View File

@@ -984,11 +984,24 @@ export class ApiService implements ApiServiceAbstraction {
} }
async getOrganizationUsers( async getOrganizationUsers(
organizationId: string organizationId: string,
options?: {
includeCollections?: boolean;
includeGroups?: boolean;
}
): Promise<ListResponse<OrganizationUserUserDetailsResponse>> { ): Promise<ListResponse<OrganizationUserUserDetailsResponse>> {
const params = new URLSearchParams();
if (options?.includeCollections) {
params.set("includeCollections", "true");
}
if (options?.includeGroups) {
params.set("includeGroups", "true");
}
const r = await this.send( const r = await this.send(
"GET", "GET",
"/organizations/" + organizationId + "/users", `/organizations/${organizationId}/users?${params.toString()}`,
null, null,
true, true,
true true

View File

@@ -7,17 +7,17 @@ let nextId = 0;
templateUrl: "./toggle-group.component.html", templateUrl: "./toggle-group.component.html",
preserveWhitespaces: false, preserveWhitespaces: false,
}) })
export class ToggleGroupComponent { export class ToggleGroupComponent<TValue = unknown> {
private id = nextId++; private id = nextId++;
name = `bit-toggle-group-${this.id}`; name = `bit-toggle-group-${this.id}`;
@Input() selected?: unknown; @Input() selected?: TValue;
@Output() selectedChange = new EventEmitter<unknown>(); @Output() selectedChange = new EventEmitter<TValue>();
@HostBinding("attr.role") role = "radiogroup"; @HostBinding("attr.role") role = "radiogroup";
@HostBinding("class") classList = ["tw-flex"]; @HostBinding("class") classList = ["tw-flex"];
onInputInteraction(value: unknown) { onInputInteraction(value: TValue) {
this.selected = value; this.selected = value;
this.selectedChange.emit(value); this.selectedChange.emit(value);
} }

View File

@@ -1,4 +1,4 @@
import { HostBinding, Component, Input } from "@angular/core"; import { Component, HostBinding, Input } from "@angular/core";
import { ToggleGroupComponent } from "./toggle-group.component"; import { ToggleGroupComponent } from "./toggle-group.component";
@@ -9,12 +9,12 @@ let nextId = 0;
templateUrl: "./toggle.component.html", templateUrl: "./toggle.component.html",
preserveWhitespaces: false, preserveWhitespaces: false,
}) })
export class ToggleComponent { export class ToggleComponent<TValue> {
id = nextId++; id = nextId++;
@Input() value?: string; @Input() value?: TValue;
constructor(private groupComponent: ToggleGroupComponent) {} constructor(private groupComponent: ToggleGroupComponent<TValue>) {}
@HostBinding("tabIndex") tabIndex = "-1"; @HostBinding("tabIndex") tabIndex = "-1";
@HostBinding("class") classList = ["tw-group"]; @HostBinding("class") classList = ["tw-group"];
@@ -67,6 +67,9 @@ export class ToggleComponent {
"tw-py-1.5", "tw-py-1.5",
"tw-px-3", "tw-px-3",
// Fix for bootstrap styles that add bottom margin
"!tw-mb-0",
// Fix for badge being pushed slightly lower when inside a button. // Fix for badge being pushed slightly lower when inside a button.
// Insipired by bootstrap, which does the same. // Insipired by bootstrap, which does the same.
"[&>[bitBadge]]:tw-relative", "[&>[bitBadge]]:tw-relative",