From 2fe8c491ede7397af49b7b91a888b05324a5f34c Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Wed, 19 Mar 2025 16:34:11 -0700 Subject: [PATCH] Remove filter component This is a PoC commit, and would need significant clean up. We shouldn't be using the vault-filter component at all anymore, but it was the most expedient way to implement the new UI --- .../components/vault-filter.component.html | 49 ++---- .../individual-vault/vault.component.html | 117 +++++++------- .../vault/individual-vault/vault.component.ts | 4 +- .../src/vault/search/saved-filters.service.ts | 144 ++++++++++++++++++ libs/components/src/card/card.component.ts | 2 +- .../src/layout/layout.component.html | 2 +- .../src/search/search.component.html | 26 ++++ .../components/src/search/search.component.ts | 22 +++ libs/components/src/search/search.stories.ts | 10 ++ 9 files changed, 276 insertions(+), 100 deletions(-) create mode 100644 libs/common/src/vault/search/saved-filters.service.ts diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/components/vault-filter.component.html b/apps/web/src/app/vault/individual-vault/vault-filter/components/vault-filter.component.html index 0e90f5fad42..8dfc246c970 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/components/vault-filter.component.html +++ b/apps/web/src/app/vault/individual-vault/vault-filter/components/vault-filter.component.html @@ -1,40 +1,11 @@ -
-
- -
-
-
- {{ "filters" | i18n }} - - - -
-
-
- -
- -
- -
-
-
-
+
+
diff --git a/apps/web/src/app/vault/individual-vault/vault.component.html b/apps/web/src/app/vault/individual-vault/vault.component.html index cd011bd052b..c66509b1f41 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.html +++ b/apps/web/src/app/vault/individual-vault/vault.component.html @@ -22,67 +22,70 @@ > -
-
- -
-
+
+
{{ trashCleanupWarning }} - - -
- - {{ "loading" | i18n }} -
-
- -

{{ "noItemsInList" | i18n }}

- -
+ +
+ + {{ "loading" | i18n }} +
+
+ +

{{ "noItemsInList" | i18n }}

