mirror of
https://github.com/bitwarden/browser.git
synced 2025-02-11 00:31:45 +01:00
Merge branch 'km/fix-bio' of github.com:bitwarden/clients into km/fix-bio
This commit is contained in:
commit
1a6d519ddc
@ -1,23 +1,76 @@
|
||||
<bit-section *ngIf="ciphers?.length > 0 || description" [disableMargin]="disableSectionMargin">
|
||||
<div class="tw-ml-1">
|
||||
<bit-section-header>
|
||||
<h2 bitTypography="h6">
|
||||
{{ title }}
|
||||
</h2>
|
||||
<button
|
||||
*ngIf="showRefresh"
|
||||
bitIconButton="bwi-refresh"
|
||||
type="button"
|
||||
size="small"
|
||||
(click)="onRefresh.emit()"
|
||||
[appA11yTitle]="'refresh' | i18n"
|
||||
></button>
|
||||
<span bitTypography="body2" slot="end">{{ ciphers.length }}</span>
|
||||
</bit-section-header>
|
||||
</div>
|
||||
<ng-container *ngIf="collapsibleKey">
|
||||
<button
|
||||
class="tw-group/vault-section-header hover:tw-bg-secondary-100 tw-pl-1 tw-w-full tw-border-x-0 tw-border-t-0 tw-border-b tw-border-solid focus-visible:tw-outline-none focus-visible:tw-rounded-md focus-visible:tw-ring-inset focus-visible:tw-ring-2 focus-visible:tw-ring-primary-600"
|
||||
[ngClass]="{
|
||||
'tw-border-b-secondary-300': !sectionOpenState(),
|
||||
'tw-border-b-transparent': sectionOpenState(),
|
||||
}"
|
||||
type="button"
|
||||
[bitDisclosureTriggerFor]="disclosureRef"
|
||||
(click)="toggleSectionOpen()"
|
||||
>
|
||||
<ng-container *ngTemplateOutlet="sectionHeader"></ng-container>
|
||||
</button>
|
||||
<ng-container *ngTemplateOutlet="descriptionText"></ng-container>
|
||||
<bit-disclosure #disclosureRef [open]="sectionOpenState()" (openChange)="rerenderViewport()">
|
||||
<ng-container *ngTemplateOutlet="itemGroup"></ng-container>
|
||||
</bit-disclosure>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!collapsibleKey">
|
||||
<div class="tw-pl-1">
|
||||
<ng-container *ngTemplateOutlet="sectionHeader"></ng-container>
|
||||
</div>
|
||||
<ng-container *ngTemplateOutlet="descriptionText"></ng-container>
|
||||
<ng-container *ngTemplateOutlet="itemGroup"></ng-container>
|
||||
</ng-container>
|
||||
</bit-section>
|
||||
|
||||
<ng-template #sectionHeader>
|
||||
<bit-section-header class="tw-p-0.5 -tw-mt-0.5 -tw-mx-0.5">
|
||||
<h2 bitTypography="h6">
|
||||
{{ title }}
|
||||
</h2>
|
||||
<button
|
||||
*ngIf="showRefresh"
|
||||
bitIconButton="bwi-refresh"
|
||||
type="button"
|
||||
size="small"
|
||||
(click)="onRefresh.emit()"
|
||||
[appA11yTitle]="'refresh' | i18n"
|
||||
></button>
|
||||
<span bitTypography="body2" slot="end">
|
||||
<span
|
||||
[ngClass]="{
|
||||
'group-hover/vault-section-header:tw-hidden group-focus-visible/vault-section-header:tw-hidden':
|
||||
collapsibleKey && sectionOpenState(),
|
||||
'tw-hidden': collapsibleKey && !sectionOpenState(),
|
||||
}"
|
||||
>
|
||||
{{ ciphers.length }}
|
||||
</span>
|
||||
<span class="tw-pr-1" *ngIf="collapsibleKey">
|
||||
<i
|
||||
class="bwi"
|
||||
[ngClass]="{
|
||||
'bwi-angle-down tw-inline-block': !sectionOpenState(),
|
||||
'bwi-angle-up tw-hidden group-hover/vault-section-header:tw-inline-block group-focus-visible/vault-section-header:tw-inline-block':
|
||||
sectionOpenState(),
|
||||
}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</span>
|
||||
</span>
|
||||
</bit-section-header>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #descriptionText>
|
||||
<div *ngIf="description" class="tw-text-muted tw-px-1 tw-mb-2" bitTypography="body2">
|
||||
{{ description }}
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #itemGroup>
|
||||
<bit-item-group>
|
||||
<cdk-virtual-scroll-viewport
|
||||
[itemSize]="itemHeight$ | async"
|
||||
@ -85,4 +138,4 @@
|
||||
</bit-item>
|
||||
</cdk-virtual-scroll-viewport>
|
||||
</bit-item-group>
|
||||
</bit-section>
|
||||
</ng-template>
|
||||
|
@ -1,6 +1,6 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { ScrollingModule } from "@angular/cdk/scrolling";
|
||||
import { CdkVirtualScrollViewport, ScrollingModule } from "@angular/cdk/scrolling";
|
||||
import { CommonModule } from "@angular/common";
|
||||
import {
|
||||
AfterViewInit,
|
||||
@ -9,8 +9,11 @@ import {
|
||||
EventEmitter,
|
||||
inject,
|
||||
Input,
|
||||
OnInit,
|
||||
Output,
|
||||
Signal,
|
||||
signal,
|
||||
ViewChild,
|
||||
} from "@angular/core";
|
||||
import { Router, RouterLink } from "@angular/router";
|
||||
import { map } from "rxjs";
|
||||
@ -25,6 +28,8 @@ import {
|
||||
BadgeModule,
|
||||
ButtonModule,
|
||||
CompactModeService,
|
||||
DisclosureComponent,
|
||||
DisclosureTriggerForDirective,
|
||||
DialogService,
|
||||
IconButtonModule,
|
||||
ItemModule,
|
||||
@ -41,6 +46,7 @@ import {
|
||||
import { BrowserApi } from "../../../../../platform/browser/browser-api";
|
||||
import BrowserPopupUtils from "../../../../../platform/popup/browser-popup-utils";
|
||||
import { VaultPopupAutofillService } from "../../../services/vault-popup-autofill.service";
|
||||
import { VaultPopupSectionService } from "../../../services/vault-popup-section.service";
|
||||
import { PopupCipherView } from "../../../views/popup-cipher.view";
|
||||
import { ItemCopyActionsComponent } from "../item-copy-action/item-copy-actions.component";
|
||||
import { ItemMoreOptionsComponent } from "../item-more-options/item-more-options.component";
|
||||
@ -61,14 +67,25 @@ import { ItemMoreOptionsComponent } from "../item-more-options/item-more-options
|
||||
ItemMoreOptionsComponent,
|
||||
OrgIconDirective,
|
||||
ScrollingModule,
|
||||
DisclosureComponent,
|
||||
DisclosureTriggerForDirective,
|
||||
DecryptionFailureDialogComponent,
|
||||
],
|
||||
selector: "app-vault-list-items-container",
|
||||
templateUrl: "vault-list-items-container.component.html",
|
||||
standalone: true,
|
||||
})
|
||||
export class VaultListItemsContainerComponent implements AfterViewInit {
|
||||
export class VaultListItemsContainerComponent implements OnInit, AfterViewInit {
|
||||
private compactModeService = inject(CompactModeService);
|
||||
private vaultPopupSectionService = inject(VaultPopupSectionService);
|
||||
|
||||
@ViewChild(CdkVirtualScrollViewport, { static: false }) viewPort: CdkVirtualScrollViewport;
|
||||
@ViewChild(DisclosureComponent) disclosure: DisclosureComponent;
|
||||
|
||||
/**
|
||||
* Indicates whether the section should be open or closed if collapsibleKey is provided
|
||||
*/
|
||||
protected sectionOpenState: Signal<boolean> | undefined;
|
||||
|
||||
/**
|
||||
* The class used to set the height of a bit item's inner content.
|
||||
@ -106,6 +123,15 @@ export class VaultListItemsContainerComponent implements AfterViewInit {
|
||||
@Input()
|
||||
title: string;
|
||||
|
||||
/**
|
||||
* Optionally allow the items to be collapsed.
|
||||
*
|
||||
* The key must be added to the state definition in `vault-popup-section.service.ts` since the
|
||||
* collapsed state is stored locally.
|
||||
*/
|
||||
@Input()
|
||||
collapsibleKey: "favorites" | "allItems" | undefined;
|
||||
|
||||
/**
|
||||
* Optional description for the vault list item section. Will be shown below the title even when
|
||||
* no ciphers are available.
|
||||
@ -168,6 +194,16 @@ export class VaultListItemsContainerComponent implements AfterViewInit {
|
||||
private dialogService: DialogService,
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (!this.collapsibleKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.sectionOpenState = this.vaultPopupSectionService.getOpenDisplayStateForSection(
|
||||
this.collapsibleKey,
|
||||
);
|
||||
}
|
||||
|
||||
async ngAfterViewInit() {
|
||||
const autofillShortcut = await this.platformUtilsService.getAutofillKeyboardShortcut();
|
||||
|
||||
@ -239,4 +275,30 @@ export class VaultListItemsContainerComponent implements AfterViewInit {
|
||||
cipher.canLaunch ? 200 : 0,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update section open/close state based on user action
|
||||
*/
|
||||
async toggleSectionOpen() {
|
||||
if (!this.collapsibleKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.vaultPopupSectionService.updateSectionOpenStoredState(
|
||||
this.collapsibleKey,
|
||||
this.disclosure.open,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Force virtual scroll to update its viewport size to avoid display bugs
|
||||
*
|
||||
* Angular CDK scroll has a bug when used with conditional rendering:
|
||||
* https://github.com/angular/components/issues/24362
|
||||
*/
|
||||
protected rerenderViewport() {
|
||||
setTimeout(() => {
|
||||
this.viewPort.checkViewportSize();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -66,12 +66,14 @@
|
||||
[title]="'favorites' | i18n"
|
||||
[ciphers]="favoriteCiphers$ | async"
|
||||
id="favorites"
|
||||
collapsibleKey="favorites"
|
||||
></app-vault-list-items-container>
|
||||
<app-vault-list-items-container
|
||||
[title]="'allItems' | i18n"
|
||||
[ciphers]="remainingCiphers$ | async"
|
||||
id="allItems"
|
||||
disableSectionMargin
|
||||
collapsibleKey="allItems"
|
||||
></app-vault-list-items-container>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
@ -0,0 +1,129 @@
|
||||
import { computed, effect, inject, Injectable, signal, Signal } from "@angular/core";
|
||||
import { toSignal } from "@angular/core/rxjs-interop";
|
||||
import { map } from "rxjs";
|
||||
|
||||
import {
|
||||
KeyDefinition,
|
||||
StateProvider,
|
||||
VAULT_SETTINGS_DISK,
|
||||
} from "@bitwarden/common/platform/state";
|
||||
|
||||
import { VaultPopupItemsService } from "./vault-popup-items.service";
|
||||
|
||||
export type PopupSectionOpen = {
|
||||
favorites: boolean;
|
||||
allItems: boolean;
|
||||
};
|
||||
|
||||
const SECTION_OPEN_KEY = new KeyDefinition<PopupSectionOpen>(VAULT_SETTINGS_DISK, "sectionOpen", {
|
||||
deserializer: (obj) => obj,
|
||||
});
|
||||
|
||||
const INITIAL_OPEN: PopupSectionOpen = {
|
||||
favorites: true,
|
||||
allItems: true,
|
||||
};
|
||||
|
||||
@Injectable({
|
||||
providedIn: "root",
|
||||
})
|
||||
export class VaultPopupSectionService {
|
||||
private vaultPopupItemsService = inject(VaultPopupItemsService);
|
||||
private stateProvider = inject(StateProvider);
|
||||
|
||||
private hasFilterOrSearchApplied = toSignal(
|
||||
this.vaultPopupItemsService.hasFilterApplied$.pipe(map((hasFilter) => hasFilter)),
|
||||
);
|
||||
|
||||
/**
|
||||
* Used to change the open/close state without persisting it to the local disk. Reflects
|
||||
* application-applied overrides.
|
||||
* `null` means there is no current override
|
||||
*/
|
||||
private temporaryStateOverride = signal<Partial<PopupSectionOpen> | null>(null);
|
||||
|
||||
constructor() {
|
||||
effect(
|
||||
() => {
|
||||
/**
|
||||
* auto-open all sections when search or filter is applied, and remove
|
||||
* override when search or filter is removed
|
||||
*/
|
||||
if (this.hasFilterOrSearchApplied()) {
|
||||
this.temporaryStateOverride.set(INITIAL_OPEN);
|
||||
} else {
|
||||
this.temporaryStateOverride.set(null);
|
||||
}
|
||||
},
|
||||
{
|
||||
allowSignalWrites: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stored disk state for the open/close state of the sections. Will be `null` if user has never
|
||||
* opened/closed a section
|
||||
*/
|
||||
private sectionOpenStateProvider = this.stateProvider.getGlobal(SECTION_OPEN_KEY);
|
||||
|
||||
/**
|
||||
* Stored disk state for the open/close state of the sections, with an initial value provided
|
||||
* if the stored disk state does not yet exist.
|
||||
*/
|
||||
private sectionOpenStoredState = toSignal<PopupSectionOpen | null>(
|
||||
this.sectionOpenStateProvider.state$.pipe(map((sectionOpen) => sectionOpen ?? INITIAL_OPEN)),
|
||||
// Indicates that the state value is loading
|
||||
{ initialValue: null },
|
||||
);
|
||||
|
||||
/**
|
||||
* Indicates the current open/close display state of each section, accounting for temporary
|
||||
* non-persisted overrides.
|
||||
*/
|
||||
sectionOpenDisplayState: Signal<Partial<PopupSectionOpen>> = computed(() => ({
|
||||
...this.sectionOpenStoredState(),
|
||||
...this.temporaryStateOverride(),
|
||||
}));
|
||||
|
||||
/**
|
||||
* Retrieve the open/close display state for a given section.
|
||||
*
|
||||
* @param sectionKey section key
|
||||
*/
|
||||
getOpenDisplayStateForSection(sectionKey: keyof PopupSectionOpen): Signal<boolean | undefined> {
|
||||
return computed(() => this.sectionOpenDisplayState()?.[sectionKey]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the stored open/close state of a given section. Should be called only when a user action
|
||||
* is taken directly to change the open/close state.
|
||||
*
|
||||
* Removes any current temporary override for the given section, as direct user action should
|
||||
* supersede any application-applied overrides.
|
||||
*
|
||||
* @param sectionKey section key
|
||||
*/
|
||||
async updateSectionOpenStoredState(
|
||||
sectionKey: keyof PopupSectionOpen,
|
||||
open: boolean,
|
||||
): Promise<void> {
|
||||
await this.sectionOpenStateProvider.update((currentState) => {
|
||||
return {
|
||||
...(currentState ?? INITIAL_OPEN),
|
||||
[sectionKey]: open,
|
||||
};
|
||||
});
|
||||
|
||||
this.temporaryStateOverride.update((prev) => {
|
||||
if (prev !== null) {
|
||||
return {
|
||||
...prev,
|
||||
[sectionKey]: open,
|
||||
};
|
||||
}
|
||||
|
||||
return prev;
|
||||
});
|
||||
}
|
||||
}
|
@ -33,7 +33,7 @@ export class ResellerWarningService {
|
||||
return {
|
||||
type: "warning",
|
||||
message: this.i18nService.t(
|
||||
"resellerPastDueWarning",
|
||||
"resellerPastDueWarningMsg",
|
||||
organization.providerName,
|
||||
this.formatDate(gracePeriodEnd),
|
||||
),
|
||||
@ -50,7 +50,7 @@ export class ResellerWarningService {
|
||||
return {
|
||||
type: "info",
|
||||
message: this.i18nService.t(
|
||||
"resellerOpenInvoiceWarning",
|
||||
"resellerOpenInvoiceWarningMgs",
|
||||
organization.providerName,
|
||||
this.formatDate(organizationBillingMetadata.invoiceCreatedDate),
|
||||
this.formatDate(organizationBillingMetadata.invoiceDueDate),
|
||||
@ -68,7 +68,7 @@ export class ResellerWarningService {
|
||||
return {
|
||||
type: "info",
|
||||
message: this.i18nService.t(
|
||||
"resellerRenewalWarning",
|
||||
"resellerRenewalWarningMsg",
|
||||
organization.providerName,
|
||||
this.formatDate(organizationBillingMetadata.subPeriodEndDate),
|
||||
),
|
||||
|
@ -113,6 +113,27 @@
|
||||
"atRiskMembers": {
|
||||
"message": "At-risk members"
|
||||
},
|
||||
"atRiskMembersWithCount": {
|
||||
"message": "At-risk members ($COUNT$)",
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"content": "$1",
|
||||
"example": "3"
|
||||
}
|
||||
}
|
||||
},
|
||||
"atRiskMembersDescription": {
|
||||
"message": "These members are logging into applications with weak, exposed, or reused passwords."
|
||||
},
|
||||
"atRiskMembersDescriptionWithApp": {
|
||||
"message": "These members are logging into $APPNAME$ with weak, exposed, or reused passwords.",
|
||||
"placeholders": {
|
||||
"appname": {
|
||||
"content": "$1",
|
||||
"example": "Salesforce"
|
||||
}
|
||||
}
|
||||
},
|
||||
"totalMembers": {
|
||||
"message": "Total members"
|
||||
},
|
||||
@ -10083,8 +10104,8 @@
|
||||
"organizationNameMaxLength": {
|
||||
"message": "Organization name cannot exceed 50 characters."
|
||||
},
|
||||
"resellerRenewalWarning": {
|
||||
"message": "Your subscription will renew soon. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.",
|
||||
"resellerRenewalWarningMsg": {
|
||||
"message": "Your subscription will renew soon. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.",
|
||||
"placeholders": {
|
||||
"reseller": {
|
||||
"content": "$1",
|
||||
@ -10096,8 +10117,8 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"resellerOpenInvoiceWarning": {
|
||||
"message": "An invoice for your subscription was issued on $ISSUED_DATE$. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.",
|
||||
"resellerOpenInvoiceWarningMgs": {
|
||||
"message": "An invoice for your subscription was issued on $ISSUED_DATE$. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.",
|
||||
"placeholders": {
|
||||
"reseller": {
|
||||
"content": "$1",
|
||||
@ -10113,8 +10134,8 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"resellerPastDueWarning": {
|
||||
"message": "The invoice for your subscription has not been paid. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.",
|
||||
"resellerPastDueWarningMsg": {
|
||||
"message": "The invoice for your subscription has not been paid. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.",
|
||||
"placeholders": {
|
||||
"reseller": {
|
||||
"content": "$1",
|
||||
|
@ -90,3 +90,13 @@ export type MemberDetailsFlat = {
|
||||
email: string;
|
||||
cipherId: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Member email with the number of at risk passwords
|
||||
* At risk member detail that contains the email
|
||||
* and the count of at risk ciphers
|
||||
*/
|
||||
export type AtRiskMemberDetail = {
|
||||
email: string;
|
||||
atRiskPasswordCount: number;
|
||||
};
|
||||
|
@ -12,6 +12,7 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import {
|
||||
ApplicationHealthReportDetail,
|
||||
ApplicationHealthReportSummary,
|
||||
AtRiskMemberDetail,
|
||||
CipherHealthReportDetail,
|
||||
CipherHealthReportUriDetail,
|
||||
ExposedPasswordDetail,
|
||||
@ -89,6 +90,30 @@ export class RiskInsightsReportService {
|
||||
return results$;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a list of members with at-risk passwords along with the number of at-risk passwords.
|
||||
*/
|
||||
generateAtRiskMemberList(
|
||||
cipherHealthReportDetails: ApplicationHealthReportDetail[],
|
||||
): AtRiskMemberDetail[] {
|
||||
const memberRiskMap = new Map<string, number>();
|
||||
|
||||
cipherHealthReportDetails.forEach((app) => {
|
||||
app.atRiskMemberDetails.forEach((member) => {
|
||||
if (memberRiskMap.has(member.email)) {
|
||||
memberRiskMap.set(member.email, memberRiskMap.get(member.email) + 1);
|
||||
} else {
|
||||
memberRiskMap.set(member.email, 1);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return Array.from(memberRiskMap.entries()).map(([email, atRiskPasswordCount]) => ({
|
||||
email,
|
||||
atRiskPasswordCount,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the summary from the application health report. Returns total members and applications as well
|
||||
* as the total at risk members and at risk applications
|
||||
|
@ -27,10 +27,11 @@
|
||||
<h2 class="tw-mb-6" bitTypography="h2">{{ "allApplications" | i18n }}</h2>
|
||||
<div class="tw-flex tw-gap-6">
|
||||
<tools-card
|
||||
class="tw-flex-1"
|
||||
class="tw-flex-1 tw-cursor-pointer"
|
||||
[title]="'atRiskMembers' | i18n"
|
||||
[value]="applicationSummary.totalAtRiskMemberCount"
|
||||
[maxValue]="applicationSummary.totalMemberCount"
|
||||
(click)="showOrgAtRiskMembers()"
|
||||
>
|
||||
</tools-card>
|
||||
<tools-card
|
||||
@ -82,7 +83,7 @@
|
||||
(change)="onCheckboxChange(r.id, $event)"
|
||||
/>
|
||||
</td>
|
||||
<td bitCell>
|
||||
<td class="tw-cursor-pointer" (click)="showAppAtRiskMembers(r.applicationName)" bitCell>
|
||||
<span>{{ r.applicationName }}</span>
|
||||
</td>
|
||||
<td bitCell>
|
||||
|
@ -19,6 +19,7 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import {
|
||||
DialogService,
|
||||
Icons,
|
||||
NoItemsModule,
|
||||
SearchModule,
|
||||
@ -30,6 +31,8 @@ import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.mod
|
||||
import { SharedModule } from "@bitwarden/web-vault/app/shared";
|
||||
import { PipesModule } from "@bitwarden/web-vault/app/vault/individual-vault/pipes/pipes.module";
|
||||
|
||||
import { openAppAtRiskMembersDialog } from "./app-at-risk-members-dialog.component";
|
||||
import { OrgAtRiskMembersDialogComponent } from "./org-at-risk-members-dialog.component";
|
||||
import { ApplicationsLoadingComponent } from "./risk-insights-loading.component";
|
||||
|
||||
@Component({
|
||||
@ -99,6 +102,7 @@ export class AllApplicationsComponent implements OnInit, OnDestroy {
|
||||
protected dataService: RiskInsightsDataService,
|
||||
protected organizationService: OrganizationService,
|
||||
protected reportService: RiskInsightsReportService,
|
||||
protected dialogService: DialogService,
|
||||
) {
|
||||
this.searchControl.valueChanges
|
||||
.pipe(debounceTime(200), takeUntilDestroyed())
|
||||
@ -135,6 +139,21 @@ export class AllApplicationsComponent implements OnInit, OnDestroy {
|
||||
return item.applicationName;
|
||||
}
|
||||
|
||||
showAppAtRiskMembers = async (applicationName: string) => {
|
||||
openAppAtRiskMembersDialog(this.dialogService, {
|
||||
members:
|
||||
this.dataSource.data.find((app) => app.applicationName === applicationName)
|
||||
?.atRiskMemberDetails ?? [],
|
||||
applicationName,
|
||||
});
|
||||
};
|
||||
|
||||
showOrgAtRiskMembers = async () => {
|
||||
this.dialogService.open(OrgAtRiskMembersDialogComponent, {
|
||||
data: this.reportService.generateAtRiskMemberList(this.dataSource.data),
|
||||
});
|
||||
};
|
||||
|
||||
onCheckboxChange(id: number, event: Event) {
|
||||
const isChecked = (event.target as HTMLInputElement).checked;
|
||||
if (isChecked) {
|
||||
|
@ -0,0 +1,21 @@
|
||||
<bit-dialog>
|
||||
<span bitDialogTitle>{{ applicationName }}</span>
|
||||
<ng-container bitDialogContent>
|
||||
<div class="tw-flex tw-flex-col tw-gap-2">
|
||||
<span bitDialogTitle>{{ "atRiskMembersWithCount" | i18n: members.length }} </span>
|
||||
<span class="tw-text-muted">{{
|
||||
"atRiskMembersDescriptionWithApp" | i18n: applicationName
|
||||
}}</span>
|
||||
<div class="tw-mt-1">
|
||||
<ng-container *ngFor="let member of members">
|
||||
<div>{{ member.email }}</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container bitDialogFooter>
|
||||
<button bitButton bitDialogClose buttonType="secondary" type="button">
|
||||
{{ "close" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
@ -0,0 +1,35 @@
|
||||
import { DIALOG_DATA } from "@angular/cdk/dialog";
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, Inject } from "@angular/core";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { MemberDetailsFlat } from "@bitwarden/bit-common/tools/reports/risk-insights/models/password-health";
|
||||
import { ButtonModule, DialogModule, DialogService } from "@bitwarden/components";
|
||||
|
||||
type AppAtRiskMembersDialogParams = {
|
||||
members: MemberDetailsFlat[];
|
||||
applicationName: string;
|
||||
};
|
||||
|
||||
export const openAppAtRiskMembersDialog = (
|
||||
dialogService: DialogService,
|
||||
dialogConfig: AppAtRiskMembersDialogParams,
|
||||
) =>
|
||||
dialogService.open<boolean, AppAtRiskMembersDialogParams>(AppAtRiskMembersDialogComponent, {
|
||||
data: dialogConfig,
|
||||
});
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
templateUrl: "./app-at-risk-members-dialog.component.html",
|
||||
imports: [ButtonModule, CommonModule, JslibModule, DialogModule],
|
||||
})
|
||||
export class AppAtRiskMembersDialogComponent {
|
||||
protected members: MemberDetailsFlat[];
|
||||
protected applicationName: string;
|
||||
|
||||
constructor(@Inject(DIALOG_DATA) private params: AppAtRiskMembersDialogParams) {
|
||||
this.members = params.members;
|
||||
this.applicationName = params.applicationName;
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
<bit-dialog>
|
||||
<ng-container bitDialogTitle>
|
||||
<span bitDialogTitle>{{ "atRiskMembersWithCount" | i18n: atRiskMembers.length }} </span>
|
||||
</ng-container>
|
||||
<ng-container bitDialogContent>
|
||||
<div class="tw-flex tw-flex-col tw-gap-2">
|
||||
<span bitTypography="body2" class="tw-text-muted">{{
|
||||
"atRiskMembersDescription" | i18n
|
||||
}}</span>
|
||||
<div class="tw-flex tw-justify-between tw-mt-2 tw-text-muted">
|
||||
<div bitTypography="body2" class="tw-font-bold">{{ "email" | i18n }}</div>
|
||||
<div bitTypography="body2" class="tw-font-bold">{{ "atRiskPasswords" | i18n }}</div>
|
||||
</div>
|
||||
<ng-container *ngFor="let member of atRiskMembers">
|
||||
<div class="tw-flex tw-justify-between tw-mt-2">
|
||||
<div>{{ member.email }}</div>
|
||||
<div>{{ member.atRiskPasswordCount }}</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container bitDialogFooter>
|
||||
<button bitButton bitDialogClose buttonType="secondary" type="button">
|
||||
{{ "close" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
@ -0,0 +1,24 @@
|
||||
import { DIALOG_DATA } from "@angular/cdk/dialog";
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, Inject } from "@angular/core";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { AtRiskMemberDetail } from "@bitwarden/bit-common/tools/reports/risk-insights/models/password-health";
|
||||
import { ButtonModule, DialogModule, DialogService, TypographyModule } from "@bitwarden/components";
|
||||
|
||||
export const openOrgAtRiskMembersDialog = (
|
||||
dialogService: DialogService,
|
||||
dialogConfig: AtRiskMemberDetail[],
|
||||
) =>
|
||||
dialogService.open<boolean, AtRiskMemberDetail[]>(OrgAtRiskMembersDialogComponent, {
|
||||
data: dialogConfig,
|
||||
});
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
templateUrl: "./org-at-risk-members-dialog.component.html",
|
||||
imports: [ButtonModule, CommonModule, DialogModule, JslibModule, TypographyModule],
|
||||
})
|
||||
export class OrgAtRiskMembersDialogComponent {
|
||||
constructor(@Inject(DIALOG_DATA) protected atRiskMembers: AtRiskMemberDetail[]) {}
|
||||
}
|
@ -2,7 +2,7 @@
|
||||
<div class="[&>*]:tw-mb-0 [&>*]:tw-text-main tw-flex tw-items-center tw-gap-1">
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
<div class="tw-text-muted has-[button]:-tw-mb-1">
|
||||
<div class="tw-text-muted -tw-mb-0.5">
|
||||
<ng-content select="[slot=end]"></ng-content>
|
||||
</div>
|
||||
</div>
|
||||
|
Loading…
Reference in New Issue
Block a user