1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-23 16:13:21 +00:00

Merge branch 'main' into beeep/dev-container

This commit is contained in:
Conner Turnbull
2026-01-26 10:39:41 -05:00
15 changed files with 244 additions and 148 deletions

View File

@@ -813,6 +813,7 @@ export class VaultComponent implements OnInit, OnDestroy, CopyClickListener {
};
return filterFn(proxyCipher as any);
}
return filterFn(cipher);
};
}

View File

@@ -642,77 +642,80 @@ export class VaultV2Component<C extends CipherViewLike>
});
}
switch (cipher.type) {
case CipherType.Login:
if (
cipher.login.canLaunch ||
cipher.login.username != null ||
cipher.login.password != null
) {
menu.push({ type: "separator" });
}
if (cipher.login.canLaunch) {
menu.push({
label: this.i18nService.t("launch"),
click: () => this.platformUtilsService.launchUri(cipher.login.launchUri),
});
}
if (cipher.login.username != null) {
menu.push({
label: this.i18nService.t("copyUsername"),
click: () => this.copyValue(cipher, cipher.login.username, "username", "Username"),
});
}
if (cipher.login.password != null && cipher.viewPassword) {
menu.push({
label: this.i18nService.t("copyPassword"),
click: () => {
this.copyValue(cipher, cipher.login.password, "password", "Password");
this.eventCollectionService
.collect(EventType.Cipher_ClientCopiedPassword, cipher.id)
.catch(() => {});
},
});
}
if (cipher.login.hasTotp && (cipher.organizationUseTotp || this.userHasPremiumAccess)) {
menu.push({
label: this.i18nService.t("copyVerificationCodeTotp"),
click: async () => {
const value = await firstValueFrom(
this.totpService.getCode$(cipher.login.totp),
).catch((): any => null);
if (value) {
this.copyValue(cipher, value.code, "verificationCodeTotp", "TOTP");
}
},
});
}
break;
case CipherType.Card:
if (cipher.card.number != null || cipher.card.code != null) {
menu.push({ type: "separator" });
}
if (cipher.card.number != null) {
menu.push({
label: this.i18nService.t("copyNumber"),
click: () => this.copyValue(cipher, cipher.card.number, "number", "Card Number"),
});
}
if (cipher.card.code != null) {
menu.push({
label: this.i18nService.t("copySecurityCode"),
click: () => {
this.copyValue(cipher, cipher.card.code, "securityCode", "Security Code");
this.eventCollectionService
.collect(EventType.Cipher_ClientCopiedCardCode, cipher.id)
.catch(() => {});
},
});
}
break;
default:
break;
if (!cipher.isDeleted) {
switch (cipher.type) {
case CipherType.Login:
if (
cipher.login.canLaunch ||
cipher.login.username != null ||
cipher.login.password != null
) {
menu.push({ type: "separator" });
}
if (cipher.login.canLaunch) {
menu.push({
label: this.i18nService.t("launch"),
click: () => this.platformUtilsService.launchUri(cipher.login.launchUri),
});
}
if (cipher.login.username != null) {
menu.push({
label: this.i18nService.t("copyUsername"),
click: () => this.copyValue(cipher, cipher.login.username, "username", "Username"),
});
}
if (cipher.login.password != null && cipher.viewPassword) {
menu.push({
label: this.i18nService.t("copyPassword"),
click: () => {
this.copyValue(cipher, cipher.login.password, "password", "Password");
this.eventCollectionService
.collect(EventType.Cipher_ClientCopiedPassword, cipher.id)
.catch(() => {});
},
});
}
if (cipher.login.hasTotp && (cipher.organizationUseTotp || this.userHasPremiumAccess)) {
menu.push({
label: this.i18nService.t("copyVerificationCodeTotp"),
click: async () => {
const value = await firstValueFrom(
this.totpService.getCode$(cipher.login.totp),
).catch((): any => null);
if (value) {
this.copyValue(cipher, value.code, "verificationCodeTotp", "TOTP");
}
},
});
}
break;
case CipherType.Card:
if (cipher.card.number != null || cipher.card.code != null) {
menu.push({ type: "separator" });
}
if (cipher.card.number != null) {
menu.push({
label: this.i18nService.t("copyNumber"),
click: () => this.copyValue(cipher, cipher.card.number, "number", "Card Number"),
});
}
if (cipher.card.code != null) {
menu.push({
label: this.i18nService.t("copySecurityCode"),
click: () => {
this.copyValue(cipher, cipher.card.code, "securityCode", "Security Code");
this.eventCollectionService
.collect(EventType.Cipher_ClientCopiedCardCode, cipher.id)
.catch(() => {});
},
});
}
break;
default:
break;
}
}
invokeMenu(menu);
}

