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