mirror of
https://github.com/bitwarden/browser
synced 2026-02-18 18:33:50 +00:00
* Add CSV export functionality to organization members page * Remove unnecessary async from getMemberExport method * Changed button position and style * fixed button alignment * updates based on feedback from product design * refactor, cleanup * fix DI * add default to user status pipe * add missing i18n key * copy update * remove redundant copy --------- Co-authored-by: Brandon <btreston@bitwarden.com>
496 lines
20 KiB
HTML
496 lines
20 KiB
HTML
@let organization = this.organization();
|
|
@if (organization) {
|
|
<app-organization-free-trial-warning
|
|
[organization]="organization"
|
|
(clicked)="billingConstraint.navigateToPaymentMethod(organization)"
|
|
>
|
|
</app-organization-free-trial-warning>
|
|
<app-header>
|
|
<bit-search
|
|
class="tw-grow"
|
|
[formControl]="searchControl"
|
|
[placeholder]="'searchMembers' | i18n"
|
|
></bit-search>
|
|
|
|
<button
|
|
type="button"
|
|
bitButton
|
|
buttonType="primary"
|
|
(click)="invite(organization)"
|
|
[disabled]="!firstLoaded"
|
|
*ngIf="showUserManagementControls()"
|
|
>
|
|
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
|
|
{{ "inviteMember" | i18n }}
|
|
</button>
|
|
</app-header>
|
|
|
|
<div class="tw-mb-4 tw-flex tw-flex-col tw-space-y-4">
|
|
<bit-toggle-group
|
|
[selected]="status"
|
|
(selectedChange)="statusToggle.next($event)"
|
|
[attr.aria-label]="'memberStatusFilter' | i18n"
|
|
*ngIf="showUserManagementControls()"
|
|
>
|
|
<bit-toggle [value]="null">
|
|
{{ "all" | i18n }}
|
|
<span bitBadge variant="info" *ngIf="dataSource.activeUserCount as allCount">{{
|
|
allCount
|
|
}}</span>
|
|
</bit-toggle>
|
|
|
|
<bit-toggle [value]="userStatusType.Invited">
|
|
{{ "invited" | i18n }}
|
|
<span bitBadge variant="info" *ngIf="dataSource.invitedUserCount as invitedCount">{{
|
|
invitedCount
|
|
}}</span>
|
|
</bit-toggle>
|
|
|
|
<bit-toggle [value]="userStatusType.Accepted">
|
|
{{ "needsConfirmation" | i18n }}
|
|
<span bitBadge variant="info" *ngIf="dataSource.acceptedUserCount as acceptedUserCount">{{
|
|
acceptedUserCount
|
|
}}</span>
|
|
</bit-toggle>
|
|
|
|
<bit-toggle [value]="userStatusType.Revoked">
|
|
{{ "revoked" | i18n }}
|
|
<span bitBadge variant="info" *ngIf="dataSource.revokedUserCount as revokedCount">{{
|
|
revokedCount
|
|
}}</span>
|
|
</bit-toggle>
|
|
</bit-toggle-group>
|
|
</div>
|
|
<ng-container *ngIf="!firstLoaded">
|
|
<i
|
|
class="bwi bwi-spinner bwi-spin tw-text-muted"
|
|
title="{{ 'loading' | i18n }}"
|
|
aria-hidden="true"
|
|
></i>
|
|
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
|
|
</ng-container>
|
|
<ng-container *ngIf="firstLoaded">
|
|
<p *ngIf="!dataSource.filteredData.length">{{ "noMembersInList" | i18n }}</p>
|
|
<ng-container *ngIf="dataSource.filteredData.length">
|
|
<bit-callout
|
|
type="info"
|
|
title="{{ 'confirmUsers' | i18n }}"
|
|
icon="bwi-check-circle"
|
|
*ngIf="showConfirmUsers"
|
|
>
|
|
{{ "usersNeedConfirmed" | i18n }}
|
|
</bit-callout>
|
|
<!-- The padding on the bottom of the cdk-virtual-scroll-viewport element is required to prevent table row content
|
|
from overflowing the <main> element. -->
|
|
<cdk-virtual-scroll-viewport bitScrollLayout [itemSize]="rowHeight" class="tw-pb-8">
|
|
<bit-table [dataSource]="dataSource">
|
|
<ng-container header>
|
|
<tr>
|
|
<th bitCell class="tw-w-20" *ngIf="showUserManagementControls()">
|
|
<input
|
|
type="checkbox"
|
|
bitCheckbox
|
|
class="tw-mr-1"
|
|
(change)="dataSource.checkAllFilteredUsers($any($event.target).checked)"
|
|
id="selectAll"
|
|
/>
|
|
<label class="tw-mb-0 !tw-font-medium !tw-text-muted" for="selectAll">{{
|
|
"all" | i18n
|
|
}}</label>
|
|
</th>
|
|
<th bitCell bitSortable="email" default>{{ "name" | i18n }}</th>
|
|
<th bitCell>{{ (organization.useGroups ? "groups" : "collections") | i18n }}</th>
|
|
<th bitCell bitSortable="type">{{ "role" | i18n }}</th>
|
|
<th bitCell>{{ "policies" | i18n }}</th>
|
|
<th bitCell>
|
|
<div class="tw-flex tw-flex-row tw-items-center tw-justify-end tw-gap-2">
|
|
<button
|
|
type="button"
|
|
bitIconButton="bwi-download"
|
|
size="small"
|
|
[bitAction]="exportMembers"
|
|
[disabled]="!firstLoaded"
|
|
label="{{ 'export' | i18n }}"
|
|
></button>
|
|
<button
|
|
[bitMenuTriggerFor]="headerMenu"
|
|
type="button"
|
|
bitIconButton="bwi-ellipsis-v"
|
|
size="small"
|
|
label="{{ 'options' | i18n }}"
|
|
*ngIf="showUserManagementControls()"
|
|
></button>
|
|
</div>
|
|
|
|
<bit-menu #headerMenu>
|
|
<ng-container *ngIf="canUseSecretsManager()">
|
|
<button type="button" bitMenuItem (click)="bulkEnableSM(organization)">
|
|
{{ "activateSecretsManager" | i18n }}
|
|
</button>
|
|
<bit-menu-divider></bit-menu-divider>
|
|
</ng-container>
|
|
<button
|
|
type="button"
|
|
bitMenuItem
|
|
(click)="bulkReinvite(organization)"
|
|
*ngIf="showBulkReinviteUsers"
|
|
>
|
|
<i class="bwi bwi-fw bwi-envelope" aria-hidden="true"></i>
|
|
{{ "reinviteSelected" | i18n }}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
bitMenuItem
|
|
(click)="bulkConfirm(organization)"
|
|
*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(organization)"
|
|
*ngIf="showBulkRestoreUsers"
|
|
>
|
|
<i class="bwi bwi-fw bwi-plus-circle" aria-hidden="true"></i>
|
|
{{ "restoreAccess" | i18n }}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
bitMenuItem
|
|
(click)="bulkRevoke(organization)"
|
|
*ngIf="showBulkRevokeUsers"
|
|
>
|
|
<i class="bwi bwi-fw bwi-minus-circle" aria-hidden="true"></i>
|
|
{{ "revokeAccess" | i18n }}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
bitMenuItem
|
|
(click)="bulkRemove(organization)"
|
|
*ngIf="showBulkRemoveUsers"
|
|
>
|
|
<span class="tw-text-danger">
|
|
<i aria-hidden="true" class="bwi bwi-fw bwi-close"></i>
|
|
{{ "remove" | i18n }}
|
|
</span>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
bitMenuItem
|
|
(click)="bulkDelete(organization)"
|
|
*ngIf="showBulkDeleteUsers"
|
|
>
|
|
<span class="tw-text-danger">
|
|
<i aria-hidden="true" class="bwi bwi-fw bwi-trash"></i>
|
|
{{ "delete" | i18n }}
|
|
</span>
|
|
</button>
|
|
</bit-menu>
|
|
</th>
|
|
</tr>
|
|
</ng-container>
|
|
<ng-template body let-rows$>
|
|
<tr
|
|
bitRow
|
|
*cdkVirtualFor="let u of rows$"
|
|
alignContent="middle"
|
|
[ngClass]="rowHeightClass"
|
|
>
|
|
<td bitCell (click)="dataSource.checkUser(u)" *ngIf="showUserManagementControls()">
|
|
<input type="checkbox" bitCheckbox [(ngModel)]="$any(u).checked" />
|
|
</td>
|
|
<ng-container *ngIf="showUserManagementControls(); else readOnlyUserInfo">
|
|
<td bitCell (click)="edit(u, organization)" class="tw-cursor-pointer">
|
|
<div class="tw-flex tw-items-center">
|
|
<bit-avatar
|
|
size="small"
|
|
[text]="u | userName"
|
|
[id]="u.userId"
|
|
[color]="u.avatarColor"
|
|
class="tw-mr-3"
|
|
></bit-avatar>
|
|
<div class="tw-flex tw-flex-col">
|
|
<div class="tw-flex tw-flex-row tw-gap-2">
|
|
<button type="button" bitLink>
|
|
{{ u.name ?? u.email }}
|
|
</button>
|
|
<span
|
|
bitBadge
|
|
class="tw-text-xs"
|
|
variant="secondary"
|
|
*ngIf="u.status === userStatusType.Invited"
|
|
>
|
|
{{ "invited" | i18n }}
|
|
</span>
|
|
<span
|
|
bitBadge
|
|
class="tw-text-xs"
|
|
variant="warning"
|
|
*ngIf="u.status === userStatusType.Accepted"
|
|
>
|
|
{{ "needsConfirmation" | i18n }}
|
|
</span>
|
|
<span
|
|
bitBadge
|
|
class="tw-text-xs"
|
|
variant="secondary"
|
|
*ngIf="u.status === userStatusType.Revoked"
|
|
>
|
|
{{ "revoked" | i18n }}
|
|
</span>
|
|
</div>
|
|
<div class="tw-text-sm tw-text-muted" *ngIf="u.name">
|
|
{{ u.email }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
</ng-container>
|
|
<ng-template #readOnlyUserInfo>
|
|
<td bitCell>
|
|
<div class="tw-flex tw-items-center">
|
|
<bit-avatar
|
|
size="small"
|
|
[text]="u | userName"
|
|
[id]="u.userId"
|
|
[color]="u.avatarColor"
|
|
class="tw-mr-3"
|
|
></bit-avatar>
|
|
<div class="tw-flex tw-flex-col">
|
|
<div class="tw-flex tw-flex-row tw-gap-2">
|
|
<span>{{ u.name ?? u.email }}</span>
|
|
<span
|
|
bitBadge
|
|
class="tw-text-xs"
|
|
variant="secondary"
|
|
*ngIf="u.status === userStatusType.Invited"
|
|
>
|
|
{{ "invited" | i18n }}
|
|
</span>
|
|
<span
|
|
bitBadge
|
|
class="tw-text-xs"
|
|
variant="warning"
|
|
*ngIf="u.status === userStatusType.Accepted"
|
|
>
|
|
{{ "needsConfirmation" | i18n }}
|
|
</span>
|
|
<span
|
|
bitBadge
|
|
class="tw-text-xs"
|
|
variant="secondary"
|
|
*ngIf="u.status === userStatusType.Revoked"
|
|
>
|
|
{{ "revoked" | i18n }}
|
|
</span>
|
|
</div>
|
|
<div class="tw-text-sm tw-text-muted" *ngIf="u.name">
|
|
{{ u.email }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
</ng-template>
|
|
|
|
<ng-container *ngIf="showUserManagementControls(); else readOnlyGroupsCell">
|
|
<td
|
|
bitCell
|
|
(click)="
|
|
edit(
|
|
u,
|
|
organization,
|
|
organization.useGroups ? memberTab.Groups : memberTab.Collections
|
|
)
|
|
"
|
|
class="tw-cursor-pointer"
|
|
>
|
|
<bit-badge-list
|
|
[items]="organization.useGroups ? u.groupNames : u.collectionNames"
|
|
[maxItems]="3"
|
|
variant="secondary"
|
|
></bit-badge-list>
|
|
</td>
|
|
</ng-container>
|
|
<ng-template #readOnlyGroupsCell>
|
|
<td bitCell>
|
|
<bit-badge-list
|
|
[items]="organization.useGroups ? u.groupNames : u.collectionNames"
|
|
[maxItems]="3"
|
|
variant="secondary"
|
|
></bit-badge-list>
|
|
</td>
|
|
</ng-template>
|
|
|
|
<ng-container *ngIf="showUserManagementControls(); else readOnlyRoleCell">
|
|
<td
|
|
bitCell
|
|
(click)="edit(u, organization, memberTab.Role)"
|
|
class="tw-cursor-pointer tw-text-sm tw-text-muted"
|
|
>
|
|
{{ u.type | userType }}
|
|
</td>
|
|
</ng-container>
|
|
<ng-template #readOnlyRoleCell>
|
|
<td bitCell class="tw-text-sm tw-text-muted">
|
|
{{ u.type | userType }}
|
|
</td>
|
|
</ng-template>
|
|
|
|
<td bitCell class="tw-text-muted">
|
|
<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>
|
|
@let resetPasswordPolicyEnabled = resetPasswordPolicyEnabled$ | async;
|
|
<ng-container
|
|
*ngIf="showEnrolledStatus($any(u), organization, resetPasswordPolicyEnabled)"
|
|
>
|
|
<i
|
|
class="bwi bwi-key"
|
|
title="{{ 'enrolledAccountRecovery' | i18n }}"
|
|
aria-hidden="true"
|
|
></i>
|
|
<span class="tw-sr-only">{{ "enrolledAccountRecovery" | i18n }}</span>
|
|
</ng-container>
|
|
</td>
|
|
<td bitCell>
|
|
<div class="tw-flex tw-flex-row tw-items-center tw-justify-end tw-gap-2">
|
|
<div class="tw-w-[32px]"></div>
|
|
<button
|
|
[bitMenuTriggerFor]="rowMenu"
|
|
type="button"
|
|
bitIconButton="bwi-ellipsis-v"
|
|
size="small"
|
|
label="{{ 'options' | i18n }}"
|
|
></button>
|
|
</div>
|
|
|
|
<bit-menu #rowMenu>
|
|
<ng-container *ngIf="showUserManagementControls()">
|
|
<button
|
|
type="button"
|
|
bitMenuItem
|
|
(click)="reinvite(u, organization)"
|
|
*ngIf="u.status === userStatusType.Invited"
|
|
>
|
|
<i aria-hidden="true" class="bwi bwi-envelope"></i>
|
|
{{ "resendInvitation" | i18n }}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
bitMenuItem
|
|
(click)="confirm(u, organization)"
|
|
*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, organization, memberTab.Role)"
|
|
>
|
|
<i aria-hidden="true" class="bwi bwi-user"></i> {{ "memberRole" | i18n }}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
bitMenuItem
|
|
(click)="edit(u, organization, memberTab.Groups)"
|
|
*ngIf="organization.useGroups"
|
|
>
|
|
<i aria-hidden="true" class="bwi bwi-users"></i> {{ "groups" | i18n }}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
bitMenuItem
|
|
(click)="edit(u, organization, memberTab.Collections)"
|
|
>
|
|
<i aria-hidden="true" class="bwi bwi-collection-shared"></i>
|
|
{{ "collections" | i18n }}
|
|
</button>
|
|
<bit-menu-divider></bit-menu-divider>
|
|
<button
|
|
type="button"
|
|
bitMenuItem
|
|
(click)="openEventsDialog(u, organization)"
|
|
*ngIf="organization.useEvents && u.status === userStatusType.Confirmed"
|
|
>
|
|
<i aria-hidden="true" class="bwi bwi-file-text"></i> {{ "eventLogs" | i18n }}
|
|
</button>
|
|
</ng-container>
|
|
|
|
<!-- Account recovery is available to all users with appropriate permissions -->
|
|
<button
|
|
type="button"
|
|
bitMenuItem
|
|
(click)="resetPassword(u, organization)"
|
|
*ngIf="allowResetPassword(u, organization, resetPasswordPolicyEnabled)"
|
|
>
|
|
<i aria-hidden="true" class="bwi bwi-key"></i> {{ "recoverAccount" | i18n }}
|
|
</button>
|
|
|
|
<ng-container *ngIf="showUserManagementControls()">
|
|
<button
|
|
type="button"
|
|
bitMenuItem
|
|
(click)="restore(u, organization)"
|
|
*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, organization)"
|
|
*ngIf="u.status !== userStatusType.Revoked"
|
|
>
|
|
<i aria-hidden="true" class="bwi bwi-minus-circle"></i>
|
|
{{ "revokeAccess" | i18n }}
|
|
</button>
|
|
<button
|
|
*ngIf="!u.managedByOrganization"
|
|
type="button"
|
|
bitMenuItem
|
|
(click)="remove(u, organization)"
|
|
>
|
|
<span class="tw-text-danger">
|
|
<i aria-hidden="true" class="bwi bwi-close"></i> {{ "remove" | i18n }}
|
|
</span>
|
|
</button>
|
|
<button
|
|
*ngIf="u.managedByOrganization"
|
|
type="button"
|
|
bitMenuItem
|
|
(click)="deleteUser(u, organization)"
|
|
>
|
|
<span class="tw-text-danger">
|
|
<i class="bwi bwi-trash" aria-hidden="true"></i>
|
|
{{ "delete" | i18n }}
|
|
</span>
|
|
</button>
|
|
</ng-container>
|
|
</bit-menu>
|
|
</td>
|
|
</tr>
|
|
</ng-template>
|
|
</bit-table>
|
|
</cdk-virtual-scroll-viewport>
|
|
</ng-container>
|
|
</ng-container>
|
|
}
|