View File

@@ -46,8 +46,11 @@ export abstract class CipherReportComponent implements OnDestroy {
organizations: Organization[] = [];
organizations$: Observable<Organization[]>;
readonly maxItemsToSwitchToChipSelect = 5;
filterStatus: any = [0];
showFilterToggle: boolean = false;
selectedFilterChip: string = "0";
chipSelectOptions: { label: string; value: string }[] = [];
vaultMsg: string = "vault";
currentFilterStatus: number | string = 0;
protected filterOrgStatus$ = new BehaviorSubject<number | string>(0);
@@ -288,6 +291,15 @@ export abstract class CipherReportComponent implements OnDestroy {
return await this.cipherService.getAllDecrypted(activeUserId);
}
protected canDisplayToggleGroup(): boolean {
return this.filterStatus.length <= this.maxItemsToSwitchToChipSelect;
}
async filterOrgToggleChipSelect(filterId: string | null) {
const selectedFilterId = filterId ?? 0;
await this.filterOrgToggle(selectedFilterId);
}
protected filterCiphersByOrg(ciphersList: CipherView[]) {
this.allCiphers = [...ciphersList];
@@ -309,5 +321,22 @@ export abstract class CipherReportComponent implements OnDestroy {
this.showFilterToggle = false;
this.vaultMsg = "vault";
}
this.chipSelectOptions = this.setupChipSelectOptions(this.filterStatus);
}
private setupChipSelectOptions(filters: string[]) {
const options = filters.map((filterId: string, index: number) => {
const name = this.getName(filterId);
const count = this.getCount(filterId);
const labelSuffix = count != null ? ` (${count})` : "";
return {
label: name + labelSuffix,
value: filterId,
};
});
return options;
}
}

View File

@@ -13,19 +13,32 @@
<bit-callout type="danger" title="{{ 'exposedPasswordsFound' | i18n }}" [useAlertRole]="true">
{{ "exposedPasswordsFoundReportDesc" | i18n: (ciphers.length | number) : vaultMsg }}
</bit-callout>
<bit-toggle-group
*ngIf="showFilterToggle && !isAdminConsoleActive"
[selected]="filterOrgStatus$ | async"
(selectedChange)="filterOrgToggle($event)"
[attr.aria-label]="'addAccessFilter' | i18n"
>
<ng-container *ngFor="let status of filterStatus">
<bit-toggle [value]="status">
{{ getName(status) }}
<span bitBadge variant="info"> {{ getCount(status) }} </span>
</bit-toggle>
</ng-container>
</bit-toggle-group>
@if (showFilterToggle && !isAdminConsoleActive) {
@if (canDisplayToggleGroup()) {
<bit-toggle-group
[selected]="filterOrgStatus$ | async"
(selectedChange)="filterOrgToggle($event)"
[attr.aria-label]="'addAccessFilter' | i18n"
>
<ng-container *ngFor="let status of filterStatus">
<bit-toggle [value]="status">
{{ getName(status) }}
<span bitBadge variant="info"> {{ getCount(status) }} </span>
</bit-toggle>
</ng-container>
</bit-toggle-group>
} @else {
<bit-chip-select
[placeholderText]="chipSelectOptions[0].label"
[options]="chipSelectOptions"
[ngModel]="selectedFilterChip"
(ngModelChange)="filterOrgToggleChipSelect($event)"
fullWidth="true"
></bit-chip-select>
}
}
<bit-table-scroll [dataSource]="dataSource" [rowSize]="75">
<ng-container header>
<th bitCell></th>

