1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-17 09:59:41 +00:00

PM-30539 created new component and added a filter (#18630)

This commit is contained in:
Vijay Oommen
2026-01-28 15:19:39 -06:00
committed by GitHub
parent 1dfd68bf57
commit 9d8f1af62b
6 changed files with 387 additions and 0 deletions

View File

@@ -14,6 +14,24 @@
"noCriticalAppsAtRisk": {
"message": "No critical applications at risk"
},
"critical":{
"message": "Critical ($COUNT$)",
"placeholders": {
"count": {
"content": "$1",
"example": "3"
}
}
},
"notCritical": {
"message": "Not critical ($COUNT$)",
"placeholders": {
"count": {
"content": "$1",
"example": "5"
}
}
},
"accessIntelligence": {
"message": "Access Intelligence"
},
@@ -250,6 +268,9 @@
"application": {
"message": "Application"
},
"applications": {
"message": "Applications"
},
"atRiskPasswords": {
"message": "At-risk passwords"
},

View File

@@ -0,0 +1,128 @@
@if ((dataService.reportStatus$ | async) == ReportStatusEnum.Loading) {
<dirt-report-loading></dirt-report-loading>
} @else {
@let drawerDetails = dataService.drawerDetails$ | async;
<div class="tw-mt-4 tw-flex tw-flex-col">
<h2 class="tw-mb-6" bitTypography="h2">{{ "allApplications" | i18n }}</h2>
<div class="tw-flex tw-gap-6">
<div
role="region"
[attr.aria-label]="'atRiskMembers' | i18n"
class="tw-flex-1 tw-box-border tw-bg-background tw-block tw-text-main tw-border-solid tw-border tw-border-secondary-300 tw-rounded-lg tw-p-4"
[ngClass]="{
'tw-bg-primary-100': drawerDetails.invokerId === 'allAppsOrgAtRiskMembers',
}"
>
<div class="tw-flex tw-flex-col tw-gap-1">
<span bitTypography="h6" class="tw-flex tw-text-main" id="allAppsOrgAtRiskMembersLabel">{{
"atRiskMembers" | i18n
}}</span>
<div class="tw-flex tw-items-baseline tw-gap-2" role="status" aria-live="polite">
<span
bitTypography="h3"
class="!tw-mb-0"
aria-describedby="allAppsOrgAtRiskMembersLabel"
>{{ applicationSummary().totalAtRiskMemberCount }}</span
>
<span bitTypography="body2">{{
"cardMetrics" | i18n: applicationSummary().totalMemberCount
}}</span>
</div>
<div class="tw-flex tw-items-baseline tw-mt-1 tw-gap-2">
<p bitTypography="body1" class="tw-mb-0">
<button
type="button"
bitLink
[attr.aria-label]="('viewAtRiskMembers' | i18n) + ': ' + ('atRiskMembers' | i18n)"
(click)="dataService.setDrawerForOrgAtRiskMembers('allAppsOrgAtRiskMembers')"
>
{{ "viewAtRiskMembers" | i18n }}
</button>
</p>
</div>
</div>
</div>
<div
role="region"
[attr.aria-label]="'atRiskApplications' | i18n"
class="tw-flex-1 tw-box-border tw-bg-background tw-block tw-text-main tw-border-solid tw-border tw-border-secondary-300 tw-rounded-lg tw-p-4"
[ngClass]="{
'tw-bg-primary-100': drawerDetails.invokerId === 'allAppsOrgAtRiskApplications',
}"
>
<div class="tw-flex tw-flex-col tw-gap-1">
<span
bitTypography="h6"
class="tw-flex tw-text-main"
id="allAppsOrgAtRiskApplicationsLabel"
>{{ "atRiskApplications" | i18n }}</span
>
<div class="tw-flex tw-items-baseline tw-gap-2" role="status" aria-live="polite">
<span
bitTypography="h3"
class="!tw-mb-0"
aria-describedby="allAppsOrgAtRiskApplicationsLabel"
>{{ applicationSummary().totalAtRiskApplicationCount }}</span
>
<span bitTypography="body2">{{
"cardMetrics" | i18n: applicationSummary().totalApplicationCount
}}</span>
</div>
<div class="tw-flex tw-items-baseline tw-mt-1 tw-gap-2">
<p bitTypography="body1" class="tw-mb-0">
<button
type="button"
bitLink
[attr.aria-label]="
('viewAtRiskApplications' | i18n) + ': ' + ('atRiskApplications' | i18n)
"
(click)="dataService.setDrawerForOrgAtRiskApps('allAppsOrgAtRiskApplications')"
>
{{ "viewAtRiskApplications" | i18n }}
</button>
</p>
</div>
</div>
</div>
</div>
<div class="tw-flex tw-mt-8 tw-mb-4 tw-gap-4 tw-items-center">
<bit-search
[placeholder]="'searchApps' | i18n"
class="tw-w-1/2"
[formControl]="searchControl"
></bit-search>
<bit-chip-select
[placeholderText]="'filter' | i18n"
placeholderIcon="bwi-sliders"
[options]="filterOptions()"
[ngModel]="selectedFilter()"
(ngModelChange)="setFilterApplicationsByStatus($event)"
fullWidth="false"
></bit-chip-select>
<button
type="button"
[buttonType]="'primary'"
bitButton
class="tw-ml-auto"
[disabled]="!selectedUrls().size"
[loading]="markingAsCritical()"
(click)="markAppsAsCritical()"
>
<i class="bwi tw-mr-2" [ngClass]="selectedUrls().size ? 'bwi-star-f' : 'bwi-star'"></i>
{{ "markAppAsCritical" | i18n }}
</button>
</div>
<app-table-row-scrollable
[dataSource]="dataSource"
[showRowCheckBox]="true"
[showRowMenuForCriticalApps]="false"
[selectedUrls]="selectedUrls()"
[openApplication]="drawerDetails.invokerId || ''"
[checkboxChange]="onCheckboxChange"
[showAppAtRiskMembers]="showAppAtRiskMembers"
></app-table-row-scrollable>
</div>
}

