1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-11-22 11:45:59 +01:00

[PM-6194] Refactor injection of services in browser services module (#8380)

* refactored injector of services on the browser service module

* refactored the search and popup serach service to use state provider

* renamed back to default

* removed token service that was readded during merge conflict

* Updated search service construction on the cli

* updated to use user key definition

* Reafctored all components that refernce issearchable

* removed commented variable

* added uncommited code to remove dependencies not needed anymore

* added uncommited code to remove dependencies not needed anymore
This commit is contained in:
SmithThe4th 2024-04-10 14:02:46 +01:00 committed by GitHub
parent 560033cb88
commit 2bce6c538c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
30 changed files with 393 additions and 182 deletions

View File

@ -514,7 +514,7 @@ export default class MainBackground {
this.apiService, this.apiService,
this.fileUploadService, this.fileUploadService,
); );
this.searchService = new SearchService(this.logService, this.i18nService); this.searchService = new SearchService(this.logService, this.i18nService, this.stateProvider);
this.collectionService = new CollectionService( this.collectionService = new CollectionService(
this.cryptoService, this.cryptoService,
@ -1177,7 +1177,7 @@ export default class MainBackground {
const newActiveUser = await this.stateService.clean({ userId: userId }); const newActiveUser = await this.stateService.clean({ userId: userId });
if (userId == null || userId === currentUserId) { if (userId == null || userId === currentUserId) {
this.searchService.clearIndex(); await this.searchService.clearIndex();
} }
await this.stateEventRunnerService.handleEvent("logout", currentUserId as UserId); await this.stateEventRunnerService.handleEvent("logout", currentUserId as UserId);

View File

@ -14,12 +14,17 @@ import {
logServiceFactory, logServiceFactory,
LogServiceInitOptions, LogServiceInitOptions,
} from "../../platform/background/service-factories/log-service.factory"; } from "../../platform/background/service-factories/log-service.factory";
import {
stateProviderFactory,
StateProviderInitOptions,
} from "../../platform/background/service-factories/state-provider.factory";
type SearchServiceFactoryOptions = FactoryOptions; type SearchServiceFactoryOptions = FactoryOptions;
export type SearchServiceInitOptions = SearchServiceFactoryOptions & export type SearchServiceInitOptions = SearchServiceFactoryOptions &
LogServiceInitOptions & LogServiceInitOptions &
I18nServiceInitOptions; I18nServiceInitOptions &
StateProviderInitOptions;
export function searchServiceFactory( export function searchServiceFactory(
cache: { searchService?: AbstractSearchService } & CachedServices, cache: { searchService?: AbstractSearchService } & CachedServices,
@ -33,6 +38,7 @@ export function searchServiceFactory(
new SearchService( new SearchService(
await logServiceFactory(cache, opts), await logServiceFactory(cache, opts),
await i18nServiceFactory(cache, opts), await i18nServiceFactory(cache, opts),
await stateProviderFactory(cache, opts),
), ),
); );
} }

View File

@ -1,17 +1,14 @@
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { StateProvider } from "@bitwarden/common/platform/state";
import { SearchService } from "@bitwarden/common/services/search.service"; import { SearchService } from "@bitwarden/common/services/search.service";
export class PopupSearchService extends SearchService { export class PopupSearchService extends SearchService {
constructor( constructor(logService: LogService, i18nService: I18nService, stateProvider: StateProvider) {
private mainSearchService: SearchService, super(logService, i18nService, stateProvider);
logService: LogService,
i18nService: I18nService,
) {
super(logService, i18nService);
} }
clearIndex() { clearIndex(): Promise<void> {
throw new Error("Not available."); throw new Error("Not available.");
} }
@ -19,7 +16,7 @@ export class PopupSearchService extends SearchService {
throw new Error("Not available."); throw new Error("Not available.");
} }
getIndexForSearch() { async getIndexForSearch() {
return this.mainSearchService.getIndexForSearch(); return await super.getIndexForSearch();
} }
} }

View File