View File

@@ -18,19 +18,32 @@
<bit-callout type="danger" title="{{ 'inactive2faFound' | i18n }}">
{{ "inactive2faFoundReportDesc" | i18n: (ciphers.length | number) : vaultMsg }}
</bit-callout>
<bit-toggle-group
*ngIf="showFilterToggle && !isAdminConsoleActive"
[selected]="filterOrgStatus$ | async"
(selectedChange)="filterOrgToggle($event)"
[attr.aria-label]="'addAccessFilter' | i18n"
>
<ng-container *ngFor="let status of filterStatus">
<bit-toggle [value]="status">
{{ getName(status) }}
<span bitBadge variant="info"> {{ getCount(status) }} </span>
</bit-toggle>
</ng-container>
</bit-toggle-group>
@if (showFilterToggle && !isAdminConsoleActive) {
@if (canDisplayToggleGroup()) {
<bit-toggle-group
[selected]="filterOrgStatus$ | async"
(selectedChange)="filterOrgToggle($event)"
[attr.aria-label]="'addAccessFilter' | i18n"
>
<ng-container *ngFor="let status of filterStatus">
<bit-toggle [value]="status">
{{ getName(status) }}
<span bitBadge variant="info"> {{ getCount(status) }} </span>
</bit-toggle>
</ng-container>
</bit-toggle-group>
} @else {
<bit-chip-select
[placeholderText]="chipSelectOptions[0].label"
[options]="chipSelectOptions"
[ngModel]="selectedFilterChip"
(ngModelChange)="filterOrgToggleChipSelect($event)"
fullWidth="true"
></bit-chip-select>
}
}
<bit-table-scroll [dataSource]="dataSource" [rowSize]="75">
<ng-container header *ngIf="!isAdminConsoleActive">
<th bitCell></th>

View File

@@ -16,7 +16,7 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { DialogService } from "@bitwarden/components";
import { ChipSelectComponent, DialogService } from "@bitwarden/components";
import {
PasswordRepromptService,
CipherFormConfigService,
@@ -45,7 +45,7 @@ import { ExposedPasswordsReportComponent as BaseExposedPasswordsReportComponent
RoutedVaultFilterService,
RoutedVaultFilterBridgeService,
],
imports: [SharedModule, HeaderModule, OrganizationBadgeModule, PipesModule],
imports: [SharedModule, HeaderModule, OrganizationBadgeModule, PipesModule, ChipSelectComponent],
})
export class ExposedPasswordsReportComponent
extends BaseExposedPasswordsReportComponent

View File

@@ -11,7 +11,7 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { DialogService } from "@bitwarden/components";
import { ChipSelectComponent, DialogService } from "@bitwarden/components";
import {
CipherFormConfigService,
PasswordRepromptService,
@@ -39,7 +39,7 @@ import { InactiveTwoFactorReportComponent as BaseInactiveTwoFactorReportComponen
RoutedVaultFilterService,
RoutedVaultFilterBridgeService,
],
imports: [SharedModule, HeaderModule, OrganizationBadgeModule, PipesModule],
imports: [SharedModule, HeaderModule, OrganizationBadgeModule, PipesModule, ChipSelectComponent],
})
export class InactiveTwoFactorReportComponent
extends BaseInactiveTwoFactorReportComponent

View File

@@ -15,7 +15,7 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { DialogService } from "@bitwarden/components";
import { ChipSelectComponent, DialogService } from "@bitwarden/components";
import {
CipherFormConfigService,
PasswordRepromptService,
@@ -44,7 +44,7 @@ import { ReusedPasswordsReportComponent as BaseReusedPasswordsReportComponent }
RoutedVaultFilterService,
RoutedVaultFilterBridgeService,
],
imports: [SharedModule, HeaderModule, OrganizationBadgeModule, PipesModule],
imports: [SharedModule, HeaderModule, OrganizationBadgeModule, PipesModule, ChipSelectComponent],
})
export class ReusedPasswordsReportComponent
extends BaseReusedPasswordsReportComponent