View File

@@ -0,0 +1,221 @@
import {
Component,
DestroyRef,
inject,
OnInit,
ChangeDetectionStrategy,
signal,
computed,
} from "@angular/core";
import { takeUntilDestroyed, toObservable } from "@angular/core/rxjs-interop";
import { FormControl, ReactiveFormsModule } from "@angular/forms";
import { ActivatedRoute } from "@angular/router";
import { combineLatest, debounceTime, startWith } from "rxjs";
import { Security } from "@bitwarden/assets/svg";
import { RiskInsightsDataService } from "@bitwarden/bit-common/dirt/reports/risk-insights";
import { createNewSummaryData } from "@bitwarden/bit-common/dirt/reports/risk-insights/helpers";
import {
OrganizationReportSummary,
ReportStatus,
} from "@bitwarden/bit-common/dirt/reports/risk-insights/models/report-models";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import {
ButtonModule,
IconButtonModule,
LinkModule,
NoItemsModule,
SearchModule,
TableDataSource,
ToastService,
TypographyModule,
ChipSelectComponent,
} from "@bitwarden/components";
import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.module";
import { SharedModule } from "@bitwarden/web-vault/app/shared";
import { PipesModule } from "@bitwarden/web-vault/app/vault/individual-vault/pipes/pipes.module";
import {
ApplicationTableDataSource,
AppTableRowScrollableComponent,
} from "../shared/app-table-row-scrollable.component";
import { ReportLoadingComponent } from "../shared/report-loading.component";
export const ApplicationFilterOption = {
All: "all",
Critical: "critical",
NonCritical: "nonCritical",
} as const;
export type ApplicationFilterOption =
(typeof ApplicationFilterOption)[keyof typeof ApplicationFilterOption];
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: "dirt-applications",
templateUrl: "./applications.component.html",
imports: [
ReportLoadingComponent,
HeaderModule,
LinkModule,
SearchModule,
PipesModule,
NoItemsModule,
SharedModule,
AppTableRowScrollableComponent,
IconButtonModule,
TypographyModule,
ButtonModule,
ReactiveFormsModule,
ChipSelectComponent,
],
})
export class ApplicationsComponent implements OnInit {
destroyRef = inject(DestroyRef);
protected ReportStatusEnum = ReportStatus;
protected noItemsIcon = Security;
// Standard properties
protected readonly dataSource = new TableDataSource<ApplicationTableDataSource>();
protected readonly searchControl = new FormControl<string>("", { nonNullable: true });
// Template driven properties
protected readonly selectedUrls = signal(new Set<string>());
protected readonly markingAsCritical = signal(false);
protected readonly applicationSummary = signal<OrganizationReportSummary>(createNewSummaryData());
protected readonly criticalApplicationsCount = signal(0);
protected readonly totalApplicationsCount = signal(0);
protected readonly nonCriticalApplicationsCount = computed(() => {
return this.totalApplicationsCount() - this.criticalApplicationsCount();
});
// filter related properties
protected readonly selectedFilter = signal<ApplicationFilterOption>(ApplicationFilterOption.All);
protected selectedFilterObservable = toObservable(this.selectedFilter);
protected readonly ApplicationFilterOption = ApplicationFilterOption;
protected readonly filterOptions = computed(() => [
{
label: this.i18nService.t("critical", this.criticalApplicationsCount()),
value: ApplicationFilterOption.Critical,
},
{
label: this.i18nService.t("notCritical", this.nonCriticalApplicationsCount()),
value: ApplicationFilterOption.NonCritical,
},
]);
constructor(
protected i18nService: I18nService,
protected activatedRoute: ActivatedRoute,
protected toastService: ToastService,
protected dataService: RiskInsightsDataService,
) {}
async ngOnInit() {
this.dataService.enrichedReportData$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
next: (report) => {
if (report != null) {
this.applicationSummary.set(report.summaryData);
// Map the report data to include the iconCipher for each application
const tableDataWithIcon = report.reportData.map((app) => ({
...app,
iconCipher:
app.cipherIds.length > 0
? this.dataService.getCipherIcon(app.cipherIds[0])
: undefined,
}));
this.dataSource.data = tableDataWithIcon;
this.totalApplicationsCount.set(report.reportData.length);
} else {
this.dataSource.data = [];
}
},
error: () => {
this.dataSource.data = [];
},
});
this.dataService.criticalReportResults$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
next: (criticalReport) => {
if (criticalReport != null) {
this.criticalApplicationsCount.set(criticalReport.reportData.length);
} else {
this.criticalApplicationsCount.set(0);
}
},
});
combineLatest([
this.searchControl.valueChanges.pipe(startWith("")),
this.selectedFilterObservable,
])
.pipe(debounceTime(200), takeUntilDestroyed(this.destroyRef))
.subscribe(([searchText, selectedFilter]) => {
let filterFunction = (app: ApplicationTableDataSource) => true;
if (selectedFilter === ApplicationFilterOption.Critical) {
filterFunction = (app) => app.isMarkedAsCritical;
} else if (selectedFilter === ApplicationFilterOption.NonCritical) {
filterFunction = (app) => !app.isMarkedAsCritical;
}
this.dataSource.filter = (app) =>
filterFunction(app) &&
app.applicationName.toLowerCase().includes(searchText.toLowerCase());
});
}
setFilterApplicationsByStatus(value: ApplicationFilterOption) {
this.selectedFilter.set(value);
}
isMarkedAsCriticalItem(applicationName: string) {
return this.selectedUrls().has(applicationName);
}
markAppsAsCritical = async () => {
this.markingAsCritical.set(true);
const count = this.selectedUrls().size;
this.dataService
.saveCriticalApplications(Array.from(this.selectedUrls()))
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe({
next: () => {
this.toastService.showToast({
variant: "success",
title: "",
message: this.i18nService.t("criticalApplicationsMarkedSuccess", count.toString()),
});
this.selectedUrls.set(new Set<string>());
this.markingAsCritical.set(false);
},
error: () => {
this.toastService.showToast({
variant: "error",
title: "",
message: this.i18nService.t("applicationsMarkedAsCriticalFail"),
});
},
});
};
showAppAtRiskMembers = async (applicationName: string) => {
await this.dataService.setDrawerForAppAtRiskMembers(applicationName);
};
onCheckboxChange = (applicationName: string, event: Event) => {
const isChecked = (event.target as HTMLInputElement).checked;
this.selectedUrls.update((selectedUrls) => {
const nextSelected = new Set(selectedUrls);
if (isChecked) {
nextSelected.add(applicationName);
} else {
nextSelected.delete(applicationName);
}
return nextSelected;
});
};
}

