diff --git a/apps/desktop/src/vault/app/vault/vault-filter/filters/organization-filter.component.html b/apps/desktop/src/vault/app/vault/vault-filter/filters/organization-filter.component.html index f77f279a96f..7298133146c 100644 --- a/apps/desktop/src/vault/app/vault/vault-filter/filters/organization-filter.component.html +++ b/apps/desktop/src/vault/app/vault/vault-filter/filters/organization-filter.component.html @@ -1,6 +1,6 @@ - +
- - - - - - - - - - - - -
-
+ diff --git a/apps/web/src/app/admin-console/organizations/collections/vault-header/vault-header.component.ts b/apps/web/src/app/admin-console/organizations/collections/vault-header/vault-header.component.ts index b343d5874bc..121a5c03ffe 100644 --- a/apps/web/src/app/admin-console/organizations/collections/vault-header/vault-header.component.ts +++ b/apps/web/src/app/admin-console/organizations/collections/vault-header/vault-header.component.ts @@ -25,6 +25,7 @@ import { SearchModule, SimpleDialogOptions, } from "@bitwarden/components"; +import { NewCipherMenuComponent } from "@bitwarden/vault"; import { HeaderModule } from "../../../../layouts/header/header.module"; import { SharedModule } from "../../../../shared"; @@ -45,6 +46,7 @@ import { CollectionDialogTabType } from "../../shared/components/collection-dial HeaderModule, SearchModule, JslibModule, + NewCipherMenuComponent, ], }) export class VaultHeaderComponent { diff --git a/apps/web/src/app/dirt/reports/shared/report-card/report-card.stories.ts b/apps/web/src/app/dirt/reports/shared/report-card/report-card.stories.ts index 8048d0dcefa..d6a330b55d7 100644 --- a/apps/web/src/app/dirt/reports/shared/report-card/report-card.stories.ts +++ b/apps/web/src/app/dirt/reports/shared/report-card/report-card.stories.ts @@ -16,8 +16,7 @@ export default { component: ReportCardComponent, decorators: [ moduleMetadata({ - imports: [JslibModule, BadgeModule, IconModule, RouterTestingModule], - declarations: [PremiumBadgeComponent], + imports: [JslibModule, BadgeModule, IconModule, RouterTestingModule, PremiumBadgeComponent], }), applicationConfig({ providers: [importProvidersFrom(PreloadedEnglishI18nModule)], diff --git a/apps/web/src/app/dirt/reports/shared/report-list/report-list.stories.ts b/apps/web/src/app/dirt/reports/shared/report-list/report-list.stories.ts index 196c0994cd9..13523174913 100644 --- a/apps/web/src/app/dirt/reports/shared/report-list/report-list.stories.ts +++ b/apps/web/src/app/dirt/reports/shared/report-list/report-list.stories.ts @@ -18,8 +18,8 @@ export default { component: ReportListComponent, decorators: [ moduleMetadata({ - imports: [JslibModule, BadgeModule, RouterTestingModule, IconModule], - declarations: [PremiumBadgeComponent, ReportCardComponent], + imports: [JslibModule, BadgeModule, RouterTestingModule, IconModule, PremiumBadgeComponent], + declarations: [ReportCardComponent], }), applicationConfig({ providers: [importProvidersFrom(PreloadedEnglishI18nModule)], diff --git a/apps/web/src/app/shared/loose-components.module.ts b/apps/web/src/app/shared/loose-components.module.ts index 63e54c46a8f..97c3fa0375c 100644 --- a/apps/web/src/app/shared/loose-components.module.ts +++ b/apps/web/src/app/shared/loose-components.module.ts @@ -42,10 +42,8 @@ import { WeakPasswordsReportComponent as OrgWeakPasswordsReportComponent } from import { RemovePasswordComponent } from "../key-management/key-connector/remove-password.component"; import { HeaderModule } from "../layouts/header/header.module"; import { PremiumBadgeComponent } from "../vault/components/premium-badge.component"; -import { FolderAddEditComponent } from "../vault/individual-vault/folder-add-edit.component"; import { OrganizationBadgeModule } from "../vault/individual-vault/organization-badge/organization-badge.module"; import { PipesModule } from "../vault/individual-vault/pipes/pipes.module"; -import { PurgeVaultComponent } from "../vault/settings/purge-vault.component"; import { AccountFingerprintComponent } from "./components/account-fingerprint/account-fingerprint.component"; import { SharedModule } from "./shared.module"; @@ -68,6 +66,7 @@ import { SharedModule } from "./shared.module"; OrganizationLayoutComponent, VerifyRecoverDeleteOrgComponent, VaultTimeoutInputComponent, + PremiumBadgeComponent, ], declarations: [ AcceptFamilySponsorshipComponent, @@ -76,7 +75,6 @@ import { SharedModule } from "./shared.module"; EmergencyAccessConfirmComponent, EmergencyAccessTakeoverComponent, EmergencyAccessViewComponent, - FolderAddEditComponent, OrgEventsComponent, OrgExposedPasswordsReportComponent, OrgInactiveTwoFactorReportComponent, @@ -84,8 +82,6 @@ import { SharedModule } from "./shared.module"; OrgUnsecuredWebsitesReportComponent, OrgUserConfirmComponent, OrgWeakPasswordsReportComponent, - PremiumBadgeComponent, - PurgeVaultComponent, RecoverDeleteComponent, RecoverTwoFactorComponent, RemovePasswordComponent, @@ -106,7 +102,6 @@ import { SharedModule } from "./shared.module"; EmergencyAccessConfirmComponent, EmergencyAccessTakeoverComponent, EmergencyAccessViewComponent, - FolderAddEditComponent, OrganizationLayoutComponent, OrgEventsComponent, OrgExposedPasswordsReportComponent, @@ -116,7 +111,6 @@ import { SharedModule } from "./shared.module"; OrgUserConfirmComponent, OrgWeakPasswordsReportComponent, PremiumBadgeComponent, - PurgeVaultComponent, RecoverDeleteComponent, RecoverTwoFactorComponent, RemovePasswordComponent, diff --git a/apps/web/src/app/vault/components/premium-badge.component.ts b/apps/web/src/app/vault/components/premium-badge.component.ts index ec444404aea..4f1d1142f91 100644 --- a/apps/web/src/app/vault/components/premium-badge.component.ts +++ b/apps/web/src/app/vault/components/premium-badge.component.ts @@ -1,6 +1,8 @@ import { Component } from "@angular/core"; +import { JslibModule } from "@bitwarden/angular/jslib.module"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; +import { BadgeModule } from "@bitwarden/components"; @Component({ selector: "app-premium-badge", @@ -9,7 +11,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag {{ "premium" | i18n }} `, - standalone: false, + imports: [JslibModule, BadgeModule], }) export class PremiumBadgeComponent { constructor(private messagingService: MessagingService) {} diff --git a/apps/web/src/app/vault/components/vault-items/vault-items.component.ts b/apps/web/src/app/vault/components/vault-items/vault-items.component.ts index 3793db6f76a..18dfa73ac5a 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-items.component.ts +++ b/apps/web/src/app/vault/components/vault-items/vault-items.component.ts @@ -510,7 +510,7 @@ export class VaultItemsComponent { private compareNames(a: VaultItem, b: VaultItem): number { const getName = (item: VaultItem) => item.collection?.name || item.cipher?.name; - return getName(a).localeCompare(getName(b)); + return getName(a)?.localeCompare(getName(b)) ?? -1; } /** diff --git a/apps/web/src/app/vault/individual-vault/folder-add-edit.component.html b/apps/web/src/app/vault/individual-vault/folder-add-edit.component.html deleted file mode 100644 index 556672534ea..00000000000 --- a/apps/web/src/app/vault/individual-vault/folder-add-edit.component.html +++ /dev/null @@ -1,32 +0,0 @@ -
- - - {{ title }} - - - - {{ "name" | i18n }} - - - - - - -
- -
-
-
-
diff --git a/apps/web/src/app/vault/individual-vault/folder-add-edit.component.ts b/apps/web/src/app/vault/individual-vault/folder-add-edit.component.ts deleted file mode 100644 index 15c3e18544c..00000000000 --- a/apps/web/src/app/vault/individual-vault/folder-add-edit.component.ts +++ /dev/null @@ -1,139 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component, Inject } from "@angular/core"; -import { FormBuilder } from "@angular/forms"; -import { firstValueFrom } from "rxjs"; - -import { FolderAddEditComponent as BaseFolderAddEditComponent } from "@bitwarden/angular/vault/components/folder-add-edit.component"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction"; -import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; -import { UnionOfValues } from "@bitwarden/common/vault/types/union-of-values"; -import { - DIALOG_DATA, - DialogConfig, - DialogRef, - DialogService, - ToastService, -} from "@bitwarden/components"; -import { KeyService } from "@bitwarden/key-management"; - -@Component({ - selector: "app-folder-add-edit", - templateUrl: "folder-add-edit.component.html", - standalone: false, -}) -export class FolderAddEditComponent extends BaseFolderAddEditComponent { - protected override componentName = "app-folder-add-edit"; - constructor( - folderService: FolderService, - folderApiService: FolderApiServiceAbstraction, - protected accountSerivce: AccountService, - protected keyService: KeyService, - i18nService: I18nService, - platformUtilsService: PlatformUtilsService, - logService: LogService, - dialogService: DialogService, - formBuilder: FormBuilder, - protected toastService: ToastService, - protected dialogRef: DialogRef, - @Inject(DIALOG_DATA) params: FolderAddEditDialogParams, - ) { - super( - folderService, - folderApiService, - accountSerivce, - keyService, - i18nService, - platformUtilsService, - logService, - dialogService, - formBuilder, - toastService, - ); - // FIXME: Remove when updating file. Eslint update - // eslint-disable-next-line @typescript-eslint/no-unused-expressions - params?.folderId ? (this.folderId = params.folderId) : null; - } - - deleteAndClose = async () => { - const confirmed = await this.dialogService.openSimpleDialog({ - title: { key: "deleteFolder" }, - content: { key: "deleteFolderConfirmation" }, - type: "warning", - }); - - if (!confirmed) { - return; - } - - try { - await this.folderApiService.delete(this.folder.id, await firstValueFrom(this.activeUserId$)); - this.toastService.showToast({ - variant: "success", - title: null, - message: this.i18nService.t("deletedFolder"), - }); - } catch (e) { - this.logService.error(e); - } - - this.dialogRef.close(FolderAddEditDialogResult.Deleted); - }; - - submitAndClose = async () => { - this.folder.name = this.formGroup.controls.name.value; - if (this.folder.name == null || this.folder.name === "") { - this.formGroup.controls.name.markAsTouched(); - return; - } - - try { - const activeAccountId = await firstValueFrom(this.activeUserId$); - const userKey = await this.keyService.getUserKeyWithLegacySupport(activeAccountId); - const folder = await this.folderService.encrypt(this.folder, userKey); - this.formPromise = this.folderApiService.save(folder, activeAccountId); - await this.formPromise; - this.toastService.showToast({ - variant: "success", - title: null, - message: this.i18nService.t(this.editMode ? "editedFolder" : "addedFolder"), - }); - this.onSavedFolder.emit(this.folder); - this.dialogRef.close(FolderAddEditDialogResult.Saved); - } catch (e) { - this.logService.error(e); - } - return; - }; -} - -export interface FolderAddEditDialogParams { - folderId: string; -} - -export const FolderAddEditDialogResult = { - Deleted: "deleted", - Canceled: "canceled", - Saved: "saved", -} as const; - -export type FolderAddEditDialogResult = UnionOfValues; - -/** - * Strongly typed helper to open a FolderAddEdit dialog - * @param dialogService Instance of the dialog service that will be used to open the dialog - * @param config Optional configuration for the dialog - */ -export function openFolderAddEditDialog( - dialogService: DialogService, - config?: DialogConfig, -) { - return dialogService.open( - FolderAddEditComponent, - config, - ); -} diff --git a/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.html b/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.html index 4ef8204cdfc..711d1166d7b 100644 --- a/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.html +++ b/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.html @@ -68,35 +68,13 @@
-
- - - @for (item of cipherMenuItems$ | async; track item.type) { - - } - - - - -
+
diff --git a/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.ts b/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.ts index 49e159143dd..f4f3ba32428 100644 --- a/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.ts @@ -3,7 +3,7 @@ import { CommonModule } from "@angular/common"; import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from "@angular/core"; import { Router } from "@angular/router"; -import { firstValueFrom, map, shareReplay } from "rxjs"; +import { firstValueFrom } from "rxjs"; import { Unassigned, @@ -18,13 +18,13 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { CipherType } from "@bitwarden/common/vault/enums"; import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; -import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service"; import { BreadcrumbsModule, DialogService, MenuModule, SimpleDialogOptions, } from "@bitwarden/components"; +import { NewCipherMenuComponent } from "@bitwarden/vault"; import { CollectionDialogTabType } from "../../../admin-console/organizations/shared/components/collection-dialog"; import { HeaderModule } from "../../../layouts/header/header.module"; @@ -46,6 +46,7 @@ import { HeaderModule, PipesModule, JslibModule, + NewCipherMenuComponent, ], changeDetection: ChangeDetectionStrategy.OnPush, }) @@ -54,21 +55,6 @@ export class VaultHeaderComponent { protected All = All; protected CollectionDialogTabType = CollectionDialogTabType; protected CipherType = CipherType; - protected allCipherMenuItems = [ - { type: CipherType.Login, icon: "bwi-globe", labelKey: "typeLogin" }, - { type: CipherType.Card, icon: "bwi-credit-card", labelKey: "typeCard" }, - { type: CipherType.Identity, icon: "bwi-id-card", labelKey: "typeIdentity" }, - { type: CipherType.SecureNote, icon: "bwi-sticky-note", labelKey: "note" }, - { type: CipherType.SshKey, icon: "bwi-key", labelKey: "typeSshKey" }, - ]; - protected cipherMenuItems$ = this.restrictedItemTypesService.restricted$.pipe( - map((restrictedTypes) => { - return this.allCipherMenuItems.filter((item) => { - return !restrictedTypes.some((restrictedType) => restrictedType.cipherType === item.type); - }); - }), - shareReplay({ bufferSize: 1, refCount: true }), - ); /** * Boolean to determine the loading state of the header. @@ -109,7 +95,6 @@ export class VaultHeaderComponent { private dialogService: DialogService, private router: Router, private configService: ConfigService, - private restrictedItemTypesService: RestrictedItemTypesService, ) {} /** diff --git a/apps/web/src/app/vault/settings/purge-vault.component.ts b/apps/web/src/app/vault/settings/purge-vault.component.ts index 0a25122788c..4c58a27adb7 100644 --- a/apps/web/src/app/vault/settings/purge-vault.component.ts +++ b/apps/web/src/app/vault/settings/purge-vault.component.ts @@ -18,14 +18,16 @@ import { ToastService, } from "@bitwarden/components"; +import { UserVerificationModule } from "../../auth/shared/components/user-verification"; +import { SharedModule } from "../../shared"; + export interface PurgeVaultDialogData { organizationId: string; } @Component({ - selector: "app-purge-vault", templateUrl: "purge-vault.component.html", - standalone: false, + imports: [SharedModule, UserVerificationModule], }) export class PurgeVaultComponent { organizationId: string = null; diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index b272dc32e3b..a58126adac5 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -647,6 +647,9 @@ "typeSecureNote": { "message": "Secure note" }, + "typeNote": { + "message": "Note" + }, "typeSshKey": { "message": "SSH key" }, @@ -8928,7 +8931,7 @@ }, "uriMatchDefaultStrategyHint": { "message": "URI match detection is how Bitwarden identifies autofill suggestions.", - "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." + "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." }, "regExAdvancedOptionWarning": { "message": "\"Regular expression\" is an advanced option with increased risk of exposing credentials.", diff --git a/libs/angular/src/vault/services/custom-nudges-services/account-security-nudge.service.ts b/libs/angular/src/vault/services/custom-nudges-services/account-security-nudge.service.ts index a64a4aa975c..dc3f39793d3 100644 --- a/libs/angular/src/vault/services/custom-nudges-services/account-security-nudge.service.ts +++ b/libs/angular/src/vault/services/custom-nudges-services/account-security-nudge.service.ts @@ -74,7 +74,10 @@ export class AccountSecurityNudgeService extends DefaultSingleNudgeService { hasSpotlightDismissed: status.hasSpotlightDismissed || hideNudge, }; - if (isPinSet || biometricUnlockEnabled || hasOrgWithRemovePinPolicyOn) { + if ( + (isPinSet || biometricUnlockEnabled || hasOrgWithRemovePinPolicyOn) && + !status.hasSpotlightDismissed + ) { await this.setNudgeStatus(nudgeType, acctSecurityNudgeStatus, userId); } return acctSecurityNudgeStatus; diff --git a/libs/angular/src/vault/services/custom-nudges-services/has-items-nudge.service.ts b/libs/angular/src/vault/services/custom-nudges-services/has-items-nudge.service.ts index 61fb08ae8c1..d030b37dbd1 100644 --- a/libs/angular/src/vault/services/custom-nudges-services/has-items-nudge.service.ts +++ b/libs/angular/src/vault/services/custom-nudges-services/has-items-nudge.service.ts @@ -44,7 +44,11 @@ export class HasItemsNudgeService extends DefaultSingleNudgeService { return cipher.deletedDate == null; }); - if (profileOlderThanCutoff && filteredCiphers.length > 0) { + if ( + profileOlderThanCutoff && + filteredCiphers.length > 0 && + !nudgeStatus.hasSpotlightDismissed + ) { const dismissedStatus = { hasSpotlightDismissed: true, hasBadgeDismissed: true, diff --git a/libs/angular/src/vault/services/custom-nudges-services/new-item-nudge.service.ts b/libs/angular/src/vault/services/custom-nudges-services/new-item-nudge.service.ts index 2202e88111e..e89b877562a 100644 --- a/libs/angular/src/vault/services/custom-nudges-services/new-item-nudge.service.ts +++ b/libs/angular/src/vault/services/custom-nudges-services/new-item-nudge.service.ts @@ -49,7 +49,7 @@ export class NewItemNudgeService extends DefaultSingleNudgeService { const ciphersBoolean = ciphers.some((cipher) => cipher.type === currentType); - if (ciphersBoolean) { + if (ciphersBoolean && !nudgeStatus.hasSpotlightDismissed) { const dismissedStatus = { hasSpotlightDismissed: true, hasBadgeDismissed: true, diff --git a/libs/common/src/auth/enums/authentication-status.ts b/libs/common/src/auth/enums/authentication-status.ts index 6a6f9467ae7..fe50bd5f39b 100644 --- a/libs/common/src/auth/enums/authentication-status.ts +++ b/libs/common/src/auth/enums/authentication-status.ts @@ -1,7 +1,28 @@ +/** + * The authentication status of the user + * + * See `AuthService.authStatusFor$` for details on how we determine the user's `AuthenticationStatus` + */ // FIXME: update to use a const object instead of a typescript enum // eslint-disable-next-line @bitwarden/platform/no-enums export enum AuthenticationStatus { + /** + * User is not authenticated + * - The user does not have an active account userId and/or an access token in state + */ LoggedOut = 0, + + /** + * User is authenticated but not decrypted + * - The user has an access token, but no user key in state + * - Vault data cannot be decrypted (because there is no user key) + */ Locked = 1, + + /** + * User is authenticated and decrypted + * - The user has an access token and a user key in state + * - Vault data can be decrypted (via user key) + */ Unlocked = 2, } diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index cf9dfcaf1cd..c967f2614c8 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -419,11 +419,13 @@ export class CipherService implements CipherServiceAbstraction { userId: UserId, ): Promise<[CipherView[], CipherView[]] | null> { if (await this.configService.getFeatureFlag(FeatureFlag.PM19941MigrateCipherDomainToSdk)) { - const decryptStartTime = new Date().getTime(); + const decryptStartTime = performance.now(); const decrypted = await this.decryptCiphersWithSdk(ciphers, userId); - this.logService.info( - `[CipherService] Decrypting ${decrypted.length} ciphers took ${new Date().getTime() - decryptStartTime}ms`, - ); + + this.logService.measure(decryptStartTime, "Vault", "CipherService", "decrypt complete", [ + ["Items", ciphers.length], + ]); + // With SDK, failed ciphers are not returned return [decrypted, []]; } @@ -442,7 +444,7 @@ export class CipherService implements CipherServiceAbstraction { }, {} as Record, ); - const decryptStartTime = new Date().getTime(); + const decryptStartTime = performance.now(); const allCipherViews = ( await Promise.all( Object.entries(grouped).map(async ([orgId, groupedCiphers]) => { @@ -462,9 +464,11 @@ export class CipherService implements CipherServiceAbstraction { ) .flat() .sort(this.getLocaleSortingFunction()); - this.logService.info( - `[CipherService] Decrypting ${allCipherViews.length} ciphers took ${new Date().getTime() - decryptStartTime}ms`, - ); + + this.logService.measure(decryptStartTime, "Vault", "CipherService", "decrypt complete", [ + ["Items", ciphers.length], + ]); + // Split ciphers into two arrays, one for successfully decrypted ciphers and one for ciphers that failed to decrypt return allCipherViews.reduce( (acc, c) => { diff --git a/libs/common/src/vault/services/folder/folder.service.ts b/libs/common/src/vault/services/folder/folder.service.ts index 12d02958049..2d440adeb29 100644 --- a/libs/common/src/vault/services/folder/folder.service.ts +++ b/libs/common/src/vault/services/folder/folder.service.ts @@ -1,6 +1,16 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { Observable, Subject, firstValueFrom, map, shareReplay, switchMap, merge } from "rxjs"; +import { + Observable, + Subject, + firstValueFrom, + map, + shareReplay, + switchMap, + merge, + filter, + combineLatest, +} from "rxjs"; // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. // eslint-disable-next-line no-restricted-imports @@ -69,8 +79,12 @@ export class FolderService implements InternalFolderServiceAbstraction { const observable = merge( this.forceFolderViews[userId], - this.encryptedFoldersState(userId).state$.pipe( - switchMap((folderData) => { + combineLatest([ + this.encryptedFoldersState(userId).state$, + this.keyService.userKey$(userId), + ]).pipe( + filter(([folderData, userKey]) => folderData != null && userKey != null), + switchMap(([folderData, _]) => { return this.decryptFolders(userId, folderData); }), ), diff --git a/libs/common/src/vault/services/search.service.ts b/libs/common/src/vault/services/search.service.ts index 4b7a26b6a31..8e54fa695bd 100644 --- a/libs/common/src/vault/services/search.service.ts +++ b/libs/common/src/vault/services/search.service.ts @@ -129,12 +129,15 @@ export class SearchService implements SearchServiceAbstraction { } async isSearchable(userId: UserId, query: string): Promise { + const time = performance.now(); query = SearchService.normalizeSearchQuery(query); const index = await this.getIndexForSearch(userId); const notSearchable = query == null || (index == null && query.length < this.searchableMinLength) || (index != null && query.length < this.searchableMinLength && query.indexOf(">") !== 0); + + this.logService.measure(time, "Vault", "SearchService", "isSearchable"); return !notSearchable; } @@ -147,7 +150,7 @@ export class SearchService implements SearchServiceAbstraction { return; } - const indexingStartTime = new Date().getTime(); + const indexingStartTime = performance.now(); await this.setIsIndexing(userId, true); await this.setIndexedEntityIdForSearch(userId, indexedEntityId as IndexedEntityId); const builder = new lunr.Builder(); @@ -188,11 +191,10 @@ export class SearchService implements SearchServiceAbstraction { await this.setIndexForSearch(userId, index.toJSON() as SerializedLunrIndex); await this.setIsIndexing(userId, false); - this.logService.info( - `[SearchService] Building search index of ${ciphers.length} ciphers took ${ - new Date().getTime() - indexingStartTime - }ms`, - ); + + this.logService.measure(indexingStartTime, "Vault", "SearchService", "index complete", [ + ["Items", ciphers.length], + ]); } async searchCiphers( diff --git a/libs/key-management-ui/src/lock/components/lock.component.ts b/libs/key-management-ui/src/lock/components/lock.component.ts index 3d5951a5ac4..9f370c88fa9 100644 --- a/libs/key-management-ui/src/lock/components/lock.component.ts +++ b/libs/key-management-ui/src/lock/components/lock.component.ts @@ -577,6 +577,9 @@ export class LockComponent implements OnInit, OnDestroy { throw new Error("No active user."); } + // Add a mark to indicate that the user has unlocked their vault. A good starting point for measuring unlock performance. + this.logService.mark("Vault unlocked"); + await this.keyService.setUserKey(key, this.activeAccount.id); // Now that we have a decrypted user key in memory, we can check if we diff --git a/libs/logging/src/console-log.service.ts b/libs/logging/src/console-log.service.ts index 3a4ffe9ead1..29246368cf7 100644 --- a/libs/logging/src/console-log.service.ts +++ b/libs/logging/src/console-log.service.ts @@ -54,4 +54,43 @@ export class ConsoleLogService implements LogService { break; } } + + measure( + start: DOMHighResTimeStamp, + trackGroup: string, + track: string, + name?: string, + properties?: [string, any][], + ): PerformanceMeasure { + const measureName = `[${track}]: ${name}`; + + const measure = performance.measure(measureName, { + start: start, + detail: { + devtools: { + dataType: "track-entry", + track, + trackGroup, + properties, + }, + }, + }); + + this.info(`${measureName} took ${measure.duration}`, properties); + return measure; + } + + mark(name: string): PerformanceMark { + const mark = performance.mark(name, { + detail: { + devtools: { + dataType: "marker", + }, + }, + }); + + this.info(mark.name, new Date().toISOString()); + + return mark; + } } diff --git a/libs/logging/src/log.service.ts b/libs/logging/src/log.service.ts index a63ad47c07e..ce391723bf9 100644 --- a/libs/logging/src/log.service.ts +++ b/libs/logging/src/log.service.ts @@ -6,4 +6,28 @@ export abstract class LogService { abstract warning(message?: any, ...optionalParams: any[]): void; abstract error(message?: any, ...optionalParams: any[]): void; abstract write(level: LogLevel, message?: any, ...optionalParams: any[]): void; + + /** + * Helper wrapper around `performance.measure` to log a measurement. Should also debug-log the data. + * + * @param start Start time of the measurement. + * @param trackGroup A track-group for the measurement, should generally be the team owning the domain. + * @param track A track for the measurement, should generally be the class name. + * @param measureName A descriptive name for the measurement. + * @param properties Additional properties to include. + */ + abstract measure( + start: DOMHighResTimeStamp, + trackGroup: string, + track: string, + measureName: string, + properties?: [string, any][], + ): PerformanceMeasure; + + /** + * Helper wrapper around `performance.mark` to log a mark. Should also debug-log the data. + * + * @param name Name of the mark to create. + */ + abstract mark(name: string): PerformanceMark; } diff --git a/libs/vault/src/components/new-cipher-menu/new-cipher-menu.component.html b/libs/vault/src/components/new-cipher-menu/new-cipher-menu.component.html new file mode 100644 index 00000000000..38b5875a605 --- /dev/null +++ b/libs/vault/src/components/new-cipher-menu/new-cipher-menu.component.html @@ -0,0 +1,37 @@ + +
+ + + @for (item of cipherMenuItems$ | async; track item.type) { + + } + + + + +
+
diff --git a/libs/vault/src/components/new-cipher-menu/new-cipher-menu.component.ts b/libs/vault/src/components/new-cipher-menu/new-cipher-menu.component.ts new file mode 100644 index 00000000000..eb3194bc5f0 --- /dev/null +++ b/libs/vault/src/components/new-cipher-menu/new-cipher-menu.component.ts @@ -0,0 +1,38 @@ +import { CommonModule } from "@angular/common"; +import { Component, input, output } from "@angular/core"; +import { map, shareReplay } from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { CipherType } from "@bitwarden/common/vault/enums"; +import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service"; +import { CIPHER_MENU_ITEMS } from "@bitwarden/common/vault/types/cipher-menu-items"; +import { ButtonModule, MenuModule } from "@bitwarden/components"; +import { I18nPipe } from "@bitwarden/ui-common"; + +@Component({ + selector: "vault-new-cipher-menu", + templateUrl: "new-cipher-menu.component.html", + imports: [ButtonModule, CommonModule, MenuModule, I18nPipe, JslibModule], +}) +export class NewCipherMenuComponent { + canCreateCipher = input(false); + canCreateFolder = input(false); + canCreateCollection = input(false); + folderAdded = output(); + collectionAdded = output(); + cipherAdded = output(); + + constructor(private restrictedItemTypesService: RestrictedItemTypesService) {} + + /** + * Returns an observable that emits the cipher menu items, filtered by the restricted types. + */ + cipherMenuItems$ = this.restrictedItemTypesService.restricted$.pipe( + map((restrictedTypes) => { + return CIPHER_MENU_ITEMS.filter((item) => { + return !restrictedTypes.some((restrictedType) => restrictedType.cipherType === item.type); + }); + }), + shareReplay({ bufferSize: 1, refCount: true }), + ); +} diff --git a/libs/vault/src/index.ts b/libs/vault/src/index.ts index b39bb85ab30..9d037d8fe5e 100644 --- a/libs/vault/src/index.ts +++ b/libs/vault/src/index.ts @@ -19,6 +19,7 @@ export { DecryptionFailureDialogComponent } from "./components/decryption-failur export { openPasswordHistoryDialog } from "./components/password-history/password-history.component"; export * from "./components/add-edit-folder-dialog/add-edit-folder-dialog.component"; export * from "./components/carousel"; +export * from "./components/new-cipher-menu/new-cipher-menu.component"; export * as VaultIcons from "./icons";