View File

@@ -15,7 +15,7 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { DialogService } from "@bitwarden/components";
import { ChipSelectComponent, DialogService } from "@bitwarden/components";
import {
CipherFormConfigService,
PasswordRepromptService,
@@ -44,7 +44,7 @@ import { UnsecuredWebsitesReportComponent as BaseUnsecuredWebsitesReportComponen
RoutedVaultFilterService,
RoutedVaultFilterBridgeService,
],
imports: [SharedModule, HeaderModule, OrganizationBadgeModule, PipesModule],
imports: [SharedModule, HeaderModule, OrganizationBadgeModule, PipesModule, ChipSelectComponent],
})
export class UnsecuredWebsitesReportComponent
extends BaseUnsecuredWebsitesReportComponent

View File

@@ -16,7 +16,7 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { DialogService } from "@bitwarden/components";
import { ChipSelectComponent, DialogService } from "@bitwarden/components";
import {
CipherFormConfigService,
PasswordRepromptService,
@@ -45,7 +45,7 @@ import { WeakPasswordsReportComponent as BaseWeakPasswordsReportComponent } from
RoutedVaultFilterService,
RoutedVaultFilterBridgeService,
],
imports: [SharedModule, HeaderModule, OrganizationBadgeModule, PipesModule],
imports: [SharedModule, HeaderModule, OrganizationBadgeModule, PipesModule, ChipSelectComponent],
})
export class WeakPasswordsReportComponent
extends BaseWeakPasswordsReportComponent

View File

@@ -1,6 +1,6 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, OnInit } from "@angular/core";
import { ChangeDetectionStrategy, Component, OnInit } from "@angular/core";
import { firstValueFrom } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
@@ -9,9 +9,8 @@ import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abs
import { reports, ReportType } from "../reports";
import { ReportEntry, ReportVariant } from "../shared";
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: "app-reports-home",
templateUrl: "reports-home.component.html",
standalone: false,

View File

@@ -19,19 +19,30 @@
{{ "reusedPasswordsFoundReportDesc" | i18n: (ciphers.length | number) : vaultMsg }}
</bit-callout>
<bit-toggle-group
*ngIf="showFilterToggle && !isAdminConsoleActive"
[selected]="filterOrgStatus$ | async"
(selectedChange)="filterOrgToggle($event)"
[attr.aria-label]="'addAccessFilter' | i18n"
>
<ng-container *ngFor="let status of filterStatus">
<bit-toggle [value]="status">
{{ getName(status) }}
<span bitBadge variant="info"> {{ getCount(status) }} </span>
</bit-toggle>
</ng-container>
</bit-toggle-group>
@if (showFilterToggle && !isAdminConsoleActive) {
@if (canDisplayToggleGroup()) {
<bit-toggle-group
[selected]="filterOrgStatus$ | async"
(selectedChange)="filterOrgToggle($event)"
[attr.aria-label]="'addAccessFilter' | i18n"
>
<ng-container *ngFor="let status of filterStatus">
<bit-toggle [value]="status">
{{ getName(status) }}
<span bitBadge variant="info"> {{ getCount(status) }} </span>
</bit-toggle>
</ng-container>
</bit-toggle-group>
} @else {
<bit-chip-select
[placeholderText]="chipSelectOptions[0].label"
[options]="chipSelectOptions"
[ngModel]="selectedFilterChip"
(ngModelChange)="filterOrgToggleChipSelect($event)"
fullWidth="true"
></bit-chip-select>
}
}
<bit-table-scroll [dataSource]="dataSource" [rowSize]="75">
<ng-container header *ngIf="!isAdminConsoleActive">