View File

@@ -81,6 +81,11 @@
<bit-tab label="{{ 'activity' | i18n }}">
<dirt-all-activity [organizationId]="this.organizationId"></dirt-all-activity>
</bit-tab>
@if (milestone11Enabled) {
<bit-tab label="{{ 'applications' | i18n }}">
<dirt-applications></dirt-applications>
</bit-tab>
}
<bit-tab label="{{ 'allApplicationsWithCount' | i18n: appsCount }}">
<dirt-all-applications></dirt-all-applications>
</bit-tab>

View File

@@ -21,6 +21,8 @@ import {
ReportStatus,
RiskInsightsDataService,
} from "@bitwarden/bit-common/dirt/reports/risk-insights";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@@ -38,6 +40,7 @@ import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.mod
import { AllActivityComponent } from "./activity/all-activity.component";
import { AllApplicationsComponent } from "./all-applications/all-applications.component";
import { ApplicationsComponent } from "./all-applications/applications.component";
import { CriticalApplicationsComponent } from "./critical-applications/critical-applications.component";
import { EmptyStateCardComponent } from "./empty-state-card.component";
import { RiskInsightsTabType } from "./models/risk-insights.models";
@@ -53,6 +56,7 @@ type ProgressStep = ReportProgress | null;
templateUrl: "./risk-insights.component.html",
imports: [
AllApplicationsComponent,
ApplicationsComponent,
AsyncActionsModule,
ButtonModule,
CommonModule,
@@ -77,6 +81,7 @@ type ProgressStep = ReportProgress | null;
export class RiskInsightsComponent implements OnInit, OnDestroy {
private destroyRef = inject(DestroyRef);
protected ReportStatusEnum = ReportStatus;
protected milestone11Enabled: boolean = false;
tabIndex: RiskInsightsTabType = RiskInsightsTabType.AllActivity;
@@ -114,6 +119,7 @@ export class RiskInsightsComponent implements OnInit, OnDestroy {
protected dialogService: DialogService,
private fileDownloadService: FileDownloadService,
private logService: LogService,
private configService: ConfigService,
) {
this.route.queryParams.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(({ tabIndex }) => {
this.tabIndex = !isNaN(Number(tabIndex)) ? Number(tabIndex) : RiskInsightsTabType.AllActivity;
@@ -121,6 +127,10 @@ export class RiskInsightsComponent implements OnInit, OnDestroy {
}
async ngOnInit() {
this.milestone11Enabled = await this.configService.getFeatureFlag(
FeatureFlag.Milestone11AppPageImprovements,
);
this.route.paramMap
.pipe(
takeUntilDestroyed(this.destroyRef),

View File

@@ -59,6 +59,7 @@ export enum FeatureFlag {
EventManagementForDataDogAndCrowdStrike = "event-management-for-datadog-and-crowdstrike",
EventManagementForHuntress = "event-management-for-huntress",
PhishingDetection = "phishing-detection",
Milestone11AppPageImprovements = "pm-30538-dirt-milestone-11-app-page-improvements",
/* Vault */
PM19941MigrateCipherDomainToSdk = "pm-19941-migrate-cipher-domain-to-sdk",
@@ -121,6 +122,7 @@ export const DefaultFeatureFlagValue = {
[FeatureFlag.EventManagementForDataDogAndCrowdStrike]: FALSE,
[FeatureFlag.EventManagementForHuntress]: FALSE,
[FeatureFlag.PhishingDetection]: FALSE,
[FeatureFlag.Milestone11AppPageImprovements]: FALSE,
/* Vault */
[FeatureFlag.CipherKeyEncryption]: FALSE,