mirror of
https://github.com/bitwarden/browser
synced 2026-02-04 10:43:47 +00:00
PM-23375 Convert risk insights drawer to dialog
This commit is contained in:
@@ -59,94 +59,4 @@
|
||||
<dirt-critical-applications></dirt-critical-applications>
|
||||
</bit-tab>
|
||||
</bit-tab-group>
|
||||
|
||||
@if (dataService.drawerDetails$ | async; as drawerDetails) {
|
||||
<bit-drawer style="width: 30%" [(open)]="isDrawerOpen" (openChange)="dataService.closeDrawer()">
|
||||
<ng-container *ngIf="dataService.isActiveDrawerType(drawerTypes.OrgAtRiskMembers)">
|
||||
<bit-drawer-header
|
||||
title="{{ 'atRiskMembersWithCount' | i18n: drawerDetails.atRiskMemberDetails.length }}"
|
||||
>
|
||||
</bit-drawer-header>
|
||||
<bit-drawer-body>
|
||||
<span bitTypography="body1" class="tw-text-muted tw-text-sm">{{
|
||||
(drawerDetails.atRiskMemberDetails.length > 0
|
||||
? "atRiskMembersDescription"
|
||||
: "atRiskMembersDescriptionNone"
|
||||
) | i18n
|
||||
}}</span>
|
||||
<ng-container *ngIf="drawerDetails.atRiskMemberDetails.length > 0">
|
||||
<div class="tw-flex tw-justify-between tw-mt-2 tw-text-muted">
|
||||
<div bitTypography="body2" class="tw-text-sm tw-font-bold">
|
||||
{{ "email" | i18n }}
|
||||
</div>
|
||||
<div bitTypography="body2" class="tw-text-sm tw-font-bold">
|
||||
{{ "atRiskPasswords" | i18n }}
|
||||
</div>
|
||||
</div>
|
||||
<ng-container *ngFor="let member of drawerDetails.atRiskMemberDetails">
|
||||
<div class="tw-flex tw-justify-between tw-mt-2">
|
||||
<div>{{ member.email }}</div>
|
||||
<div>{{ member.atRiskPasswordCount }}</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</bit-drawer-body>
|
||||
</ng-container>
|
||||
|
||||
@if (dataService.isActiveDrawerType(drawerTypes.AppAtRiskMembers)) {
|
||||
<bit-drawer-header title="{{ drawerDetails.appAtRiskMembers.applicationName }}">
|
||||
</bit-drawer-header>
|
||||
<bit-drawer-body>
|
||||
<div bitTypography="body1" class="tw-mb-2">
|
||||
{{ "atRiskMembersWithCount" | i18n: drawerDetails.appAtRiskMembers.members.length }}
|
||||
</div>
|
||||
<div bitTypography="body1" class="tw-text-muted tw-text-sm tw-mb-2">
|
||||
{{
|
||||
(drawerDetails.appAtRiskMembers.members.length > 0
|
||||
? "atRiskMembersDescriptionWithApp"
|
||||
: "atRiskMembersDescriptionWithAppNone"
|
||||
) | i18n: drawerDetails.appAtRiskMembers.applicationName
|
||||
}}
|
||||
</div>
|
||||
<div class="tw-mt-1">
|
||||
<ng-container *ngFor="let member of drawerDetails.appAtRiskMembers.members">
|
||||
<div>{{ member.email }}</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</bit-drawer-body>
|
||||
}
|
||||
|
||||
@if (dataService.isActiveDrawerType(drawerTypes.OrgAtRiskApps)) {
|
||||
<bit-drawer-header
|
||||
title="{{ 'atRiskApplicationsWithCount' | i18n: drawerDetails.atRiskAppDetails.length }}"
|
||||
>
|
||||
</bit-drawer-header>
|
||||
|
||||
<bit-drawer-body>
|
||||
<span bitTypography="body2" class="tw-text-muted tw-text-sm">{{
|
||||
(drawerDetails.atRiskAppDetails.length > 0
|
||||
? "atRiskApplicationsDescription"
|
||||
: "atRiskApplicationsDescriptionNone"
|
||||
) | i18n
|
||||
}}</span>
|
||||
<ng-container *ngIf="drawerDetails.atRiskAppDetails.length > 0">
|
||||
<div class="tw-flex tw-justify-between tw-mt-2 tw-text-muted">
|
||||
<div bitTypography="body2" class="tw-text-sm tw-font-bold">
|
||||
{{ "application" | i18n }}
|
||||
</div>
|
||||
<div bitTypography="body2" class="tw-text-sm tw-font-bold">
|
||||
{{ "atRiskPasswords" | i18n }}
|
||||
</div>
|
||||
</div>
|
||||
<ng-container *ngFor="let app of drawerDetails.atRiskAppDetails">
|
||||
<div class="tw-flex tw-justify-between tw-mt-2">
|
||||
<div>{{ app.applicationName }}</div>
|
||||
<div>{{ app.atRiskPasswordCount }}</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</bit-drawer-body>
|
||||
}
|
||||
</bit-drawer>
|
||||
}
|
||||
</ng-container>
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, DestroyRef, OnDestroy, OnInit, inject } from "@angular/core";
|
||||
import {
|
||||
Component,
|
||||
DestroyRef,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
inject,
|
||||
ChangeDetectionStrategy,
|
||||
} from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { EMPTY } from "rxjs";
|
||||
@@ -13,24 +20,17 @@ import {
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import {
|
||||
AsyncActionsModule,
|
||||
ButtonModule,
|
||||
DrawerBodyComponent,
|
||||
DrawerComponent,
|
||||
DrawerHeaderComponent,
|
||||
TabsModule,
|
||||
} from "@bitwarden/components";
|
||||
import { AsyncActionsModule, ButtonModule, DialogService, TabsModule } from "@bitwarden/components";
|
||||
import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.module";
|
||||
|
||||
import { AllActivityComponent } from "./activity/all-activity.component";
|
||||
import { AllApplicationsComponent } from "./all-applications/all-applications.component";
|
||||
import { CriticalApplicationsComponent } from "./critical-applications/critical-applications.component";
|
||||
import { RiskInsightsTabType } from "./models/risk-insights.models";
|
||||
import { RiskInsightsDrawerDialogComponent } from "./shared/risk-insights-drawer-dialog.component";
|
||||
|
||||
// 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,
|
||||
templateUrl: "./risk-insights.component.html",
|
||||
imports: [
|
||||
AllApplicationsComponent,
|
||||
@@ -41,9 +41,6 @@ import { RiskInsightsTabType } from "./models/risk-insights.models";
|
||||
JslibModule,
|
||||
HeaderModule,
|
||||
TabsModule,
|
||||
DrawerComponent,
|
||||
DrawerBodyComponent,
|
||||
DrawerHeaderComponent,
|
||||
AllActivityComponent,
|
||||
],
|
||||
})
|
||||
@@ -67,6 +64,7 @@ export class RiskInsightsComponent implements OnInit, OnDestroy {
|
||||
private router: Router,
|
||||
private configService: ConfigService,
|
||||
protected dataService: RiskInsightsDataService,
|
||||
protected dialogService: DialogService,
|
||||
) {
|
||||
this.route.queryParams.pipe(takeUntilDestroyed()).subscribe(({ tabIndex }) => {
|
||||
this.tabIndex = !isNaN(Number(tabIndex)) ? Number(tabIndex) : RiskInsightsTabType.AllApps;
|
||||
@@ -79,6 +77,16 @@ export class RiskInsightsComponent implements OnInit, OnDestroy {
|
||||
this.isRiskInsightsActivityTabFeatureEnabled = isEnabled;
|
||||
this.tabIndex = 0; // default to first tab
|
||||
});
|
||||
|
||||
this.dataService.drawerDetails$
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe((details) => {
|
||||
if (details.activeDrawerType !== DrawerType.None) {
|
||||
this.dialogService.openDrawer(RiskInsightsDrawerDialogComponent, {
|
||||
data: { ...details },
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
@if (isActiveDrawerType(drawerTypes.OrgAtRiskMembers)) {
|
||||
<bit-dialog dialogSize="large" disablePadding="false">
|
||||
<ng-container bitDialogTitle>
|
||||
<span>{{ "atRiskMembersWithCount" | i18n: drawerDetails.atRiskMemberDetails.length }}</span>
|
||||
</ng-container>
|
||||
<ng-container bitDialogContent>
|
||||
<span bitTypography="body1" class="tw-text-muted tw-text-sm">{{
|
||||
(drawerDetails.atRiskMemberDetails.length > 0
|
||||
? "atRiskMembersDescription"
|
||||
: "atRiskMembersDescriptionNone"
|
||||
) | i18n
|
||||
}}</span>
|
||||
|
||||
<ng-container *ngIf="drawerDetails.atRiskMemberDetails.length > 0">
|
||||
<div class="tw-flex tw-justify-between tw-mt-2 tw-text-muted">
|
||||
<div bitTypography="body2" class="tw-text-sm tw-font-bold">
|
||||
{{ "email" | i18n }}
|
||||
</div>
|
||||
<div bitTypography="body2" class="tw-text-sm tw-font-bold">
|
||||
{{ "atRiskPasswords" | i18n }}
|
||||
</div>
|
||||
</div>
|
||||
<ng-container *ngFor="let member of drawerDetails.atRiskMemberDetails">
|
||||
<div class="tw-flex tw-justify-between tw-mt-2">
|
||||
<div>{{ member.email }}</div>
|
||||
<div>{{ member.atRiskPasswordCount }}</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
<ng-container bitDialogFooter> </ng-container>
|
||||
</bit-dialog>
|
||||
}
|
||||
|
||||
@if (isActiveDrawerType(drawerTypes.AppAtRiskMembers)) {
|
||||
<bit-dialog dialogSize="large" disablePadding="false">
|
||||
<ng-container bitDialogTitle>
|
||||
<span>{{ drawerDetails.appAtRiskMembers.applicationName }}</span>
|
||||
</ng-container>
|
||||
<ng-container bitDialogContent>
|
||||
<div bitTypography="body1" class="tw-mb-2">
|
||||
{{ "atRiskMembersWithCount" | i18n: drawerDetails.appAtRiskMembers.members.length }}
|
||||
</div>
|
||||
<div bitTypography="body1" class="tw-text-muted tw-text-sm tw-mb-2">
|
||||
{{
|
||||
(drawerDetails.appAtRiskMembers.members.length > 0
|
||||
? "atRiskMembersDescriptionWithApp"
|
||||
: "atRiskMembersDescriptionWithAppNone"
|
||||
) | i18n: drawerDetails.appAtRiskMembers.applicationName
|
||||
}}
|
||||
</div>
|
||||
<div class="tw-mt-1">
|
||||
<ng-container *ngFor="let member of drawerDetails.appAtRiskMembers.members">
|
||||
<div>{{ member.email }}</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
}
|
||||
|
||||
@if (isActiveDrawerType(drawerTypes.OrgAtRiskApps)) {
|
||||
<bit-dialog dialogSize="large" disablePadding="false">
|
||||
<ng-container bitDialogTitle>
|
||||
<span>{{ "atRiskApplicationsWithCount" | i18n: drawerDetails.atRiskAppDetails.length }}</span>
|
||||
</ng-container>
|
||||
<ng-container bitDialogContent>
|
||||
<span bitTypography="body1" class="tw-text-muted tw-text-sm">{{
|
||||
(drawerDetails.atRiskAppDetails.length > 0
|
||||
? "atRiskApplicationsDescription"
|
||||
: "atRiskApplicationsDescriptionNone"
|
||||
) | i18n
|
||||
}}</span>
|
||||
<ng-container *ngFor="let app of drawerDetails.atRiskAppDetails">
|
||||
<div class="tw-flex tw-justify-between tw-mt-2">
|
||||
<div>{{ app.applicationName }}</div>
|
||||
<div>{{ app.atRiskPasswordCount }}</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import { DrawerDetails, DrawerType } from "@bitwarden/bit-common/dirt/reports/risk-insights";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { DIALOG_DATA } from "@bitwarden/components";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
|
||||
import { RiskInsightsDrawerDialogComponent } from "./risk-insights-drawer-dialog.component";
|
||||
|
||||
beforeAll(() => {
|
||||
// Mock element.animate for jsdom
|
||||
// the animate function is not available in jsdom, so we provide a mock implementation
|
||||
// This is necessary for tests that rely on animations
|
||||
// This mock does not perform any actual animations, it just provides a structure that allows tests
|
||||
// to run without throwing errors related to missing animate function
|
||||
if (!HTMLElement.prototype.animate) {
|
||||
HTMLElement.prototype.animate = function () {
|
||||
return {
|
||||
play: () => {},
|
||||
pause: () => {},
|
||||
finish: () => {},
|
||||
cancel: () => {},
|
||||
reverse: () => {},
|
||||
addEventListener: () => {},
|
||||
removeEventListener: () => {},
|
||||
dispatchEvent: () => false,
|
||||
onfinish: null,
|
||||
oncancel: null,
|
||||
startTime: 0,
|
||||
currentTime: 0,
|
||||
playbackRate: 1,
|
||||
playState: "idle",
|
||||
replaceState: "active",
|
||||
effect: null,
|
||||
finished: Promise.resolve(),
|
||||
id: "",
|
||||
remove: () => {},
|
||||
timeline: null,
|
||||
ready: Promise.resolve(),
|
||||
} as unknown as Animation;
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
describe("DrawerDialogComponent", () => {
|
||||
let component: RiskInsightsDrawerDialogComponent;
|
||||
let fixture: ComponentFixture<RiskInsightsDrawerDialogComponent>;
|
||||
const mockI18nService = mock<I18nService>();
|
||||
const drawerDetails: DrawerDetails = {
|
||||
open: true,
|
||||
invokerId: "test-invoker",
|
||||
activeDrawerType: DrawerType.None,
|
||||
atRiskMemberDetails: [],
|
||||
appAtRiskMembers: null,
|
||||
atRiskAppDetails: null,
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [RiskInsightsDrawerDialogComponent, BrowserAnimationsModule],
|
||||
providers: [
|
||||
{ provide: DIALOG_DATA, useValue: drawerDetails },
|
||||
{ provide: I18nPipe, useValue: mock<I18nPipe>() },
|
||||
{ provide: I18nService, useValue: mockI18nService },
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(RiskInsightsDrawerDialogComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it("should create", () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
describe("drawerTypes getter", () => {
|
||||
it("should return DrawerType enum", () => {
|
||||
expect(component.drawerTypes).toBe(DrawerType);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isActiveDrawerType", () => {
|
||||
it("should return true if type matches activeDrawerType", () => {
|
||||
component.drawerDetails.activeDrawerType = DrawerType.None;
|
||||
expect(component.isActiveDrawerType(DrawerType.None)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should return false if type does not match activeDrawerType", () => {
|
||||
component.drawerDetails.activeDrawerType = DrawerType.None;
|
||||
expect(component.isActiveDrawerType(DrawerType.AppAtRiskMembers)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Component, ChangeDetectionStrategy, Inject } from "@angular/core";
|
||||
|
||||
import { DrawerDetails, DrawerType } from "@bitwarden/bit-common/dirt/reports/risk-insights";
|
||||
import { DIALOG_DATA } from "@bitwarden/components";
|
||||
import { SharedModule } from "@bitwarden/web-vault/app/shared";
|
||||
|
||||
@Component({
|
||||
imports: [SharedModule],
|
||||
templateUrl: "./risk-insights-drawer-dialog.component.html",
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class RiskInsightsDrawerDialogComponent {
|
||||
constructor(@Inject(DIALOG_DATA) public drawerDetails: DrawerDetails) {}
|
||||
|
||||
// Get a list of drawer types
|
||||
get drawerTypes(): typeof DrawerType {
|
||||
return DrawerType;
|
||||
}
|
||||
|
||||
isActiveDrawerType(type: DrawerType): boolean {
|
||||
return this.drawerDetails.activeDrawerType === type;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user