View File

@@ -19,19 +19,31 @@
{{ "unsecuredWebsitesFoundReportDesc" | i18n: (ciphers.length | number) : vaultMsg }}
</bit-callout>
<bit-toggle-group
*ngIf="showFilterToggle && !isAdminConsoleActive"
[selected]="filterOrgStatus$ | async"
(selectedChange)="filterOrgToggle($event)"
[attr.aria-label]="'addAccessFilter' | i18n"
>
<ng-container *ngFor="let status of filterStatus">
<bit-toggle [value]="status">
{{ getName(status) }}
<span bitBadge variant="info"> {{ getCount(status) }} </span>
</bit-toggle>
</ng-container>
</bit-toggle-group>
@if (showFilterToggle && !isAdminConsoleActive) {
@if (canDisplayToggleGroup()) {
<bit-toggle-group
[selected]="filterOrgStatus$ | async"
(selectedChange)="filterOrgToggle($event)"
[attr.aria-label]="'addAccessFilter' | i18n"
>
<ng-container *ngFor="let status of filterStatus">
<bit-toggle [value]="status">
{{ getName(status) }}
<span bitBadge variant="info"> {{ getCount(status) }} </span>
</bit-toggle>
</ng-container>
</bit-toggle-group>
} @else {
<bit-chip-select
[placeholderText]="chipSelectOptions[0].label"
[options]="chipSelectOptions"
[ngModel]="selectedFilterChip"
(ngModelChange)="filterOrgToggleChipSelect($event)"
fullWidth="true"
></bit-chip-select>
}
}
<bit-table-scroll [dataSource]="dataSource" [rowSize]="75">
<ng-container header *ngIf="!isAdminConsoleActive">
<th bitCell></th>

View File

@@ -18,19 +18,32 @@
<bit-callout type="danger" title="{{ 'weakPasswordsFound' | i18n }}">
{{ "weakPasswordsFoundReportDesc" | i18n: (ciphers.length | number) : vaultMsg }}
</bit-callout>
<bit-toggle-group
*ngIf="showFilterToggle && !isAdminConsoleActive"
[selected]="filterOrgStatus$ | async"
(selectedChange)="filterOrgToggle($event)"
[attr.aria-label]="'addAccessFilter' | i18n"
>
<ng-container *ngFor="let status of filterStatus">
<bit-toggle [value]="status">
{{ getName(status) }}
<span bitBadge variant="info"> {{ getCount(status) }} </span>
</bit-toggle>
</ng-container>
</bit-toggle-group>
@if (showFilterToggle && !isAdminConsoleActive) {
@if (canDisplayToggleGroup()) {
<bit-toggle-group
[selected]="filterOrgStatus$ | async"
(selectedChange)="filterOrgToggle($event)"
[attr.aria-label]="'addAccessFilter' | i18n"
>
<ng-container *ngFor="let status of filterStatus">
<bit-toggle [value]="status">
{{ getName(status) }}
<span bitBadge variant="info"> {{ getCount(status) }} </span>
</bit-toggle>
</ng-container>
</bit-toggle-group>
} @else {
<bit-chip-select
[placeholderText]="chipSelectOptions[0].label"
[options]="chipSelectOptions"
[ngModel]="selectedFilterChip"
(ngModelChange)="filterOrgToggleChipSelect($event)"
fullWidth="true"
></bit-chip-select>
}
}
<bit-table-scroll [dataSource]="dataSource" [rowSize]="75">
<ng-container header>
<th bitCell></th>

View File

@@ -1,6 +1,7 @@
import { CommonModule } from "@angular/common";
import { NgModule } from "@angular/core";
import { ChipSelectComponent } from "@bitwarden/components";
import {
CipherFormConfigService,
DefaultCipherFormConfigService,
@@ -34,6 +35,7 @@ import { ReportsSharedModule } from "./shared";
OrganizationBadgeModule,
PipesModule,
HeaderModule,
ChipSelectComponent,
],
declarations: [
BreachReportComponent,