PS-515 Fix/org users list performance (#1673)
* [PS-515] Use virtual scroll to speed up long user lists WIP: this is currently showing a large blank area under the last user. Need to figure out why virtual-scroll-spacer is sized too large. * Fix cdk-virtual-scroll styling * Format csp for readability * Set Viewport height The viewport height was * Calculate viewport height from item size Virtual scroll viewports need set heights, we can emulate the old modal behavior by calculating an approximate height required by the viewport to display all items. It will not go beyond the window due to the `.modal-dialog-scrollable` class * Remove modal css changes * pr review
This commit is contained in:
parent
85aa4274f3
commit
ca35ccbd35
|
@ -35,7 +35,6 @@ import { BulkStatusComponent as OrgBulkStatusComponent } from "../organizations/
|
||||||
import { CollectionAddEditComponent as OrgCollectionAddEditComponent } from "../organizations/manage/collection-add-edit.component";
|
import { CollectionAddEditComponent as OrgCollectionAddEditComponent } from "../organizations/manage/collection-add-edit.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 { EntityUsersComponent as OrgEntityUsersComponent } from "../organizations/manage/entity-users.component";
|
|
||||||
import { EventsComponent as OrgEventsComponent } from "../organizations/manage/events.component";
|
import { EventsComponent as OrgEventsComponent } from "../organizations/manage/events.component";
|
||||||
import { GroupAddEditComponent as OrgGroupAddEditComponent } from "../organizations/manage/group-add-edit.component";
|
import { GroupAddEditComponent as OrgGroupAddEditComponent } from "../organizations/manage/group-add-edit.component";
|
||||||
import { GroupsComponent as OrgGroupsComponent } from "../organizations/manage/groups.component";
|
import { GroupsComponent as OrgGroupsComponent } from "../organizations/manage/groups.component";
|
||||||
|
@ -245,7 +244,6 @@ import { OrganizationBadgeModule } from "./vault/modules/organization-badge/orga
|
||||||
OrgCollectionAddEditComponent,
|
OrgCollectionAddEditComponent,
|
||||||
OrgCollectionsComponent,
|
OrgCollectionsComponent,
|
||||||
OrgEntityEventsComponent,
|
OrgEntityEventsComponent,
|
||||||
OrgEntityUsersComponent,
|
|
||||||
OrgEventsComponent,
|
OrgEventsComponent,
|
||||||
OrgExportComponent,
|
OrgExportComponent,
|
||||||
OrgExposedPasswordsReportComponent,
|
OrgExposedPasswordsReportComponent,
|
||||||
|
@ -406,7 +404,6 @@ import { OrganizationBadgeModule } from "./vault/modules/organization-badge/orga
|
||||||
OrgCollectionAddEditComponent,
|
OrgCollectionAddEditComponent,
|
||||||
OrgCollectionsComponent,
|
OrgCollectionsComponent,
|
||||||
OrgEntityEventsComponent,
|
OrgEntityEventsComponent,
|
||||||
OrgEntityUsersComponent,
|
|
||||||
OrgEventsComponent,
|
OrgEventsComponent,
|
||||||
OrgExportComponent,
|
OrgExportComponent,
|
||||||
OrgExposedPasswordsReportComponent,
|
OrgExposedPasswordsReportComponent,
|
||||||
|
|
|
@ -29,52 +29,52 @@
|
||||||
></i>
|
></i>
|
||||||
<span class="sr-only">{{ "loading" | i18n }}</span>
|
<span class="sr-only">{{ "loading" | i18n }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<cdk-virtual-scroll-viewport
|
||||||
class="modal-body"
|
itemSize="46"
|
||||||
*ngIf="
|
minBufferPx="600"
|
||||||
!loading && users && (users | search: searchText:'name':'email':'id') as searchedUsers
|
maxBufferPx="1200"
|
||||||
"
|
[style]="scrollViewportStyle"
|
||||||
>
|
>
|
||||||
<div class="d-flex">
|
<div class="modal-body" *ngIf="!loading && users && searchedUsers">
|
||||||
<div class="mr-3">
|
<div class="d-flex">
|
||||||
<label class="sr-only" for="search">{{ "search" | i18n }}</label>
|
<div class="mr-3">
|
||||||
<input
|
<label class="sr-only" for="search">{{ "search" | i18n }}</label>
|
||||||
type="search"
|
<input
|
||||||
class="form-control form-control-sm"
|
type="search"
|
||||||
id="search"
|
class="form-control form-control-sm"
|
||||||
placeholder="{{ 'search' | i18n }}"
|
id="search"
|
||||||
name="SearchText"
|
placeholder="{{ 'search' | i18n }}"
|
||||||
[(ngModel)]="searchText"
|
name="SearchText"
|
||||||
/>
|
[(ngModel)]="searchText"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="btn-group btn-group-sm" role="group">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline-secondary"
|
||||||
|
[ngClass]="{ active: !showSelected }"
|
||||||
|
(click)="filterSelected(false)"
|
||||||
|
>
|
||||||
|
{{ "all" | i18n }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline-secondary"
|
||||||
|
[ngClass]="{ active: showSelected }"
|
||||||
|
(click)="filterSelected(true)"
|
||||||
|
>
|
||||||
|
{{ "selected" | i18n }}
|
||||||
|
<span class="badge badge-pill badge-info" *ngIf="selectedCount">{{
|
||||||
|
selectedCount
|
||||||
|
}}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="btn-group btn-group-sm" role="group">
|
<ng-container *ngIf="!searchedUsers.length">
|
||||||
<button
|
<hr />
|
||||||
type="button"
|
{{ "noUsersInList" | i18n }}
|
||||||
class="btn btn-outline-secondary"
|
</ng-container>
|
||||||
[ngClass]="{ active: !showSelected }"
|
<table class="table table-hover table-list mb-0" [hidden]="!searchedUsers.length">
|
||||||
(click)="filterSelected(false)"
|
|
||||||
>
|
|
||||||
{{ "all" | i18n }}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-outline-secondary"
|
|
||||||
[ngClass]="{ active: showSelected }"
|
|
||||||
(click)="filterSelected(true)"
|
|
||||||
>
|
|
||||||
{{ "selected" | i18n }}
|
|
||||||
<span class="badge badge-pill badge-info" *ngIf="selectedCount">{{
|
|
||||||
selectedCount
|
|
||||||
}}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ng-container *ngIf="!searchedUsers.length">
|
|
||||||
<hr />
|
|
||||||
{{ "noUsersInList" | i18n }}
|
|
||||||
</ng-container>
|
|
||||||
<ng-container *ngIf="searchedUsers.length">
|
|
||||||
<table class="table table-hover table-list mb-0">
|
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th> </th>
|
<th> </th>
|
||||||
|
@ -91,7 +91,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let u of searchedUsers">
|
<tr *cdkVirtualFor="let u of searchedUsers" class="">
|
||||||
<td class="table-list-checkbox" (click)="check(u)">
|
<td class="table-list-checkbox" (click)="check(u)">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
|
@ -164,8 +164,8 @@
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</ng-container>
|
</div>
|
||||||
</div>
|
</cdk-virtual-scroll-viewport>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
||||||
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
|
@ -1,5 +1,6 @@
|
||||||
import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core";
|
import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core";
|
||||||
|
|
||||||
|
import { SearchPipe } from "jslib-angular/pipes/search.pipe";
|
||||||
import { ApiService } from "jslib-common/abstractions/api.service";
|
import { ApiService } from "jslib-common/abstractions/api.service";
|
||||||
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||||
import { LogService } from "jslib-common/abstractions/log.service";
|
import { LogService } from "jslib-common/abstractions/log.service";
|
||||||
|
@ -13,6 +14,7 @@ import { OrganizationUserUserDetailsResponse } from "jslib-common/models/respons
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-entity-users",
|
selector: "app-entity-users",
|
||||||
templateUrl: "entity-users.component.html",
|
templateUrl: "entity-users.component.html",
|
||||||
|
providers: [SearchPipe],
|
||||||
})
|
})
|
||||||
export class EntityUsersComponent implements OnInit {
|
export class EntityUsersComponent implements OnInit {
|
||||||
@Input() entity: "group" | "collection";
|
@Input() entity: "group" | "collection";
|
||||||
|
@ -33,6 +35,7 @@ export class EntityUsersComponent implements OnInit {
|
||||||
private allUsers: OrganizationUserUserDetailsResponse[] = [];
|
private allUsers: OrganizationUserUserDetailsResponse[] = [];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
private search: SearchPipe,
|
||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
private platformUtilsService: PlatformUtilsService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
|
@ -52,6 +55,14 @@ export class EntityUsersComponent implements OnInit {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get searchedUsers() {
|
||||||
|
return this.search.transform(this.users, this.searchText, "name", "email", "id");
|
||||||
|
}
|
||||||
|
|
||||||
|
get scrollViewportStyle() {
|
||||||
|
return `min-height: 120px; height: ${120 + this.searchedUsers.length * 46}px`;
|
||||||
|
}
|
||||||
|
|
||||||
async loadUsers() {
|
async loadUsers() {
|
||||||
const users = await this.apiService.getOrganizationUsers(this.organizationId);
|
const users = await this.apiService.getOrganizationUsers(this.organizationId);
|
||||||
this.allUsers = users.data.map((r) => r).sort(Utils.getSortFunction(this.i18nService, "email"));
|
this.allUsers = users.data.map((r) => r).sort(Utils.getSortFunction(this.i18nService, "email"));
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { ScrollingModule } from "@angular/cdk/scrolling";
|
||||||
|
import { NgModule } from "@angular/core";
|
||||||
|
|
||||||
|
import { SharedModule } from "../../shared.module";
|
||||||
|
|
||||||
|
import { EntityUsersComponent } from "./entity-users.component";
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [SharedModule, ScrollingModule],
|
||||||
|
declarations: [EntityUsersComponent],
|
||||||
|
exports: [EntityUsersComponent],
|
||||||
|
})
|
||||||
|
export class OrganizationManageModule {}
|
|
@ -20,8 +20,9 @@ import {
|
||||||
import { ListResponse } from "jslib-common/models/response/listResponse";
|
import { ListResponse } from "jslib-common/models/response/listResponse";
|
||||||
import { CollectionView } from "jslib-common/models/view/collectionView";
|
import { CollectionView } from "jslib-common/models/view/collectionView";
|
||||||
|
|
||||||
|
import { EntityUsersComponent } from "../../modules/organizations/manage/entity-users.component";
|
||||||
|
|
||||||
import { CollectionAddEditComponent } from "./collection-add-edit.component";
|
import { CollectionAddEditComponent } from "./collection-add-edit.component";
|
||||||
import { EntityUsersComponent } from "./entity-users.component";
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-org-manage-collections",
|
selector: "app-org-manage-collections",
|
||||||
|
|
|
@ -12,7 +12,8 @@ import { SearchService } from "jslib-common/abstractions/search.service";
|
||||||
import { Utils } from "jslib-common/misc/utils";
|
import { Utils } from "jslib-common/misc/utils";
|
||||||
import { GroupResponse } from "jslib-common/models/response/groupResponse";
|
import { GroupResponse } from "jslib-common/models/response/groupResponse";
|
||||||
|
|
||||||
import { EntityUsersComponent } from "./entity-users.component";
|
import { EntityUsersComponent } from "../../modules/organizations/manage/entity-users.component";
|
||||||
|
|
||||||
import { GroupAddEditComponent } from "./group-add-edit.component";
|
import { GroupAddEditComponent } from "./group-add-edit.component";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { NgModule } from "@angular/core";
|
import { NgModule } from "@angular/core";
|
||||||
|
|
||||||
import { LooseComponentsModule } from "./modules/loose-components.module";
|
import { LooseComponentsModule } from "./modules/loose-components.module";
|
||||||
|
import { OrganizationManageModule } from "./modules/organizations/manage/organization-manage.module";
|
||||||
import { PipesModule } from "./modules/pipes/pipes.module";
|
import { PipesModule } from "./modules/pipes/pipes.module";
|
||||||
import { SharedModule } from "./modules/shared.module";
|
import { SharedModule } from "./modules/shared.module";
|
||||||
import { VaultFilterModule } from "./modules/vault-filter/vault-filter.module";
|
import { VaultFilterModule } from "./modules/vault-filter/vault-filter.module";
|
||||||
|
@ -13,6 +14,7 @@ import { OrganizationBadgeModule } from "./modules/vault/modules/organization-ba
|
||||||
VaultFilterModule,
|
VaultFilterModule,
|
||||||
OrganizationBadgeModule,
|
OrganizationBadgeModule,
|
||||||
PipesModule,
|
PipesModule,
|
||||||
|
OrganizationManageModule,
|
||||||
],
|
],
|
||||||
exports: [LooseComponentsModule, VaultFilterModule, OrganizationBadgeModule, PipesModule],
|
exports: [LooseComponentsModule, VaultFilterModule, OrganizationBadgeModule, PipesModule],
|
||||||
bootstrap: [],
|
bootstrap: [],
|
||||||
|
|
|
@ -204,8 +204,60 @@ const devServer =
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
key: "Content-Security-Policy",
|
key: "Content-Security-Policy",
|
||||||
value:
|
value: `
|
||||||
"default-src 'self'; script-src 'self' 'sha256-ryoU+5+IUZTuUyTElqkrQGBJXr1brEv6r2CA62WUw8w=' https://js.stripe.com https://js.braintreegateway.com https://www.paypalobjects.com; style-src 'self' https://assets.braintreegateway.com https://*.paypal.com 'sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=' 'sha256-JVRXyYPueLWdwGwY9m/7u4QlZ1xeQdqUj2t8OVIzZE4='; img-src 'self' data: https://icons.bitwarden.net https://*.paypal.com https://www.paypalobjects.com https://q.stripe.com https://haveibeenpwned.com https://www.gravatar.com; child-src 'self' https://js.stripe.com https://assets.braintreegateway.com https://*.paypal.com https://*.duosecurity.com; frame-src 'self' https://js.stripe.com https://assets.braintreegateway.com https://*.paypal.com https://*.duosecurity.com; connect-src 'self' wss://notifications.bitwarden.com https://notifications.bitwarden.com https://cdn.bitwarden.net https://api.pwnedpasswords.com https://2fa.directory/api/v3/totp.json https://api.stripe.com https://www.paypal.com https://api.braintreegateway.com https://client-analytics.braintreegateway.com https://*.braintree-api.com https://*.blob.core.windows.net https://app.simplelogin.io/api/alias/random/new https://app.anonaddy.com/api/v1/aliases; object-src 'self' blob:;",
|
default-src 'self';
|
||||||
|
script-src
|
||||||
|
'self'
|
||||||
|
'sha256-ryoU+5+IUZTuUyTElqkrQGBJXr1brEv6r2CA62WUw8w='
|
||||||
|
https://js.stripe.com
|
||||||
|
https://js.braintreegateway.com
|
||||||
|
https://www.paypalobjects.com;
|
||||||
|
style-src
|
||||||
|
'self'
|
||||||
|
https://assets.braintreegateway.com
|
||||||
|
https://*.paypal.com
|
||||||
|
'sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU='
|
||||||
|
'sha256-JVRXyYPueLWdwGwY9m/7u4QlZ1xeQdqUj2t8OVIzZE4=';
|
||||||
|
'sha256-0xHKHIT3+e2Gknxsm/cpErSprhL+o254L/y5bljg74U='
|
||||||
|
img-src
|
||||||
|
'self'
|
||||||
|
data:
|
||||||
|
https://icons.bitwarden.net
|
||||||
|
https://*.paypal.com
|
||||||
|
https://www.paypalobjects.com
|
||||||
|
https://q.stripe.com
|
||||||
|
https://haveibeenpwned.com
|
||||||
|
https://www.gravatar.com;
|
||||||
|
child-src
|
||||||
|
'self'
|
||||||
|
https://js.stripe.com
|
||||||
|
https://assets.braintreegateway.com
|
||||||
|
https://*.paypal.com
|
||||||
|
https://*.duosecurity.com;
|
||||||
|
frame-src
|
||||||
|
'self'
|
||||||
|
https://js.stripe.com
|
||||||
|
https://assets.braintreegateway.com
|
||||||
|
https://*.paypal.com
|
||||||
|
https://*.duosecurity.com;
|
||||||
|
connect-src
|
||||||
|
'self'
|
||||||
|
wss://notifications.bitwarden.com
|
||||||
|
https://notifications.bitwarden.com
|
||||||
|
https://cdn.bitwarden.net
|
||||||
|
https://api.pwnedpasswords.com
|
||||||
|
https://2fa.directory/api/v3/totp.json
|
||||||
|
https://api.stripe.com
|
||||||
|
https://www.paypal.com
|
||||||
|
https://api.braintreegateway.com
|
||||||
|
https://client-analytics.braintreegateway.com
|
||||||
|
https://*.braintree-api.com
|
||||||
|
https://*.blob.core.windows.net
|
||||||
|
https://app.simplelogin.io/api/alias/random/new
|
||||||
|
https://app.anonaddy.com/api/v1/aliases;
|
||||||
|
object-src
|
||||||
|
'self'
|
||||||
|
blob:;`,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue