mirror of
https://github.com/bitwarden/browser
synced 2025-12-18 17:23:37 +00:00
PM-15070 Star critical apps (#12109)
Ability to star a record when flagged as critical. This is still behind a feature flag
This commit is contained in:
@@ -1,14 +1,19 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { safeProvider } from "@bitwarden/angular/platform/utils/safe-provider";
|
||||
import { CriticalAppsService } from "@bitwarden/bit-common/tools/reports/risk-insights";
|
||||
import {
|
||||
CriticalAppsApiService,
|
||||
MemberCipherDetailsApiService,
|
||||
RiskInsightsDataService,
|
||||
RiskInsightsReportService,
|
||||
} from "@bitwarden/bit-common/tools/reports/risk-insights/services";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength/password-strength.service.abstraction";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { AccessIntelligenceRoutingModule } from "./access-intelligence-routing.module";
|
||||
import { RiskInsightsComponent } from "./risk-insights.component";
|
||||
@@ -33,6 +38,16 @@ import { RiskInsightsComponent } from "./risk-insights.component";
|
||||
provide: RiskInsightsDataService,
|
||||
deps: [RiskInsightsReportService],
|
||||
},
|
||||
safeProvider({
|
||||
provide: CriticalAppsService,
|
||||
useClass: CriticalAppsService,
|
||||
deps: [KeyService, EncryptService, CriticalAppsApiService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: CriticalAppsApiService,
|
||||
useClass: CriticalAppsApiService,
|
||||
deps: [ApiService],
|
||||
}),
|
||||
],
|
||||
})
|
||||
export class AccessIntelligenceModule {}
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
buttonType="secondary"
|
||||
bitButton
|
||||
*ngIf="isCriticalAppsFeatureEnabled"
|
||||
[disabled]="!selectedIds.size"
|
||||
[disabled]="!selectedUrls.size"
|
||||
[loading]="markingAsCritical"
|
||||
(click)="markAppsAsCritical()"
|
||||
>
|
||||
@@ -80,9 +80,11 @@
|
||||
<input
|
||||
bitCheckbox
|
||||
type="checkbox"
|
||||
[checked]="selectedIds.has(r.id)"
|
||||
(change)="onCheckboxChange(r.id, $event)"
|
||||
*ngIf="!r.isMarkedAsCritical"
|
||||
[checked]="selectedUrls.has(r.applicationName)"
|
||||
(change)="onCheckboxChange(r.applicationName, $event)"
|
||||
/>
|
||||
<i class="bwi bwi-star-f" *ngIf="r.isMarkedAsCritical"></i>
|
||||
</td>
|
||||
<td class="tw-cursor-pointer" (click)="showAppAtRiskMembers(r.applicationName)" bitCell>
|
||||
<span>{{ r.applicationName }}</span>
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import { Component, DestroyRef, OnDestroy, OnInit, inject } from "@angular/core";
|
||||
import { Component, DestroyRef, inject, OnInit } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { FormControl } from "@angular/forms";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { debounceTime, map, Observable, of, Subscription } from "rxjs";
|
||||
import { combineLatest, debounceTime, map, Observable, of, skipWhile } from "rxjs";
|
||||
|
||||
import {
|
||||
CriticalAppsService,
|
||||
RiskInsightsDataService,
|
||||
RiskInsightsReportService,
|
||||
} from "@bitwarden/bit-common/tools/reports/risk-insights";
|
||||
import {
|
||||
ApplicationHealthReportDetail,
|
||||
ApplicationHealthReportDetailWithCriticalFlag,
|
||||
ApplicationHealthReportSummary,
|
||||
} from "@bitwarden/bit-common/tools/reports/risk-insights/models/password-health";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
@@ -50,16 +52,15 @@ import { ApplicationsLoadingComponent } from "./risk-insights-loading.component"
|
||||
SharedModule,
|
||||
],
|
||||
})
|
||||
export class AllApplicationsComponent implements OnInit, OnDestroy {
|
||||
protected dataSource = new TableDataSource<ApplicationHealthReportDetail>();
|
||||
protected selectedIds: Set<number> = new Set<number>();
|
||||
export class AllApplicationsComponent implements OnInit {
|
||||
protected dataSource = new TableDataSource<ApplicationHealthReportDetailWithCriticalFlag>();
|
||||
protected selectedUrls: Set<string> = new Set<string>();
|
||||
protected searchControl = new FormControl("", { nonNullable: true });
|
||||
protected loading = true;
|
||||
protected organization = {} as Organization;
|
||||
noItemsIcon = Icons.Security;
|
||||
protected markingAsCritical = false;
|
||||
protected applicationSummary = {} as ApplicationHealthReportSummary;
|
||||
private subscription = new Subscription();
|
||||
|
||||
destroyRef = inject(DestroyRef);
|
||||
isLoading$: Observable<boolean> = of(false);
|
||||
@@ -70,28 +71,33 @@ export class AllApplicationsComponent implements OnInit, OnDestroy {
|
||||
FeatureFlag.CriticalApps,
|
||||
);
|
||||
|
||||
const organizationId = this.activatedRoute.snapshot.paramMap.get("organizationId");
|
||||
const organizationId = this.activatedRoute.snapshot.paramMap.get("organizationId") ?? "";
|
||||
combineLatest([
|
||||
this.dataService.applications$,
|
||||
this.criticalAppsService.getAppsListForOrg(organizationId),
|
||||
this.organizationService.get$(organizationId),
|
||||
])
|
||||
.pipe(
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
skipWhile(([_, __, organization]) => !organization),
|
||||
map(([applications, criticalApps, organization]) => {
|
||||
const criticalUrls = criticalApps.map((ca) => ca.uri);
|
||||
const data = applications?.map((app) => ({
|
||||
...app,
|
||||
isMarkedAsCritical: criticalUrls.includes(app.applicationName),
|
||||
})) as ApplicationHealthReportDetailWithCriticalFlag[];
|
||||
return { data, organization };
|
||||
}),
|
||||
)
|
||||
.subscribe(({ data, organization }) => {
|
||||
this.dataSource.data = data ?? [];
|
||||
this.applicationSummary = this.reportService.generateApplicationsSummary(data ?? []);
|
||||
if (organization) {
|
||||
this.organization = organization;
|
||||
}
|
||||
});
|
||||
|
||||
if (organizationId) {
|
||||
this.organization = await this.organizationService.get(organizationId);
|
||||
this.subscription = this.dataService.applications$
|
||||
.pipe(
|
||||
map((applications) => {
|
||||
if (applications) {
|
||||
this.dataSource.data = applications;
|
||||
this.applicationSummary =
|
||||
this.reportService.generateApplicationsSummary(applications);
|
||||
}
|
||||
}),
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
)
|
||||
.subscribe();
|
||||
this.isLoading$ = this.dataService.isLoading$;
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.subscription?.unsubscribe();
|
||||
this.isLoading$ = this.dataService.isLoading$;
|
||||
}
|
||||
|
||||
constructor(
|
||||
@@ -103,6 +109,7 @@ export class AllApplicationsComponent implements OnInit, OnDestroy {
|
||||
protected dataService: RiskInsightsDataService,
|
||||
protected organizationService: OrganizationService,
|
||||
protected reportService: RiskInsightsReportService,
|
||||
protected criticalAppsService: CriticalAppsService,
|
||||
protected dialogService: DialogService,
|
||||
) {
|
||||
this.searchControl.valueChanges
|
||||
@@ -119,21 +126,28 @@ export class AllApplicationsComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
};
|
||||
|
||||
isMarkedAsCriticalItem(applicationName: string) {
|
||||
return this.selectedUrls.has(applicationName);
|
||||
}
|
||||
|
||||
markAppsAsCritical = async () => {
|
||||
// TODO: Send to API once implemented
|
||||
this.markingAsCritical = true;
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
this.selectedIds.clear();
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: "",
|
||||
message: this.i18nService.t("appsMarkedAsCritical"),
|
||||
});
|
||||
resolve(true);
|
||||
this.markingAsCritical = false;
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
try {
|
||||
await this.criticalAppsService.setCriticalApps(
|
||||
this.organization.id,
|
||||
Array.from(this.selectedUrls),
|
||||
);
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: "",
|
||||
message: this.i18nService.t("appsMarkedAsCritical"),
|
||||
});
|
||||
} finally {
|
||||
this.selectedUrls.clear();
|
||||
this.markingAsCritical = false;
|
||||
}
|
||||
};
|
||||
|
||||
trackByFunction(_: number, item: ApplicationHealthReportDetail) {
|
||||
@@ -161,12 +175,14 @@ export class AllApplicationsComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
};
|
||||
|
||||
onCheckboxChange(id: number, event: Event) {
|
||||
onCheckboxChange(applicationName: string, event: Event) {
|
||||
const isChecked = (event.target as HTMLInputElement).checked;
|
||||
if (isChecked) {
|
||||
this.selectedIds.add(id);
|
||||
this.selectedUrls.add(applicationName);
|
||||
} else {
|
||||
this.selectedIds.delete(id);
|
||||
this.selectedUrls.delete(applicationName);
|
||||
}
|
||||
}
|
||||
|
||||
getSelectedUrls = () => Array.from(this.selectedUrls);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ export const applicationTableMockData = [
|
||||
totalPasswords: 10,
|
||||
atRiskMembers: 2,
|
||||
totalMembers: 5,
|
||||
isMarkedAsCritical: false,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
@@ -14,6 +15,7 @@ export const applicationTableMockData = [
|
||||
totalPasswords: 8,
|
||||
atRiskMembers: 1,
|
||||
totalMembers: 3,
|
||||
isMarkedAsCritical: false,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
@@ -22,6 +24,7 @@ export const applicationTableMockData = [
|
||||
totalPasswords: 6,
|
||||
atRiskMembers: 0,
|
||||
totalMembers: 2,
|
||||
isMarkedAsCritical: false,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
@@ -30,6 +33,7 @@ export const applicationTableMockData = [
|
||||
totalPasswords: 4,
|
||||
atRiskMembers: 0,
|
||||
totalMembers: 1,
|
||||
isMarkedAsCritical: false,
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
@@ -38,6 +42,7 @@ export const applicationTableMockData = [
|
||||
totalPasswords: 2,
|
||||
atRiskMembers: 0,
|
||||
totalMembers: 0,
|
||||
isMarkedAsCritical: false,
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
@@ -46,5 +51,6 @@ export const applicationTableMockData = [
|
||||
totalPasswords: 1,
|
||||
atRiskMembers: 0,
|
||||
totalMembers: 0,
|
||||
isMarkedAsCritical: false,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
<bit-tab *ngIf="isCriticalAppsFeatureEnabled">
|
||||
<ng-template bitTabLabel>
|
||||
<i class="bwi bwi-star"></i>
|
||||
{{ "criticalApplicationsWithCount" | i18n: criticalAppsCount }}
|
||||
{{ "criticalApplicationsWithCount" | i18n: (criticalApps$ | async)?.length ?? 0 }}
|
||||
</ng-template>
|
||||
<tools-critical-applications></tools-critical-applications>
|
||||
</bit-tab>
|
||||
|
||||
@@ -6,11 +6,17 @@ import { Observable, EMPTY } from "rxjs";
|
||||
import { map, switchMap } from "rxjs/operators";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { RiskInsightsDataService } from "@bitwarden/bit-common/tools/reports/risk-insights";
|
||||
import {
|
||||
RiskInsightsDataService,
|
||||
CriticalAppsService,
|
||||
PasswordHealthReportApplicationsResponse,
|
||||
} from "@bitwarden/bit-common/tools/reports/risk-insights";
|
||||
import { ApplicationHealthReportDetail } from "@bitwarden/bit-common/tools/reports/risk-insights/models/password-health";
|
||||
// eslint-disable-next-line no-restricted-imports -- used for dependency injection
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { devFlagEnabled } from "@bitwarden/common/platform/misc/flags";
|
||||
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { AsyncActionsModule, ButtonModule, TabsModule } from "@bitwarden/components";
|
||||
import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.module";
|
||||
|
||||
@@ -51,6 +57,7 @@ export class RiskInsightsComponent implements OnInit {
|
||||
dataLastUpdated: Date = new Date();
|
||||
|
||||
isCriticalAppsFeatureEnabled: boolean = false;
|
||||
criticalApps$: Observable<PasswordHealthReportApplicationsResponse[]> = new Observable();
|
||||
showDebugTabs: boolean = false;
|
||||
|
||||
appsCount: number = 0;
|
||||
@@ -69,10 +76,13 @@ export class RiskInsightsComponent implements OnInit {
|
||||
private router: Router,
|
||||
private configService: ConfigService,
|
||||
private dataService: RiskInsightsDataService,
|
||||
private criticalAppsService: CriticalAppsService,
|
||||
) {
|
||||
this.route.queryParams.pipe(takeUntilDestroyed()).subscribe(({ tabIndex }) => {
|
||||
this.tabIndex = !isNaN(Number(tabIndex)) ? Number(tabIndex) : RiskInsightsTabType.AllApps;
|
||||
});
|
||||
const orgId = this.route.snapshot.paramMap.get("organizationId") ?? "";
|
||||
this.criticalApps$ = this.criticalAppsService.getAppsListForOrg(orgId);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
@@ -104,6 +114,7 @@ export class RiskInsightsComponent implements OnInit {
|
||||
if (applications) {
|
||||
this.appsCount = applications.length;
|
||||
}
|
||||
this.criticalAppsService.setOrganizationId(this.organizationId as OrganizationId);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user