@ -74,17 +74,15 @@ import {
GlobalStateProvider, GlobalStateProvider,
StateProvider, StateProvider,
} from "@bitwarden/common/platform/state"; } from "@bitwarden/common/platform/state";
import { SearchService } from "@bitwarden/common/services/search.service";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
import { UsernameGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/username"; import { UsernameGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/username";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service"; import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service";
import { CipherFileUploadService } from "@bitwarden/common/vault/abstractions/file-upload/cipher-file-upload.service";
import { FolderService as FolderServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { FolderService as FolderServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service"; import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/vault/abstractions/totp.service";
import { TotpService } from "@bitwarden/common/vault/services/totp.service";
import { DialogService } from "@bitwarden/components"; import { DialogService } from "@bitwarden/components";
import { VaultExportServiceAbstraction } from "@bitwarden/vault-export-core";
import { UnauthGuardService } from "../../auth/popup/services"; import { UnauthGuardService } from "../../auth/popup/services";
import { AutofillService as AutofillServiceAbstraction } from "../../autofill/services/abstractions/autofill.service"; import { AutofillService as AutofillServiceAbstraction } from "../../autofill/services/abstractions/autofill.service";
@ -187,19 +185,8 @@ const safeProviders: SafeProvider[] = [
}), }),
safeProvider({ safeProvider({
provide: SearchServiceAbstraction, provide: SearchServiceAbstraction,
useFactory: (logService: LogService, i18nService: I18nServiceAbstraction) => { useClass: PopupSearchService,
return new PopupSearchService( deps: [LogService, I18nServiceAbstraction, StateProvider],
getBgService<SearchService>("searchService")(),
logService,
i18nService,
);
},
deps: [LogService, I18nServiceAbstraction],
}),
safeProvider({
provide: CipherFileUploadService,
useFactory: getBgService<CipherFileUploadService>("cipherFileUploadService"),
deps: [],
}), }),
safeProvider({ safeProvider({
provide: CipherService, provide: CipherService,
@ -231,11 +218,6 @@ const safeProviders: SafeProvider[] = [
useClass: BrowserEnvironmentService, useClass: BrowserEnvironmentService,
deps: [LogService, StateProvider, AccountServiceAbstraction], deps: [LogService, StateProvider, AccountServiceAbstraction],
}), }),
safeProvider({
provide: TotpService,
useFactory: getBgService<TotpService>("totpService"),
deps: [],
}),
safeProvider({ safeProvider({
provide: I18nServiceAbstraction, provide: I18nServiceAbstraction,
useFactory: (globalStateProvider: GlobalStateProvider) => { useFactory: (globalStateProvider: GlobalStateProvider) => {
@ -252,6 +234,11 @@ const safeProviders: SafeProvider[] = [
}, },
deps: [EncryptService], deps: [EncryptService],
}), }),
safeProvider({
provide: TotpServiceAbstraction,
useClass: TotpService,
deps: [CryptoFunctionService, LogService],
}),
safeProvider({ safeProvider({
provide: AuthRequestServiceAbstraction, provide: AuthRequestServiceAbstraction,
useFactory: getBgService<AuthRequestServiceAbstraction>("authRequestService"), useFactory: getBgService<AuthRequestServiceAbstraction>("authRequestService"),
@ -333,11 +320,6 @@ const safeProviders: SafeProvider[] = [
BillingAccountProfileStateService, BillingAccountProfileStateService,
], ],
}), }),
safeProvider({
provide: VaultExportServiceAbstraction,
useFactory: getBgService<VaultExportServiceAbstraction>("exportService"),
deps: [],
}),
safeProvider({ safeProvider({
provide: KeyConnectorService, provide: KeyConnectorService,
useFactory: getBgService<KeyConnectorService>("keyConnectorService"), useFactory: getBgService<KeyConnectorService>("keyConnectorService"),

View File

@ -171,9 +171,7 @@ export class SendGroupingsComponent extends BaseSendComponent {
} }
showSearching() { showSearching() {
return ( return this.hasSearched || (!this.searchPending && this.isSearchable);
this.hasSearched || (!this.searchPending && this.searchService.isSearchable(this.searchText))
);
} }
private calculateTypeCounts() { private calculateTypeCounts() {

View File

@ -6,7 +6,7 @@ import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abs
import { EventType } from "@bitwarden/common/enums"; import { EventType } from "@bitwarden/common/enums";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service"; import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/vault/abstractions/totp.service";
import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
@ -31,7 +31,7 @@ export class ActionButtonsComponent implements OnInit, OnDestroy {
private i18nService: I18nService, private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService, private platformUtilsService: PlatformUtilsService,
private eventCollectionService: EventCollectionService, private eventCollectionService: EventCollectionService,
private totpService: TotpService, private totpService: TotpServiceAbstraction,
private passwordRepromptService: PasswordRepromptService, private passwordRepromptService: PasswordRepromptService,
private billingAccountProfileStateService: BillingAccountProfileStateService, private billingAccountProfileStateService: BillingAccountProfileStateService,
) {} ) {}

View File

@ -311,7 +311,7 @@ export class Fido2Component implements OnInit, OnDestroy {
} }
protected async search() { protected async search() {
this.hasSearched = this.searchService.isSearchable(this.searchText); this.hasSearched = await this.searchService.isSearchable(this.searchText);
this.searchPending = true; this.searchPending = true;
if (this.hasSearched) { if (this.hasSearched) {
this.displayedCiphers = await this.searchService.searchCiphers( this.displayedCiphers = await this.searchService.searchCiphers(

View File

@ -1,7 +1,7 @@
import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core"; import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core";
import { Router } from "@angular/router"; import { Router } from "@angular/router";
import { Subject, firstValueFrom } from "rxjs"; import { Subject, firstValueFrom, from } from "rxjs";
import { debounceTime, takeUntil } from "rxjs/operators"; import { debounceTime, switchMap, takeUntil } from "rxjs/operators";
import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
@ -120,8 +120,14 @@ export class CurrentTabComponent implements OnInit, OnDestroy {
} }
this.search$ this.search$
.pipe(debounceTime(500), takeUntil(this.destroy$)) .pipe(
.subscribe(() => this.searchVault()); debounceTime(500),
switchMap(() => {
return from(this.searchVault());
}),
takeUntil(this.destroy$),
)
.subscribe();
const autofillOnPageLoadOrgPolicy = await firstValueFrom( const autofillOnPageLoadOrgPolicy = await firstValueFrom(
this.autofillSettingsService.activateAutofillOnPageLoadFromPolicy$, this.autofillSettingsService.activateAutofillOnPageLoadFromPolicy$,
@ -232,14 +238,12 @@ export class CurrentTabComponent implements OnInit, OnDestroy {
} }
} }
searchVault() { async searchVault() {
if (!this.searchService.isSearchable(this.searchText)) { if (!(await this.searchService.isSearchable(this.searchText))) {
return; return;
} }
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. await this.router.navigate(["/tabs/vault"], { queryParams: { searchText: this.searchText } });
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/tabs/vault"], { queryParams: { searchText: this.searchText } });
} }
closeOnEsc(e: KeyboardEvent) { closeOnEsc(e: KeyboardEvent) {

View File

@ -1,8 +1,8 @@
import { Location } from "@angular/common"; import { Location } from "@angular/common";
import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core"; import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router"; import { ActivatedRoute, Router } from "@angular/router";
import { firstValueFrom } from "rxjs"; import { BehaviorSubject, Subject, firstValueFrom, from } from "rxjs";
import { first } from "rxjs/operators"; import { first, switchMap, takeUntil } from "rxjs/operators";
import { VaultFilter } from "@bitwarden/angular/vault/vault-filter/models/vault-filter.model"; import { VaultFilter } from "@bitwarden/angular/vault/vault-filter/models/vault-filter.model";
import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { SearchService } from "@bitwarden/common/abstractions/search.service";
@ -53,7 +53,6 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
folderCounts = new Map<string, number>(); folderCounts = new Map<string, number>();
collectionCounts = new Map<string, number>(); collectionCounts = new Map<string, number>();
typeCounts = new Map<CipherType, number>(); typeCounts = new Map<CipherType, number>();
searchText: string;
state: BrowserGroupingsComponentState; state: BrowserGroupingsComponentState;
showLeftHeader = true; showLeftHeader = true;
searchPending = false; searchPending = false;
@ -71,6 +70,16 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
private hasSearched = false; private hasSearched = false;
private hasLoadedAllCiphers = false; private hasLoadedAllCiphers = false;
private allCiphers: CipherView[] = null; private allCiphers: CipherView[] = null;
private destroy$ = new Subject<void>();
private _searchText$ = new BehaviorSubject<string>("");
private isSearchable: boolean = false;
get searchText() {
return this._searchText$.value;
}
set searchText(value: string) {
this._searchText$.next(value);
}
constructor( constructor(
private i18nService: I18nService, private i18nService: I18nService,
@ -148,6 +157,15 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
BrowserPopupUtils.setContentScrollY(window, this.state?.scrollY); BrowserPopupUtils.setContentScrollY(window, this.state?.scrollY);
} }
}); });
this._searchText$
.pipe(
switchMap((searchText) => from(this.searchService.isSearchable(searchText))),
takeUntil(this.destroy$),
)
.subscribe((isSearchable) => {
this.isSearchable = isSearchable;
});
} }
ngOnDestroy() { ngOnDestroy() {
@ -161,6 +179,8 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
// eslint-disable-next-line @typescript-eslint/no-floating-promises // eslint-disable-next-line @typescript-eslint/no-floating-promises
this.saveState(); this.saveState();
this.broadcasterService.unsubscribe(ComponentId); this.broadcasterService.unsubscribe(ComponentId);
this.destroy$.next();
this.destroy$.complete();
} }
async load() { async load() {
@ -181,7 +201,7 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
async loadCiphers() { async loadCiphers() {
this.allCiphers = await this.cipherService.getAllDecrypted(); this.allCiphers = await this.cipherService.getAllDecrypted();
if (!this.hasLoadedAllCiphers) { if (!this.hasLoadedAllCiphers) {
this.hasLoadedAllCiphers = !this.searchService.isSearchable(this.searchText); this.hasLoadedAllCiphers = !(await this.searchService.isSearchable(this.searchText));
} }
await this.search(null); await this.search(null);
this.getCounts(); this.getCounts();
@ -210,7 +230,7 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
} }
const filterDeleted = (c: CipherView) => !c.isDeleted; const filterDeleted = (c: CipherView) => !c.isDeleted;
if (timeout == null) { if (timeout == null) {
this.hasSearched = this.searchService.isSearchable(this.searchText); this.hasSearched = this.isSearchable;
this.ciphers = await this.searchService.searchCiphers( this.ciphers = await this.searchService.searchCiphers(
this.searchText, this.searchText,
filterDeleted, filterDeleted,
@ -223,7 +243,7 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
} }
this.searchPending = true; this.searchPending = true;
this.searchTimeout = setTimeout(async () => { this.searchTimeout = setTimeout(async () => {
this.hasSearched = this.searchService.isSearchable(this.searchText); this.hasSearched = this.isSearchable;
if (!this.hasLoadedAllCiphers && !this.hasSearched) { if (!this.hasLoadedAllCiphers && !this.hasSearched) {
await this.loadCiphers(); await this.loadCiphers();
} else { } else {
@ -381,9 +401,7 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
} }
showSearching() { showSearching() {
return ( return this.hasSearched || (!this.searchPending && this.isSearchable);
this.hasSearched || (!this.searchPending && this.searchService.isSearchable(this.searchText))
);
} }
closeOnEsc(e: KeyboardEvent) { closeOnEsc(e: KeyboardEvent) {

View File

@ -20,7 +20,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service"; import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/vault/abstractions/totp.service";
import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherType } from "@bitwarden/common/vault/enums";
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view"; import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
@ -74,7 +74,7 @@ export class ViewComponent extends BaseViewComponent {
constructor( constructor(
cipherService: CipherService, cipherService: CipherService,
folderService: FolderService, folderService: FolderService,
totpService: TotpService, totpService: TotpServiceAbstraction,
tokenService: TokenService, tokenService: TokenService,
i18nService: I18nService, i18nService: I18nService,
cryptoService: CryptoService, cryptoService: CryptoService,

View File

@ -414,7 +414,7 @@ export class Main {
this.sendService, this.sendService,
); );
this.searchService = new SearchService(this.logService, this.i18nService); this.searchService = new SearchService(this.logService, this.i18nService, this.stateProvider);
this.broadcasterService = new BroadcasterService(); this.broadcasterService = new BroadcasterService();

View File

@ -609,7 +609,7 @@ export class AppComponent implements OnInit, OnDestroy {
// This must come last otherwise the logout will prematurely trigger // This must come last otherwise the logout will prematurely trigger
// a process reload before all the state service user data can be cleaned up // a process reload before all the state service user data can be cleaned up
if (userBeingLoggedOut === preLogoutActiveUserId) { if (userBeingLoggedOut === preLogoutActiveUserId) {
this.searchService.clearIndex(); await this.searchService.clearIndex();
this.authService.logOut(async () => { this.authService.logOut(async () => {
if (expired) { if (expired) {
this.platformUtilsService.showToast( this.platformUtilsService.showToast(

View File

@ -1,5 +1,5 @@
import { Directive, ViewChild, ViewContainerRef } from "@angular/core"; import { Directive, OnDestroy, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
import { firstValueFrom } from "rxjs"; import { BehaviorSubject, Subject, firstValueFrom, from, switchMap, takeUntil } from "rxjs";
import { SearchPipe } from "@bitwarden/angular/pipes/search.pipe"; 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";
@ -33,7 +33,9 @@ const MaxCheckedCount = 500;
@Directive() @Directive()
export abstract class BasePeopleComponent< export abstract class BasePeopleComponent<
UserType extends ProviderUserUserDetailsResponse | OrganizationUserView, UserType extends ProviderUserUserDetailsResponse | OrganizationUserView,
> { >
implements OnInit, OnDestroy
{
@ViewChild("confirmTemplate", { read: ViewContainerRef, static: true }) @ViewChild("confirmTemplate", { read: ViewContainerRef, static: true })
confirmModalRef: ViewContainerRef; confirmModalRef: ViewContainerRef;
@ -88,7 +90,6 @@ export abstract class BasePeopleComponent<
status: StatusType; status: StatusType;
users: UserType[] = []; users: UserType[] = [];
pagedUsers: UserType[] = []; pagedUsers: UserType[] = [];
searchText: string;
actionPromise: Promise<void>; actionPromise: Promise<void>;
protected allUsers: UserType[] = []; protected allUsers: UserType[] = [];
@ -97,7 +98,19 @@ export abstract class BasePeopleComponent<
protected didScroll = false; protected didScroll = false;
protected pageSize = 100; protected pageSize = 100;
protected destroy$ = new Subject<void>();
private pagedUsersCount = 0; private pagedUsersCount = 0;
private _searchText$ = new BehaviorSubject<string>("");
private isSearching: boolean = false;
get searchText() {
return this._searchText$.value;
}
set searchText(value: string) {
this._searchText$.next(value);
}
constructor( constructor(
protected apiService: ApiService, protected apiService: ApiService,
@ -122,6 +135,22 @@ export abstract class BasePeopleComponent<
abstract reinviteUser(id: string): Promise<void>; abstract reinviteUser(id: string): Promise<void>;
abstract confirmUser(user: UserType, publicKey: Uint8Array): Promise<void>; abstract confirmUser(user: UserType, publicKey: Uint8Array): Promise<void>;
ngOnInit(): void {
this._searchText$
.pipe(
switchMap((searchText) => from(this.searchService.isSearchable(searchText))),
takeUntil(this.destroy$),
)
.subscribe((isSearchable) => {
this.isSearching = isSearchable;
});
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
async load() { async load() {
const response = await this.getUsers(); const response = await this.getUsers();
this.statusMap.clear(); this.statusMap.clear();
@ -390,12 +419,8 @@ export abstract class BasePeopleComponent<
} }
} }
isSearching() {
return this.searchService.isSearchable(this.searchText);
}
isPaging() { isPaging() {
const searching = this.isSearching(); const searching = this.isSearching;
if (searching && this.didScroll) { if (searching && this.didScroll) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises // eslint-disable-next-line @typescript-eslint/no-floating-promises

View File

@ -91,15 +91,16 @@ export class GroupsComponent implements OnInit, OnDestroy {
private pagedGroupsCount = 0; private pagedGroupsCount = 0;
private pagedGroups: GroupDetailsRow[]; private pagedGroups: GroupDetailsRow[];
private searchedGroups: GroupDetailsRow[]; private searchedGroups: GroupDetailsRow[];
private _searchText: string; private _searchText$ = new BehaviorSubject<string>("");
private destroy$ = new Subject<void>(); private destroy$ = new Subject<void>();
private refreshGroups$ = new BehaviorSubject<void>(null); private refreshGroups$ = new BehaviorSubject<void>(null);
private isSearching: boolean = false;
get searchText() { get searchText() {
return this._searchText; return this._searchText$.value;
} }
set searchText(value: string) { set searchText(value: string) {
this._searchText = value; this._searchText$.next(value);
// Manually update as we are not using the search pipe in the template // Manually update as we are not using the search pipe in the template
this.updateSearchedGroups(); this.updateSearchedGroups();
} }
@ -114,7 +115,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
if (this.isPaging()) { if (this.isPaging()) {
return this.pagedGroups; return this.pagedGroups;
} }
if (this.isSearching()) { if (this.isSearching) {
return this.searchedGroups; return this.searchedGroups;
} }
return this.groups; return this.groups;
@ -180,6 +181,15 @@ export class GroupsComponent implements OnInit, OnDestroy {
takeUntil(this.destroy$), takeUntil(this.destroy$),
) )
.subscribe(); .subscribe();
this._searchText$
.pipe(
switchMap((searchText) => this.searchService.isSearchable(searchText)),
takeUntil(this.destroy$),
)
.subscribe((isSearchable) => {
this.isSearching = isSearchable;
});
} }
ngOnDestroy() { ngOnDestroy() {
@ -297,10 +307,6 @@ export class GroupsComponent implements OnInit, OnDestroy {
this.loadMore(); this.loadMore();
} }
isSearching() {
return this.searchService.isSearchable(this.searchText);
}
check(groupRow: GroupDetailsRow) { check(groupRow: GroupDetailsRow) {
groupRow.checked = !groupRow.checked; groupRow.checked = !groupRow.checked;
} }
@ -310,7 +316,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
} }
isPaging() { isPaging() {
const searching = this.isSearching(); const searching = this.isSearching;
if (searching && this.didScroll) { if (searching && this.didScroll) {
this.resetPaging(); this.resetPaging();
} }
@ -340,7 +346,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
} }
private updateSearchedGroups() { private updateSearchedGroups() {
if (this.searchService.isSearchable(this.searchText)) { if (this.isSearching) {
// Making use of the pipe in the component as we need know which groups where filtered // Making use of the pipe in the component as we need know which groups where filtered
this.searchedGroups = this.searchPipe.transform( this.searchedGroups = this.searchPipe.transform(
this.groups, this.groups,

View File

@ -1,4 +1,4 @@
import { Component, OnDestroy, OnInit, ViewChild, ViewContainerRef } from "@angular/core"; import { Component, ViewChild, ViewContainerRef } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router"; import { ActivatedRoute, Router } from "@angular/router";
import { import {
combineLatest, combineLatest,
@ -9,7 +9,6 @@ import {
map, map,
Observable, Observable,
shareReplay, shareReplay,
Subject,
switchMap, switchMap,
takeUntil, takeUntil,
} from "rxjs"; } from "rxjs";
@ -73,10 +72,7 @@ import { ResetPasswordComponent } from "./components/reset-password.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<OrganizationUserView> {
extends BasePeopleComponent<OrganizationUserView>
implements OnInit, OnDestroy
{
@ViewChild("groupsTemplate", { read: ViewContainerRef, static: true }) @ViewChild("groupsTemplate", { read: ViewContainerRef, static: true })
groupsModalRef: ViewContainerRef; groupsModalRef: ViewContainerRef;
@ViewChild("confirmTemplate", { read: ViewContainerRef, static: true }) @ViewChild("confirmTemplate", { read: ViewContainerRef, static: true })
@ -99,7 +95,6 @@ export class PeopleComponent
orgResetPasswordPolicyEnabled = false; orgResetPasswordPolicyEnabled = false;
protected canUseSecretsManager$: Observable<boolean>; protected canUseSecretsManager$: Observable<boolean>;
private destroy$ = new Subject<void>();
constructor( constructor(
apiService: ApiService, apiService: ApiService,
@ -210,8 +205,7 @@ export class PeopleComponent
} }
ngOnDestroy(): void { ngOnDestroy(): void {
this.destroy$.next(); super.ngOnDestroy();
this.destroy$.complete();
} }
async load() { async load() {

View File

@ -281,7 +281,7 @@ export class AppComponent implements OnDestroy, OnInit {
await this.stateEventRunnerService.handleEvent("logout", userId as UserId); await this.stateEventRunnerService.handleEvent("logout", userId as UserId);
this.searchService.clearIndex(); await this.searchService.clearIndex();
this.authService.logOut(async () => { this.authService.logOut(async () => {
if (expired) { if (expired) {
this.platformUtilsService.showToast( this.platformUtilsService.showToast(

View File

@ -272,7 +272,7 @@ export class VaultComponent implements OnInit, OnDestroy {
concatMap(async ([ciphers, filter, searchText]) => { concatMap(async ([ciphers, filter, searchText]) => {
const filterFunction = createFilterFunction(filter); const filterFunction = createFilterFunction(filter);
if (this.searchService.isSearchable(searchText)) { if (await this.searchService.isSearchable(searchText)) {
return await this.searchService.searchCiphers(searchText, [filterFunction], ciphers); return await this.searchService.searchCiphers(searchText, [filterFunction], ciphers);
} }
@ -283,7 +283,7 @@ export class VaultComponent implements OnInit, OnDestroy {
const collections$ = combineLatest([nestedCollections$, filter$, this.currentSearchText$]).pipe( const collections$ = combineLatest([nestedCollections$, filter$, this.currentSearchText$]).pipe(
filter(([collections, filter]) => collections != undefined && filter != undefined), filter(([collections, filter]) => collections != undefined && filter != undefined),
map(([collections, filter, searchText]) => { concatMap(async ([collections, filter, searchText]) => {
if (filter.collectionId === undefined || filter.collectionId === Unassigned) { if (filter.collectionId === undefined || filter.collectionId === Unassigned) {
return []; return [];
} }
@ -303,7 +303,7 @@ export class VaultComponent implements OnInit, OnDestroy {
collectionsToReturn = selectedCollection?.children.map((c) => c.node) ?? []; collectionsToReturn = selectedCollection?.children.map((c) => c.node) ?? [];
} }
if (this.searchService.isSearchable(searchText)) { if (await this.searchService.isSearchable(searchText)) {
collectionsToReturn = this.searchPipe.transform( collectionsToReturn = this.searchPipe.transform(
collectionsToReturn, collectionsToReturn,
searchText, searchText,

View File

@ -331,7 +331,7 @@ export class VaultComponent implements OnInit, OnDestroy {
} }
} }
this.searchService.indexCiphers(ciphers, organization.id); await this.searchService.indexCiphers(ciphers, organization.id);
return ciphers; return ciphers;
}), }),
); );
@ -350,7 +350,7 @@ export class VaultComponent implements OnInit, OnDestroy {
const collections$ = combineLatest([nestedCollections$, filter$, this.currentSearchText$]).pipe( const collections$ = combineLatest([nestedCollections$, filter$, this.currentSearchText$]).pipe(
filter(([collections, filter]) => collections != undefined && filter != undefined), filter(([collections, filter]) => collections != undefined && filter != undefined),
map(([collections, filter, searchText]) => { concatMap(async ([collections, filter, searchText]) => {
if ( if (
filter.collectionId === Unassigned || filter.collectionId === Unassigned ||
(filter.collectionId === undefined && filter.type !== undefined) (filter.collectionId === undefined && filter.type !== undefined)
@ -369,7 +369,7 @@ export class VaultComponent implements OnInit, OnDestroy {
collectionsToReturn = selectedCollection?.children.map((c) => c.node) ?? []; collectionsToReturn = selectedCollection?.children.map((c) => c.node) ?? [];
} }
if (this.searchService.isSearchable(searchText)) { if (await this.searchService.isSearchable(searchText)) {
collectionsToReturn = this.searchPipe.transform( collectionsToReturn = this.searchPipe.transform(
collectionsToReturn, collectionsToReturn,
searchText, searchText,
@ -436,7 +436,7 @@ export class VaultComponent implements OnInit, OnDestroy {
const filterFunction = createFilterFunction(filter); const filterFunction = createFilterFunction(filter);
if (this.searchService.isSearchable(searchText)) { if (await this.searchService.isSearchable(searchText)) {
return await this.searchService.searchCiphers(searchText, [filterFunction], ciphers); return await this.searchService.searchCiphers(searchText, [filterFunction], ciphers);
} }

View File

@ -1,7 +1,7 @@
import { Component, OnInit } from "@angular/core"; import { Component, OnInit } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router"; import { ActivatedRoute, Router } from "@angular/router";
import { firstValueFrom } from "rxjs"; import { BehaviorSubject, Subject, firstValueFrom, from } from "rxjs";
import { first } from "rxjs/operators"; import { first, switchMap, takeUntil } from "rxjs/operators";
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";
@ -39,7 +39,6 @@ const DisallowedPlanTypes = [
// eslint-disable-next-line rxjs-angular/prefer-takeuntil // eslint-disable-next-line rxjs-angular/prefer-takeuntil
export class ClientsComponent implements OnInit { export class ClientsComponent implements OnInit {
providerId: string; providerId: string;
searchText: string;
addableOrganizations: Organization[]; addableOrganizations: Organization[];
loading = true; loading = true;
manageOrganizations = false; manageOrganizations = false;
@ -57,6 +56,17 @@ export class ClientsComponent implements OnInit {
FeatureFlag.EnableConsolidatedBilling, FeatureFlag.EnableConsolidatedBilling,
false, false,
); );
private destroy$ = new Subject<void>();
private _searchText$ = new BehaviorSubject<string>("");
private isSearching: boolean = false;
get searchText() {
return this._searchText$.value;
}
set searchText(value: string) {
this._searchText$.next(value);
}
constructor( constructor(
private route: ActivatedRoute, private route: ActivatedRoute,
@ -77,27 +87,41 @@ export class ClientsComponent implements OnInit {
) {} ) {}
async ngOnInit() { async ngOnInit() {
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
const enableConsolidatedBilling = await firstValueFrom(this.enableConsolidatedBilling$); const enableConsolidatedBilling = await firstValueFrom(this.enableConsolidatedBilling$);
if (enableConsolidatedBilling) { if (enableConsolidatedBilling) {
await this.router.navigate(["../manage-client-organizations"], { relativeTo: this.route }); await this.router.navigate(["../manage-client-organizations"], { relativeTo: this.route });
} else { } else {
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe this.route.parent.params
this.route.parent.params.subscribe(async (params) => { .pipe(
switchMap((params) => {
this.providerId = params.providerId; this.providerId = params.providerId;
return from(this.load());
}),
takeUntil(this.destroy$),
)
.subscribe();
await this.load(); this.route.queryParams.pipe(first(), takeUntil(this.destroy$)).subscribe((qParams) => {
/* eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe, rxjs/no-nested-subscribe */
this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
this.searchText = qParams.search; this.searchText = qParams.search;
}); });
this._searchText$
.pipe(
switchMap((searchText) => from(this.searchService.isSearchable(searchText))),
takeUntil(this.destroy$),
)
.subscribe((isSearchable) => {
this.isSearching = isSearchable;
}); });
} }
} }
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
async load() { async load() {
const response = await this.apiService.getProviderClients(this.providerId); const response = await this.apiService.getProviderClients(this.providerId);
this.clients = response.data != null && response.data.length > 0 ? response.data : []; this.clients = response.data != null && response.data.length > 0 ? response.data : [];
@ -118,20 +142,14 @@ export class ClientsComponent implements OnInit {
} }
isPaging() { isPaging() {
const searching = this.isSearching(); const searching = this.isSearching;
if (searching && this.didScroll) { if (searching && this.didScroll) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.resetPaging(); this.resetPaging();
} }
return !searching && this.clients && this.clients.length > this.pageSize; return !searching && this.clients && this.clients.length > this.pageSize;
} }
isSearching() { resetPaging() {
return this.searchService.isSearchable(this.searchText);
}
async resetPaging() {
this.pagedClients = []; this.pagedClients = [];
this.loadMore(); this.loadMore();
} }

View File

@ -1,4 +1,4 @@
import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core"; import { Component, ViewChild, ViewContainerRef } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router"; import { ActivatedRoute, Router } from "@angular/router";
import { first } from "rxjs/operators"; import { first } from "rxjs/operators";
@ -34,10 +34,7 @@ import { UserAddEditComponent } from "./user-add-edit.component";
templateUrl: "people.component.html", templateUrl: "people.component.html",
}) })
// eslint-disable-next-line rxjs-angular/prefer-takeuntil // eslint-disable-next-line rxjs-angular/prefer-takeuntil
export class PeopleComponent export class PeopleComponent extends BasePeopleComponent<ProviderUserUserDetailsResponse> {
extends BasePeopleComponent<ProviderUserUserDetailsResponse>
implements OnInit
{
@ViewChild("addEdit", { read: ViewContainerRef, static: true }) addEditModalRef: ViewContainerRef; @ViewChild("addEdit", { read: ViewContainerRef, static: true }) addEditModalRef: ViewContainerRef;
@ViewChild("groupsTemplate", { read: ViewContainerRef, static: true }) @ViewChild("groupsTemplate", { read: ViewContainerRef, static: true })
groupsModalRef: ViewContainerRef; groupsModalRef: ViewContainerRef;
@ -119,6 +116,10 @@ export class PeopleComponent
}); });
} }
ngOnDestroy(): void {
super.ngOnDestroy();
}
getUsers(): Promise<ListResponse<ProviderUserUserDetailsResponse>> { getUsers(): Promise<ListResponse<ProviderUserUserDetailsResponse>> {
return this.apiService.getProviderUsers(this.providerId); return this.apiService.getProviderUsers(this.providerId);
} }

View File

@ -1,8 +1,8 @@
import { SelectionModel } from "@angular/cdk/collections"; import { SelectionModel } from "@angular/cdk/collections";
import { Component, OnInit } from "@angular/core"; import { Component, OnDestroy, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router"; import { ActivatedRoute } from "@angular/router";
import { firstValueFrom } from "rxjs"; import { BehaviorSubject, Subject, firstValueFrom, from } from "rxjs";
import { first } from "rxjs/operators"; import { first, switchMap, takeUntil } from "rxjs/operators";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { SearchService } from "@bitwarden/common/abstractions/search.service";
@ -23,12 +23,22 @@ import { ManageClientOrganizationSubscriptionComponent } from "./manage-client-o
}) })
// eslint-disable-next-line rxjs-angular/prefer-takeuntil // eslint-disable-next-line rxjs-angular/prefer-takeuntil
export class ManageClientOrganizationsComponent implements OnInit { export class ManageClientOrganizationsComponent implements OnInit, OnDestroy {
providerId: string; providerId: string;
loading = true; loading = true;
manageOrganizations = false; manageOrganizations = false;
private destroy$ = new Subject<void>();
private _searchText$ = new BehaviorSubject<string>("");
private isSearching: boolean = false;
get searchText() {
return this._searchText$.value;
}
set searchText(search: string) { set searchText(search: string) {
this._searchText$.value;
this.selection.clear(); this.selection.clear();
this.dataSource.filter = search; this.dataSource.filter = search;
} }
@ -67,6 +77,20 @@ export class ManageClientOrganizationsComponent implements OnInit {
this.searchText = qParams.search; this.searchText = qParams.search;
}); });
}); });
this._searchText$
.pipe(
switchMap((searchText) => from(this.searchService.isSearchable(searchText))),
takeUntil(this.destroy$),
)
.subscribe((isSearchable) => {
this.isSearching = isSearchable;
});
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
} }
async load() { async load() {
@ -80,7 +104,7 @@ export class ManageClientOrganizationsComponent implements OnInit {
} }
isPaging() { isPaging() {
const searching = this.isSearching(); const searching = this.isSearching;
if (searching && this.didScroll) { if (searching && this.didScroll) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises // eslint-disable-next-line @typescript-eslint/no-floating-promises
@ -89,10 +113,6 @@ export class ManageClientOrganizationsComponent implements OnInit {
return !searching && this.clients && this.clients.length > this.pageSize; return !searching && this.clients && this.clients.length > this.pageSize;
} }
isSearching() {
return this.searchService.isSearchable(this.searchText);
}
async resetPaging() { async resetPaging() {
this.pagedClients = []; this.pagedClients = [];
this.loadMore(); this.loadMore();

View File

@ -726,7 +726,7 @@ const safeProviders: SafeProvider[] = [
safeProvider({ safeProvider({
provide: SearchServiceAbstraction, provide: SearchServiceAbstraction,
useClass: SearchService, useClass: SearchService,
deps: [LogService, I18nServiceAbstraction], deps: [LogService, I18nServiceAbstraction, StateProvider],
}), }),
safeProvider({ safeProvider({
provide: NotificationsServiceAbstraction, provide: NotificationsServiceAbstraction,

View File

@ -1,5 +1,13 @@
import { Directive, NgZone, OnDestroy, OnInit } from "@angular/core"; import { Directive, NgZone, OnDestroy, OnInit } from "@angular/core";
import { Subject, firstValueFrom, mergeMap, takeUntil } from "rxjs"; import {
BehaviorSubject,
Subject,
firstValueFrom,
mergeMap,
from,
switchMap,
takeUntil,
} from "rxjs";
import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
@ -24,7 +32,6 @@ export class SendComponent implements OnInit, OnDestroy {
expired = false; expired = false;
type: SendType = null; type: SendType = null;
sends: SendView[] = []; sends: SendView[] = [];
searchText: string;
selectedType: SendType; selectedType: SendType;
selectedAll: boolean; selectedAll: boolean;
filter: (cipher: SendView) => boolean; filter: (cipher: SendView) => boolean;
@ -39,6 +46,8 @@ export class SendComponent implements OnInit, OnDestroy {
private searchTimeout: any; private searchTimeout: any;
private destroy$ = new Subject<void>(); private destroy$ = new Subject<void>();
private _filteredSends: SendView[]; private _filteredSends: SendView[];
private _searchText$ = new BehaviorSubject<string>("");
protected isSearchable: boolean = false;
get filteredSends(): SendView[] { get filteredSends(): SendView[] {
return this._filteredSends; return this._filteredSends;
@ -48,6 +57,14 @@ export class SendComponent implements OnInit, OnDestroy {
this._filteredSends = filteredSends; this._filteredSends = filteredSends;
} }
get searchText() {
return this._searchText$.value;
}
set searchText(value: string) {
this._searchText$.next(value);
}
constructor( constructor(
protected sendService: SendService, protected sendService: SendService,
protected i18nService: I18nService, protected i18nService: I18nService,
@ -68,6 +85,15 @@ export class SendComponent implements OnInit, OnDestroy {
.subscribe((policyAppliesToActiveUser) => { .subscribe((policyAppliesToActiveUser) => {
this.disableSend = policyAppliesToActiveUser; this.disableSend = policyAppliesToActiveUser;
}); });
this._searchText$
.pipe(
switchMap((searchText) => from(this.searchService.isSearchable(searchText))),
takeUntil(this.destroy$),
)
.subscribe((isSearchable) => {
this.isSearchable = isSearchable;
});
} }
ngOnDestroy() { ngOnDestroy() {
@ -122,14 +148,14 @@ export class SendComponent implements OnInit, OnDestroy {
clearTimeout(this.searchTimeout); clearTimeout(this.searchTimeout);
} }
if (timeout == null) { if (timeout == null) {
this.hasSearched = this.searchService.isSearchable(this.searchText); this.hasSearched = this.isSearchable;
this.filteredSends = this.sends.filter((s) => this.filter == null || this.filter(s)); this.filteredSends = this.sends.filter((s) => this.filter == null || this.filter(s));
this.applyTextSearch(); this.applyTextSearch();
return; return;
} }
this.searchPending = true; this.searchPending = true;
this.searchTimeout = setTimeout(async () => { this.searchTimeout = setTimeout(async () => {
this.hasSearched = this.searchService.isSearchable(this.searchText); this.hasSearched = this.isSearchable;
this.filteredSends = this.sends.filter((s) => this.filter == null || this.filter(s)); this.filteredSends = this.sends.filter((s) => this.filter == null || this.filter(s));
this.applyTextSearch(); this.applyTextSearch();
this.searchPending = false; this.searchPending = false;

View File

@ -1,4 +1,5 @@
import { Directive, EventEmitter, Input, Output } from "@angular/core"; import { Directive, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
import { BehaviorSubject, Subject, from, switchMap, takeUntil } from "rxjs";
import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
@ -6,7 +7,7 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
@Directive() @Directive()
export class VaultItemsComponent { export class VaultItemsComponent implements OnInit, OnDestroy {
@Input() activeCipherId: string = null; @Input() activeCipherId: string = null;
@Output() onCipherClicked = new EventEmitter<CipherView>(); @Output() onCipherClicked = new EventEmitter<CipherView>();
@Output() onCipherRightClicked = new EventEmitter<CipherView>(); @Output() onCipherRightClicked = new EventEmitter<CipherView>();
@ -23,13 +24,15 @@ export class VaultItemsComponent {
protected searchPending = false; protected searchPending = false;
private destroy$ = new Subject<void>();
private searchTimeout: any = null; private searchTimeout: any = null;
private _searchText: string = null; private isSearchable: boolean = false;
private _searchText$ = new BehaviorSubject<string>("");
get searchText() { get searchText() {
return this._searchText; return this._searchText$.value;
} }
set searchText(value: string) { set searchText(value: string) {
this._searchText = value; this._searchText$.next(value);
} }
constructor( constructor(
@ -37,6 +40,21 @@ export class VaultItemsComponent {
protected cipherService: CipherService, protected cipherService: CipherService,
) {} ) {}
ngOnInit(): void {
this._searchText$
.pipe(
switchMap((searchText) => from(this.searchService.isSearchable(searchText))),
takeUntil(this.destroy$),
)
.subscribe((isSearchable) => {
this.isSearchable = isSearchable;
});
}
ngOnDestroy(): void {
throw new Error("Method not implemented.");
}
async load(filter: (cipher: CipherView) => boolean = null, deleted = false) { async load(filter: (cipher: CipherView) => boolean = null, deleted = false) {
this.deleted = deleted ?? false; this.deleted = deleted ?? false;
await this.applyFilter(filter); await this.applyFilter(filter);
@ -90,7 +108,7 @@ export class VaultItemsComponent {
} }
isSearching() { isSearching() {
return !this.searchPending && this.searchService.isSearchable(this.searchText); return !this.searchPending && this.isSearchable;
} }
protected deletedFilter: (cipher: CipherView) => boolean = (c) => c.isDeleted === this.deleted; protected deletedFilter: (cipher: CipherView) => boolean = (c) => c.isDeleted === this.deleted;

View File

@ -1,11 +1,15 @@
import { Observable } from "rxjs";
import { SendView } from "../tools/send/models/view/send.view"; import { SendView } from "../tools/send/models/view/send.view";
import { IndexedEntityId } from "../types/guid";
import { CipherView } from "../vault/models/view/cipher.view"; import { CipherView } from "../vault/models/view/cipher.view";
export abstract class SearchService { export abstract class SearchService {
indexedEntityId?: string = null; indexedEntityId$: Observable<IndexedEntityId | null>;
clearIndex: () => void;
isSearchable: (query: string) => boolean; clearIndex: () => Promise<void>;
indexCiphers: (ciphersToIndex: CipherView[], indexedEntityGuid?: string) => void; isSearchable: (query: string) => Promise<boolean>;
indexCiphers: (ciphersToIndex: CipherView[], indexedEntityGuid?: string) => Promise<void>;
searchCiphers: ( searchCiphers: (
query: string, query: string,
filter?: ((cipher: CipherView) => boolean) | ((cipher: CipherView) => boolean)[], filter?: ((cipher: CipherView) => boolean) | ((cipher: CipherView) => boolean)[],

View File

@ -127,3 +127,4 @@ export const VAULT_SETTINGS_DISK = new StateDefinition("vaultSettings", "disk",
web: "disk-local", web: "disk-local",
}); });
export const VAULT_BROWSER_MEMORY = new StateDefinition("vaultBrowser", "memory"); export const VAULT_BROWSER_MEMORY = new StateDefinition("vaultBrowser", "memory");
export const VAULT_SEARCH_MEMORY = new StateDefinition("vaultSearch", "memory");

View File

@ -1,20 +1,91 @@
import * as lunr from "lunr"; import * as lunr from "lunr";
import { Observable, firstValueFrom, map } from "rxjs";
import { Jsonify } from "type-fest";
import { SearchService as SearchServiceAbstraction } from "../abstractions/search.service"; import { SearchService as SearchServiceAbstraction } from "../abstractions/search.service";
import { UriMatchStrategy } from "../models/domain/domain-service"; import { UriMatchStrategy } from "../models/domain/domain-service";
import { I18nService } from "../platform/abstractions/i18n.service"; import { I18nService } from "../platform/abstractions/i18n.service";
import { LogService } from "../platform/abstractions/log.service"; import { LogService } from "../platform/abstractions/log.service";
import {
ActiveUserState,
StateProvider,
UserKeyDefinition,
VAULT_SEARCH_MEMORY,
} from "../platform/state";
import { SendView } from "../tools/send/models/view/send.view"; import { SendView } from "../tools/send/models/view/send.view";
import { IndexedEntityId } from "../types/guid";
import { FieldType } from "../vault/enums"; import { FieldType } from "../vault/enums";
import { CipherType } from "../vault/enums/cipher-type"; import { CipherType } from "../vault/enums/cipher-type";
import { CipherView } from "../vault/models/view/cipher.view"; import { CipherView } from "../vault/models/view/cipher.view";
export type SerializedLunrIndex = {
version: string;
fields: string[];
fieldVectors: [string, number[]];
invertedIndex: any[];
pipeline: string[];
};
/**
* The `KeyDefinition` for accessing the search index in application state.
* The key definition is configured to clear the index when the user locks the vault.
*/
export const LUNR_SEARCH_INDEX = new UserKeyDefinition<SerializedLunrIndex>(
VAULT_SEARCH_MEMORY,
"searchIndex",
{
deserializer: (obj: Jsonify<SerializedLunrIndex>) => obj,
clearOn: ["lock"],
},
);
/**
* The `KeyDefinition` for accessing the ID of the entity currently indexed by Lunr search.
* The key definition is configured to clear the indexed entity ID when the user locks the vault.
*/
export const LUNR_SEARCH_INDEXED_ENTITY_ID = new UserKeyDefinition<IndexedEntityId>(
VAULT_SEARCH_MEMORY,
"searchIndexedEntityId",
{
deserializer: (obj: Jsonify<IndexedEntityId>) => obj,
clearOn: ["lock"],
},
);
/**
* The `KeyDefinition` for accessing the state of Lunr search indexing, indicating whether the Lunr search index is currently being built or updating.
* The key definition is configured to clear the indexing state when the user locks the vault.
*/
export const LUNR_SEARCH_INDEXING = new UserKeyDefinition<boolean>(
VAULT_SEARCH_MEMORY,
"isIndexing",
{
deserializer: (obj: Jsonify<boolean>) => obj,
clearOn: ["lock"],
},
);
export class SearchService implements SearchServiceAbstraction { export class SearchService implements SearchServiceAbstraction {
private static registeredPipeline = false; private static registeredPipeline = false;
indexedEntityId?: string = null; private searchIndexState: ActiveUserState<SerializedLunrIndex> =
private indexing = false; this.stateProvider.getActive(LUNR_SEARCH_INDEX);
private index: lunr.Index = null; private readonly index$: Observable<lunr.Index | null> = this.searchIndexState.state$.pipe(
map((searchIndex) => (searchIndex ? lunr.Index.load(searchIndex) : null)),
);
private searchIndexEntityIdState: ActiveUserState<IndexedEntityId> = this.stateProvider.getActive(
LUNR_SEARCH_INDEXED_ENTITY_ID,
);
readonly indexedEntityId$: Observable<IndexedEntityId | null> =
this.searchIndexEntityIdState.state$.pipe(map((id) => id));
private searchIsIndexingState: ActiveUserState<boolean> =
this.stateProvider.getActive(LUNR_SEARCH_INDEXING);
private readonly searchIsIndexing$: Observable<boolean> = this.searchIsIndexingState.state$.pipe(
map((indexing) => indexing ?? false),
);
private readonly immediateSearchLocales: string[] = ["zh-CN", "zh-TW", "ja", "ko", "vi"]; private readonly immediateSearchLocales: string[] = ["zh-CN", "zh-TW", "ja", "ko", "vi"];
private readonly defaultSearchableMinLength: number = 2; private readonly defaultSearchableMinLength: number = 2;
private searchableMinLength: number = this.defaultSearchableMinLength; private searchableMinLength: number = this.defaultSearchableMinLength;
@ -22,6 +93,7 @@ export class SearchService implements SearchServiceAbstraction {
constructor( constructor(
private logService: LogService, private logService: LogService,
private i18nService: I18nService, private i18nService: I18nService,
private stateProvider: StateProvider,
) { ) {
this.i18nService.locale$.subscribe((locale) => { this.i18nService.locale$.subscribe((locale) => {
if (this.immediateSearchLocales.indexOf(locale) !== -1) { if (this.immediateSearchLocales.indexOf(locale) !== -1) {
@ -40,28 +112,29 @@ export class SearchService implements SearchServiceAbstraction {
} }
} }
clearIndex(): void { async clearIndex(): Promise<void> {
this.indexedEntityId = null; await this.searchIndexEntityIdState.update(() => null);
this.index = null; await this.searchIndexState.update(() => null);
await this.searchIsIndexingState.update(() => null);
} }
isSearchable(query: string): boolean { async isSearchable(query: string): Promise<boolean> {
query = SearchService.normalizeSearchQuery(query); query = SearchService.normalizeSearchQuery(query);
const index = await this.getIndexForSearch();
const notSearchable = const notSearchable =
query == null || query == null ||
(this.index == null && query.length < this.searchableMinLength) || (index == null && query.length < this.searchableMinLength) ||
(this.index != null && query.length < this.searchableMinLength && query.indexOf(">") !== 0); (index != null && query.length < this.searchableMinLength && query.indexOf(">") !== 0);
return !notSearchable; return !notSearchable;
} }
indexCiphers(ciphers: CipherView[], indexedEntityId?: string): void { async indexCiphers(ciphers: CipherView[], indexedEntityId?: string): Promise<void> {
if (this.indexing) { if (await this.getIsIndexing()) {
return; return;
} }
this.indexing = true; await this.setIsIndexing(true);
this.indexedEntityId = indexedEntityId; await this.setIndexedEntityIdForSearch(indexedEntityId as IndexedEntityId);
this.index = null;
const builder = new lunr.Builder(); const builder = new lunr.Builder();
builder.pipeline.add(this.normalizeAccentsPipelineFunction); builder.pipeline.add(this.normalizeAccentsPipelineFunction);
builder.ref("id"); builder.ref("id");
@ -95,9 +168,11 @@ export class SearchService implements SearchServiceAbstraction {
builder.field("organizationid", { extractor: (c: CipherView) => c.organizationId }); builder.field("organizationid", { extractor: (c: CipherView) => c.organizationId });
ciphers = ciphers || []; ciphers = ciphers || [];
ciphers.forEach((c) => builder.add(c)); ciphers.forEach((c) => builder.add(c));
this.index = builder.build(); const index = builder.build();
this.indexing = false; await this.setIndexForSearch(index.toJSON() as SerializedLunrIndex);
await this.setIsIndexing(false);
this.logService.info("Finished search indexing"); this.logService.info("Finished search indexing");
} }
@ -125,18 +200,18 @@ export class SearchService implements SearchServiceAbstraction {
ciphers = ciphers.filter(filter as (cipher: CipherView) => boolean); ciphers = ciphers.filter(filter as (cipher: CipherView) => boolean);
} }
if (!this.isSearchable(query)) { if (!(await this.isSearchable(query))) {
return ciphers; return ciphers;
} }
if (this.indexing) { if (await this.getIsIndexing()) {
await new Promise((r) => setTimeout(r, 250)); await new Promise((r) => setTimeout(r, 250));
if (this.indexing) { if (await this.getIsIndexing()) {
await new Promise((r) => setTimeout(r, 500)); await new Promise((r) => setTimeout(r, 500));
} }
} }
const index = this.getIndexForSearch(); const index = await this.getIndexForSearch();
if (index == null) { if (index == null) {
// Fall back to basic search if index is not available // Fall back to basic search if index is not available
return this.searchCiphersBasic(ciphers, query); return this.searchCiphersBasic(ciphers, query);
@ -230,8 +305,24 @@ export class SearchService implements SearchServiceAbstraction {
return sendsMatched.concat(lowPriorityMatched); return sendsMatched.concat(lowPriorityMatched);
} }
getIndexForSearch(): lunr.Index { async getIndexForSearch(): Promise<lunr.Index | null> {
return this.index; return await firstValueFrom(this.index$);
}
private async setIndexForSearch(index: SerializedLunrIndex): Promise<void> {
await this.searchIndexState.update(() => index);
}
private async setIndexedEntityIdForSearch(indexedEntityId: IndexedEntityId): Promise<void> {
await this.searchIndexEntityIdState.update(() => indexedEntityId);
}
private async setIsIndexing(indexing: boolean): Promise<void> {
await this.searchIsIndexingState.update(() => indexing);
}
private async getIsIndexing(): Promise<boolean> {
return await firstValueFrom(this.searchIsIndexing$);
} }
private fieldExtractor(c: CipherView, joined: boolean) { private fieldExtractor(c: CipherView, joined: boolean) {

View File

@ -91,7 +91,7 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
const currentUserId = (await firstValueFrom(this.accountService.activeAccount$)).id; const currentUserId = (await firstValueFrom(this.accountService.activeAccount$)).id;
if (userId == null || userId === currentUserId) { if (userId == null || userId === currentUserId) {
this.searchService.clearIndex(); await this.searchService.clearIndex();
await this.folderService.clearCache(); await this.folderService.clearCache();
await this.collectionService.clearActiveUserCache(); await this.collectionService.clearActiveUserCache();
} }

View File

@ -8,3 +8,4 @@ export type CollectionId = Opaque<string, "CollectionId">;
export type ProviderId = Opaque<string, "ProviderId">; export type ProviderId = Opaque<string, "ProviderId">;
export type PolicyId = Opaque<string, "PolicyId">; export type PolicyId = Opaque<string, "PolicyId">;
export type CipherId = Opaque<string, "CipherId">; export type CipherId = Opaque<string, "CipherId">;
export type IndexedEntityId = Opaque<string, "IndexedEntityId">;

View File

@ -89,9 +89,9 @@ export class CipherService implements CipherServiceAbstraction {
} }
if (this.searchService != null) { if (this.searchService != null) {
if (value == null) { if (value == null) {
this.searchService.clearIndex(); await this.searchService.clearIndex();
} else { } else {
this.searchService.indexCiphers(value); await this.searchService.indexCiphers(value);
} }
} }
} }
@ -333,9 +333,10 @@ export class CipherService implements CipherServiceAbstraction {
private async reindexCiphers() { private async reindexCiphers() {
const userId = await this.stateService.getUserId(); const userId = await this.stateService.getUserId();
const reindexRequired = const reindexRequired =
this.searchService != null && (this.searchService.indexedEntityId ?? userId) !== userId; this.searchService != null &&
((await firstValueFrom(this.searchService.indexedEntityId$)) ?? userId) !== userId;
if (reindexRequired) { if (reindexRequired) {
this.searchService.indexCiphers(await this.getDecryptedCipherCache(), userId); await this.searchService.indexCiphers(await this.getDecryptedCipherCache(), userId);
} }
} }