+ +
+
diff --git a/apps/web/src/app/vault/individual-vault/vault.component.ts b/apps/web/src/app/vault/individual-vault/vault.component.ts index 247eb3e763d..9a8972c9123 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault.component.ts @@ -67,7 +67,7 @@ import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { FilterService } from "@bitwarden/common/vault/search/filter.service"; import { ServiceUtils } from "@bitwarden/common/vault/service-utils"; -import { DialogService, Icons, ToastService } from "@bitwarden/components"; +import { CardComponent, DialogService, Icons, ToastService } from "@bitwarden/components"; import { AddEditFolderDialogComponent, AddEditFolderDialogResult, @@ -140,7 +140,7 @@ const SearchTextDebounceInterval = 200; VaultFilterModule, VaultItemsModule, SharedModule, - DecryptionFailureDialogComponent, + CardComponent, ], providers: [ RoutedVaultFilterService, diff --git a/libs/common/src/vault/search/saved-filters.service.ts b/libs/common/src/vault/search/saved-filters.service.ts new file mode 100644 index 00000000000..daed8b19f10 --- /dev/null +++ b/libs/common/src/vault/search/saved-filters.service.ts @@ -0,0 +1,144 @@ +import { Observable, combineLatestWith, map, mergeMap } from "rxjs"; +import { Tagged } from "type-fest/source/opaque"; + +// eslint-disable-next-line no-restricted-imports --- TODO move this outside of common +import { KeyService } from "../../../../key-management/src/abstractions/key.service"; +import { EncryptService } from "../../key-management/crypto/abstractions/encrypt.service"; +import { EncString, EncryptedString } from "../../platform/models/domain/enc-string"; +import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key"; +import { + SingleUserStateProvider, + UserKeyDefinition, + VAULT_SETTINGS_DISK, +} from "../../platform/state"; +import { OrganizationId, UserId } from "../../types/guid"; + +export abstract class SavedFiltersService { + abstract filtersFor$(userId: UserId): Observable>; + abstract SaveFilter(userId: UserId, name: FilterName, filter: FilterString): Promise; +} + +export type FilterName = string & Tagged<"FilterName">; +export type FilterString = string & Tagged<"FilterString">; + +type UserSearchFilters = Record; +type DecryptedSearchFilters = Record; + +const SavedFiltersStateDefinition = new UserKeyDefinition( + VAULT_SETTINGS_DISK, + "SavedFilters", + { + deserializer: (value) => { + if (value == null) { + return {}; + } + + const result: Record = {}; + for (const [k, v] of Object.entries(value)) { + const key = k as EncryptedString; + result[key] = EncString.fromJSON(v); + } + return result; + }, + clearOn: ["logout"], + }, +); + +export class DefaultSavedFiltersService implements SavedFiltersService { + constructor( + private readonly stateProvider: SingleUserStateProvider, + private readonly encryptService: EncryptService, + private readonly keyService: KeyService, + ) {} + + filtersFor$(userId: UserId, orgId?: OrganizationId): Observable { + const state = this.stateProvider.get(userId, SavedFiltersStateDefinition); + const decryptedState = state.state$.pipe( + combineLatestWith(this.keyService.userKey$(userId)), + mergeMap(async ([state, userKey]) => { + if (userKey == null || state == null) { + return {}; + } + return await this.decryptFilters(state, userKey); + }), + ); + + return decryptedState; + } + + async SaveFilter(userId: UserId, name: FilterName, filter: FilterString): Promise { + const state = this.stateProvider.get(userId, SavedFiltersStateDefinition); + await state.update((_, newState) => newState, { + combineLatestWith: state.state$.pipe( + combineLatestWith(this.keyService.userKey$(userId)), + mergeMap(async ([encrypted, userKey]) => { + return [await this.decryptFilters(encrypted, userKey), userKey] as const; + }), + map(([oldState, userKey]) => { + oldState ??= {}; + oldState[name] = filter; + return [oldState, userKey] as const; + }), + mergeMap(async ([newState, userKey]) => { + return await this.encryptHistory(newState, userKey); + }), + ), + shouldUpdate: (oldEncrypted, newEncrypted) => !recordsEqual(oldEncrypted, newEncrypted), + }); + } + + private async decryptFilters( + history: UserSearchFilters | null, + userKey: SymmetricCryptoKey | null, + ): Promise { + const decrypted: DecryptedSearchFilters = {}; + if (history == null || userKey == null) { + return decrypted; + } + + for (const [k, v] of Object.entries(history)) { + const encryptedKey = new EncString(k as EncryptedString); + const key = (await encryptedKey.decryptWithKey(userKey, this.encryptService)) as FilterName; + decrypted[key] = (await v.decryptWithKey(userKey, this.encryptService)) as FilterString; + } + return decrypted; + } + + private async encryptHistory( + history: DecryptedSearchFilters | null, + userKey: SymmetricCryptoKey | null, + ) { + if (history == null || userKey == null) { + return null; + } + + const encrypted: UserSearchFilters = {}; + for (const [k, v] of Object.entries(history)) { + const DecryptedKey = k as FilterName; + const key = (await this.encryptService.encrypt(DecryptedKey, userKey)).encryptedString!; + encrypted[key] = await this.encryptService.encrypt(v, userKey); + } + return encrypted; + } +} + +function recordsEqual( + a: Record | null, + b: Record | null, +): boolean { + if (a == null && b == null) { + return true; + } + if (a == null || b == null) { + return false; + } + if (Object.keys(a).length !== Object.keys(b).length) { + return false; + } + for (const k of Object.keys(a)) { + if (a[k].encryptedString !== b[k].encryptedString) { + return false; + } + } + return true; +} diff --git a/libs/components/src/card/card.component.ts b/libs/components/src/card/card.component.ts index fdb02f280da..813fd548459 100644 --- a/libs/components/src/card/card.component.ts +++ b/libs/components/src/card/card.component.ts @@ -7,7 +7,7 @@ import { ChangeDetectionStrategy, Component } from "@angular/core"; changeDetection: ChangeDetectionStrategy.OnPush, host: { class: - "tw-box-border tw-block tw-bg-background tw-text-main tw-border-solid tw-border-b tw-border-0 tw-border-b-secondary-300 [&:not(bit-layout_*)]:tw-rounded-lg [&:not(bit-layout_*)]:tw-border-b-shadow tw-py-4 bit-compact:tw-py-3 tw-px-3 bit-compact:tw-px-2", + "tw-box-border tw-block tw-bg-background tw-text-main tw-border-solid tw-border-b tw-border-0 tw-border-b-secondary-300 tw-rounded-lg tw-border-b-shadow tw-py-4 bit-compact:tw-py-3 tw-px-3 bit-compact:tw-px-2", }, }) export class CardComponent {} diff --git a/libs/components/src/layout/layout.component.html b/libs/components/src/layout/layout.component.html index 33b8de81572..969c0c16abb 100644 --- a/libs/components/src/layout/layout.component.html +++ b/libs/components/src/layout/layout.component.html @@ -18,7 +18,7 @@
diff --git a/libs/components/src/search/search.component.html b/libs/components/src/search/search.component.html index 609bf66e740..1af0e4b5678 100644 --- a/libs/components/src/search/search.component.html +++ b/libs/components/src/search/search.component.html @@ -22,6 +22,14 @@ [disabled]="disabled" [attr.autocomplete]="autocomplete" /> +
+ +
+
    +
  • + {{ data.name }} +
  • +
+
+
+
    (""); + protected showSavedFilters = false; @Input() disabled: boolean; @Input() placeholder: string; @Input() autocomplete: string; @Input() history: string[] | null; + @Input() savedFilters: Record | null; + + get savedFilterData() { + if (this.savedFilters == null) { + return []; + } + + return Object.entries(this.savedFilters).map(([name, filter]) => { + return { + name, + filter, + }; + }); + } get showHistory() { + // turn off history for now + return false; return this.history != null && this.focused; } + get filteredHistory$() { // TODO: Not clear if filtering is better or worse return this.textUpdated$.pipe(map((text) => this.history)); @@ -113,6 +131,10 @@ export class SearchComponent implements ControlValueAccessor, FocusableElement { this.disabled = isDisabled; } + toggleSavedFilters() { + this.showSavedFilters = !this.showSavedFilters; + } + filterToggled() { this._selectedContent.next(this._selectedContent.value !== "filter" ? "filter" : null); } diff --git a/libs/components/src/search/search.stories.ts b/libs/components/src/search/search.stories.ts index 641ca133910..5ae4c7d6780 100644 --- a/libs/components/src/search/search.stories.ts +++ b/libs/components/src/search/search.stories.ts @@ -50,3 +50,13 @@ export const WithHistory: Story = { }), args: {}, }; + +export const WithSavedFilters: Story = { + render: (args) => ({ + props: args, + template: ` + + `, + }), + args: {}, +};