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:
@@ -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 }),
|
||||
);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user