mirror of
https://github.com/bitwarden/browser
synced 2025-12-10 13:23:34 +00:00
[PM-19127] - Nested Traverse Optimization (#14881)
* Draft optimization of getNestedCollectionTree * Added feature flag to wrap nestedTraverse_vNext. added the old implementation back in for feature flagging. * Correction from CR * Copied tests over for the vNext method. --------- Co-authored-by: Thomas Rittson <trittson@bitwarden.com>
This commit is contained in:
@@ -37,6 +37,31 @@ export function getNestedCollectionTree(
|
||||
return nodes;
|
||||
}
|
||||
|
||||
export function getNestedCollectionTree_vNext(
|
||||
collections: (CollectionView | CollectionAdminView)[],
|
||||
): TreeNode<CollectionView | CollectionAdminView>[] {
|
||||
if (!collections) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Collections need to be cloned because ServiceUtils.nestedTraverse actively
|
||||
// modifies the names of collections.
|
||||
// These changes risk affecting collections store in StateService.
|
||||
const clonedCollections = collections
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map(cloneCollection);
|
||||
|
||||
const nodes: TreeNode<CollectionView | CollectionAdminView>[] = [];
|
||||
clonedCollections.forEach((collection) => {
|
||||
const parts =
|
||||
collection.name != null
|
||||
? collection.name.replace(/^\/+|\/+$/g, "").split(NestingDelimiter)
|
||||
: [];
|
||||
ServiceUtils.nestedTraverse_vNext(nodes, 0, parts, collection, null, NestingDelimiter);
|
||||
});
|
||||
return nodes;
|
||||
}
|
||||
|
||||
export function getFlatCollectionTree(
|
||||
nodes: TreeNode<CollectionAdminView>[],
|
||||
): CollectionAdminView[];
|
||||
|
||||
@@ -125,7 +125,11 @@ import {
|
||||
BulkCollectionsDialogResult,
|
||||
} from "./bulk-collections-dialog";
|
||||
import { CollectionAccessRestrictedComponent } from "./collection-access-restricted.component";
|
||||
import { getNestedCollectionTree, getFlatCollectionTree } from "./utils";
|
||||
import {
|
||||
getNestedCollectionTree,
|
||||
getFlatCollectionTree,
|
||||
getNestedCollectionTree_vNext,
|
||||
} from "./utils";
|
||||
import { VaultFilterModule } from "./vault-filter/vault-filter.module";
|
||||
import { VaultHeaderComponent } from "./vault-header/vault-header.component";
|
||||
|
||||
@@ -420,9 +424,16 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
}),
|
||||
);
|
||||
|
||||
const nestedCollections$ = allCollections$.pipe(
|
||||
map((collections) => getNestedCollectionTree(collections)),
|
||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||
const nestedCollections$ = combineLatest([
|
||||
this.allCollectionsWithoutUnassigned$,
|
||||
this.configService.getFeatureFlag$(FeatureFlag.OptimizeNestedTraverseTypescript),
|
||||
]).pipe(
|
||||
map(
|
||||
([collections, shouldOptimize]) =>
|
||||
(shouldOptimize
|
||||
? getNestedCollectionTree_vNext(collections)
|
||||
: getNestedCollectionTree(collections)) as TreeNode<CollectionAdminView>[],
|
||||
),
|
||||
);
|
||||
|
||||
const collections$ = combineLatest([
|
||||
|
||||
@@ -49,7 +49,9 @@ import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction";
|
||||
import { EventType } from "@bitwarden/common/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
@@ -82,6 +84,7 @@ import {
|
||||
import {
|
||||
getNestedCollectionTree,
|
||||
getFlatCollectionTree,
|
||||
getNestedCollectionTree_vNext,
|
||||
} from "../../admin-console/organizations/collections";
|
||||
import {
|
||||
CollectionDialogAction,
|
||||
@@ -270,6 +273,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
private trialFlowService: TrialFlowService,
|
||||
private organizationBillingService: OrganizationBillingServiceAbstraction,
|
||||
private billingNotificationService: BillingNotificationService,
|
||||
private configService: ConfigService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
@@ -326,8 +330,15 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
|
||||
const filter$ = this.routedVaultFilterService.filter$;
|
||||
const allCollections$ = this.collectionService.decryptedCollections$;
|
||||
const nestedCollections$ = allCollections$.pipe(
|
||||
map((collections) => getNestedCollectionTree(collections)),
|
||||
const nestedCollections$ = combineLatest([
|
||||
allCollections$,
|
||||
this.configService.getFeatureFlag$(FeatureFlag.OptimizeNestedTraverseTypescript),
|
||||
]).pipe(
|
||||
map(([collections, shouldOptimize]) =>
|
||||
shouldOptimize
|
||||
? getNestedCollectionTree_vNext(collections)
|
||||
: getNestedCollectionTree(collections),
|
||||
),
|
||||
);
|
||||
|
||||
this.searchText$
|
||||
|
||||
@@ -13,6 +13,7 @@ export enum FeatureFlag {
|
||||
/* Admin Console Team */
|
||||
LimitItemDeletion = "pm-15493-restrict-item-deletion-to-can-manage-permission",
|
||||
SeparateCustomRolePermissions = "pm-19917-separate-custom-role-permissions",
|
||||
OptimizeNestedTraverseTypescript = "pm-21695-optimize-nested-traverse-typescript",
|
||||
|
||||
/* Auth */
|
||||
PM16117_ChangeExistingPasswordRefactor = "pm-16117-change-existing-password-refactor",
|
||||
@@ -82,6 +83,7 @@ export const DefaultFeatureFlagValue = {
|
||||
/* Admin Console Team */
|
||||
[FeatureFlag.LimitItemDeletion]: FALSE,
|
||||
[FeatureFlag.SeparateCustomRolePermissions]: FALSE,
|
||||
[FeatureFlag.OptimizeNestedTraverseTypescript]: FALSE,
|
||||
|
||||
/* Autofill */
|
||||
[FeatureFlag.BlockBrowserInjectionsByDomain]: FALSE,
|
||||
|
||||
@@ -36,6 +36,24 @@ describe("serviceUtils", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("nestedTraverse_vNext", () => {
|
||||
it("should traverse a tree and add a node at the correct position given a valid path", () => {
|
||||
const nodeToBeAdded: FakeObject = { id: "1.2.1", name: "1.2.1" };
|
||||
const path = ["1", "1.2", "1.2.1"];
|
||||
|
||||
ServiceUtils.nestedTraverse_vNext(nodeTree, 0, path, nodeToBeAdded, null, "/");
|
||||
expect(nodeTree[0].children[1].children[0].node).toEqual(nodeToBeAdded);
|
||||
});
|
||||
|
||||
it("should combine the path for missing nodes and use as the added node name given an invalid path", () => {
|
||||
const nodeToBeAdded: FakeObject = { id: "blank", name: "blank" };
|
||||
const path = ["3", "3.1", "3.1.1"];
|
||||
|
||||
ServiceUtils.nestedTraverse_vNext(nodeTree, 0, path, nodeToBeAdded, null, "/");
|
||||
expect(nodeTree[2].children[0].node.name).toEqual("3.1/3.1.1");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getTreeNodeObject", () => {
|
||||
it("should return a matching node given a single tree branch and a valid id", () => {
|
||||
const id = "1.1.1";
|
||||
|
||||
@@ -3,15 +3,6 @@
|
||||
import { ITreeNodeObject, TreeNode } from "./models/domain/tree-node";
|
||||
|
||||
export class ServiceUtils {
|
||||
/**
|
||||
* Recursively adds a node to nodeTree
|
||||
* @param {TreeNode<ITreeNodeObject>[]} nodeTree - An array of TreeNodes that the node will be added to
|
||||
* @param {number} partIndex - Index of the `parts` array that is being processed
|
||||
* @param {string[]} parts - Array of strings that represent the path to the `obj` node
|
||||
* @param {ITreeNodeObject} obj - The node to be added to the tree
|
||||
* @param {ITreeNodeObject} parent - The parent node of the `obj` node
|
||||
* @param {string} delimiter - The delimiter used to split the path string, will be used to combine the path for missing nodes
|
||||
*/
|
||||
static nestedTraverse(
|
||||
nodeTree: TreeNode<ITreeNodeObject>[],
|
||||
partIndex: number,
|
||||
@@ -70,11 +61,75 @@ export class ServiceUtils {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively adds a node to nodeTree
|
||||
* @param {TreeNode<ITreeNodeObject>[]} nodeTree - An array of TreeNodes that the node will be added to
|
||||
* @param {number} partIndex - Index of the `parts` array that is being processed
|
||||
* @param {string[]} parts - Array of strings that represent the path to the `obj` node
|
||||
* @param {ITreeNodeObject} obj - The node to be added to the tree
|
||||
* @param {ITreeNodeObject} parent - The parent node of the `obj` node
|
||||
* @param {string} delimiter - The delimiter used to split the path string, will be used to combine the path for missing nodes
|
||||
*/
|
||||
static nestedTraverse_vNext(
|
||||
nodeTree: TreeNode<ITreeNodeObject>[],
|
||||
partIndex: number,
|
||||
parts: string[],
|
||||
obj: ITreeNodeObject,
|
||||
parent: TreeNode<ITreeNodeObject> | undefined,
|
||||
delimiter: string,
|
||||
) {
|
||||
if (parts.length <= partIndex) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 'end' indicates we've traversed as far as we can based on the object name
|
||||
const end: boolean = partIndex === parts.length - 1;
|
||||
const partName: string = parts[partIndex];
|
||||
|
||||
// If we're at the end, just add the node - it doesn't matter what else is here
|
||||
if (end) {
|
||||
nodeTree.push(new TreeNode(obj, parent, partName));
|
||||
return;
|
||||
}
|
||||
|
||||
// Get matching nodes at this level by name
|
||||
// NOTE: this is effectively a loop so we only want to do it once
|
||||
const matchingNodes = nodeTree.filter((n) => n.node.name === partName);
|
||||
|
||||
// If there are no matching nodes...
|
||||
if (matchingNodes.length === 0) {
|
||||
// And we're not at the end of the path (because we didn't trigger the early return above),
|
||||
// combine the current name with the next name.
|
||||
// 1, *1.2, 1.2.1 becomes
|
||||
// 1, *1.2/1.2.1
|
||||
const newPartName = partName + delimiter + parts[partIndex + 1];
|
||||
ServiceUtils.nestedTraverse_vNext(
|
||||
nodeTree,
|
||||
0,
|
||||
[newPartName, ...parts.slice(partIndex + 2)],
|
||||
obj,
|
||||
parent,
|
||||
delimiter,
|
||||
);
|
||||
} else {
|
||||
// There is a node here with the same name, descend into it
|
||||
ServiceUtils.nestedTraverse_vNext(
|
||||
matchingNodes[0].children,
|
||||
partIndex + 1,
|
||||
parts,
|
||||
obj,
|
||||
matchingNodes[0],
|
||||
delimiter,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches a tree for a node with a matching `id`
|
||||
* @param {TreeNode<T>} nodeTree - A single TreeNode branch that will be searched
|
||||
* @param {TreeNode<T extends ITreeNodeObject>} nodeTree - A single TreeNode branch that will be searched
|
||||
* @param {string} id - The id of the node to be found
|
||||
* @returns {TreeNode<T>} The node with a matching `id`
|
||||
* @returns {TreeNode<T extends ITreeNodeObject>} The node with a matching `id`
|
||||
*/
|
||||
static getTreeNodeObject<T extends ITreeNodeObject>(
|
||||
nodeTree: TreeNode<T>,
|
||||
@@ -96,9 +151,9 @@ export class ServiceUtils {
|
||||
|
||||
/**
|
||||
* Searches an array of tree nodes for a node with a matching `id`
|
||||
* @param {TreeNode<T>} nodeTree - An array of TreeNode branches that will be searched
|
||||
* @param {TreeNode<T extends ITreeNodeObject>} nodeTree - An array of TreeNode branches that will be searched
|
||||
* @param {string} id - The id of the node to be found
|
||||
* @returns {TreeNode<T>} The node with a matching `id`
|
||||
* @returns {TreeNode<T extends ITreeNodeObject>} The node with a matching `id`
|
||||
*/
|
||||
static getTreeNodeObjectFromList<T extends ITreeNodeObject>(
|
||||
nodeTree: TreeNode<T>[],
|
||||
|
||||
Reference in New Issue
Block a user