1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-10 05:30:01 +00:00

PoC: filter service can tag ciphers

This commit is contained in:
Matt Gibson
2025-03-20 06:55:24 -07:00
parent 1af9e9e2bc
commit b8ba35f576
3 changed files with 124 additions and 19 deletions

View File

@@ -25,6 +25,7 @@ import {
takeUntil,
tap,
catchError,
combineLatestWith,
} from "rxjs/operators";
import {
@@ -66,6 +67,7 @@ import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-repromp
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 { SavedFiltersService } from "@bitwarden/common/vault/search/saved-filters.service";
import { ServiceUtils } from "@bitwarden/common/vault/service-utils";
import { CardComponent, DialogService, Icons, ToastService } from "@bitwarden/components";
import {
@@ -274,6 +276,7 @@ export class VaultComponent implements OnInit, OnDestroy {
private organizationBillingService: OrganizationBillingServiceAbstraction,
private billingNotificationService: BillingNotificationService,
private folderService: FolderService,
private savedFilterService: SavedFiltersService,
) {}
async ngOnInit() {
@@ -356,12 +359,22 @@ export class VaultComponent implements OnInit, OnDestroy {
});
}),
);
const savedFilters$ = this.accountService.activeAccount$.pipe(
switchMap((account) => this.savedFilterService.filtersFor$(account.id)),
map((filters) => {
const savedFilters: [string, string][] = [];
for (const [key, value] of Object.entries(filters ?? {})) {
savedFilters.push([key, value]);
}
return savedFilters;
}),
);
const ciphers$ = combineLatest([this.currentSearchText$, context$]).pipe(
switchMap(([currentText, context]) => {
return this.filterService.filter(of([currentText, context]));
}),
// this.filterService.filter,
// switchMap(([currentText, context]) => {
// return this.filterService.filter(of([currentText, context]));
// }),
this.filterService.filter,
switchMap((result) => {
if (result.isError) {
// return all ciphers
@@ -371,6 +384,20 @@ export class VaultComponent implements OnInit, OnDestroy {
return of(result.ciphers);
}
}),
combineLatestWith(context$),
map(([filtered, prevContext]) => {
prevContext.ciphers = filtered;
return prevContext;
}),
combineLatestWith(savedFilters$),
switchMap(([context, savedFilters]) => {
return this.filterService.tag(of([savedFilters, context]));
}),
// this.filterService.tag,
map((result) => {
// ignore tagging errors for now
return result.ciphers;
}),
shareReplay({ refCount: true, bufferSize: 1 }),
);

View File

@@ -9,14 +9,25 @@ import { CipherView } from "../models/view/cipher.view";
import { parseQuery } from "./parse";
import {
FilterResult,
FilterTagged,
ObservableSearchContextInput,
ParseResult,
SearchContext,
TagResult,
} from "./query.types";
export abstract class FilterService {
abstract readonly parse: OperatorFunction<string, ParseResult>;
abstract readonly filter: OperatorFunction<[string | ParseResult, SearchContext], FilterResult>;
/**
* Tags ciphers based on the provided queries.
*
* The queries are in the form of a record, where the key is the name of the tag and the value is either a string or a ParseResult query to perform.
*/
abstract readonly tag: OperatorFunction<
[[string, string][] | [string, ParseResult][], SearchContext],
TagResult
>;
abstract context$(context: ObservableSearchContextInput): Observable<SearchContext>;
}
@@ -60,25 +71,25 @@ export class DefaultFilterService implements FilterService {
processInstructions: null,
});
} else {
return query.pipe(
map((query: string) => {
try {
return {
isError: false as const,
processInstructions: parseQuery(query, this.logService),
};
} catch {
return {
isError: true as const,
processInstructions: null,
};
}
}),
);
return query.pipe(map((query: string) => this.parseQueryString(query)));
}
};
}
private parseQueryString(query: string): ParseResult {
try {
return {
isError: false as const,
processInstructions: parseQuery(query, this.logService),
};
} catch {
return {
isError: true as const,
processInstructions: null,
};
}
}
get filter(): OperatorFunction<[string | ParseResult, SearchContext], FilterResult> {
return pipe(
switchMap(([queryOrParsedQuery, context]) => {
@@ -106,6 +117,66 @@ export class DefaultFilterService implements FilterService {
);
}
get tag(): OperatorFunction<
[[string, string][] | [string, ParseResult][], SearchContext],
TagResult
> {
return pipe(
switchMap(([queryOrParsedQueries, context]) => {
const parsedQueries: [string, ParseResult][] = [];
queryOrParsedQueries.forEach(([name, q]) => {
if (q == null || typeof q === "string") {
// it's a string that needs parsing
parsedQueries.push([name, this.parseQueryString(q as string)]);
} else {
// It's a parsed query
parsedQueries.push([name, q]);
}
});
return combineLatest([of(parsedQueries), of(context)]);
}),
map(([parseResults, context]) => {
if (parseResults.length === 0) {
return {
ciphers: context.ciphers as FilterTagged<CipherView>[],
isError: false,
};
}
// Reduce the parse results to a single result
return parseResults.reduce(
(acc, [name, parseResult]) => {
// cannot process if any parse result is error
if (parseResult.isError) {
return {
ciphers: acc.ciphers,
isError: true,
};
}
// Identify the ciphers to tag for this query
const hitCipherIds = parseResult.processInstructions
.filter(context)
.ciphers.map((c) => c.id);
return {
// tag ciphers for this query
ciphers: acc.ciphers.map((c) => {
const tagged = c as FilterTagged<CipherView>;
tagged.tags ??= [];
if (hitCipherIds.includes(c.id)) {
tagged.tags.push(name);
}
return tagged;
}),
isError: acc.isError,
};
},
// Initial value of the accumulator
{ ciphers: context.ciphers, isError: false } as TagResult,
);
}),
);
}
private buildIndex(ciphers: CipherView[]) {
const builder = new lunr.Builder();
builder.pipeline.add(this.normalizeAccentsPipelineFunction);

View File

@@ -31,6 +31,13 @@ export type FilterResult =
isError: true;
};
export type TagResult = {
ciphers: FilterTagged<CipherView>[];
isError: boolean;
};
export type FilterTagged<T> = T & { tags: string[] };
export type ProcessInstructions = {
filter: (context: SearchContext) => SearchContext;
ast: Search;