1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-26 09:33:22 +00:00

Merge branch 'main' into km/pm-14445

This commit is contained in:
Maciej Zieniuk
2024-11-05 12:21:59 +00:00
105 changed files with 5172 additions and 7967 deletions

View File

@@ -23,15 +23,14 @@
<bit-tab [label]="'role' | i18n">
<ng-container *ngIf="!editMode">
<p bitTypography="body1">{{ "inviteUserDesc" | i18n }}</p>
<bit-form-field *ngIf="remainingSeats$ | async as remainingSeats">
<bit-form-field>
<bit-label>{{ "email" | i18n }}</bit-label>
<input id="emails" type="text" appAutoFocus bitInput formControlName="emails" />
<bit-hint *ngIf="remainingSeats > 1; else singleSeat">{{
"inviteMultipleEmailDesc" | i18n: remainingSeats
<bit-hint>{{
"inviteMultipleEmailDesc"
| i18n
: (organization.productTierType === ProductTierType.TeamsStarter ? "10" : "20")
}}</bit-hint>
<ng-template #singleSeat>
<bit-hint>{{ "inviteSingleEmailDesc" | i18n: remainingSeats }}</bit-hint>
</ng-template>
</bit-form-field>
</ng-container>
<bit-radio-group formControlName="type">
@@ -265,6 +264,16 @@
<button
*ngIf="editMode"
type="button"
bitIconButton="bwi-close"
buttonType="danger"
bitFormButton
[appA11yTitle]="'remove' | i18n"
[bitAction]="remove"
[disabled]="loading"
></button>
<button
*ngIf="editMode && params.managedByOrganization === true"
type="button"
bitIconButton="bwi-trash"
buttonType="danger"
bitFormButton

View File

@@ -65,6 +65,7 @@ export interface MemberDialogParams {
isOnSecretsManagerStandalone: boolean;
initialTab?: MemberDialogTab;
numConfirmedMembers: number;
managedByOrganization?: boolean;
}
export enum MemberDialogResult {
@@ -89,7 +90,6 @@ export class MemberDialogComponent implements OnDestroy {
PermissionMode = PermissionMode;
showNoMasterPasswordWarning = false;
isOnSecretsManagerStandalone: boolean;
remainingSeats$: Observable<number>;
protected organization$: Observable<Organization>;
protected collectionAccessItems: AccessItemView[] = [];
@@ -251,10 +251,6 @@ export class MemberDialogComponent implements OnDestroy {
this.loading = false;
});
this.remainingSeats$ = this.organization$.pipe(
map((organization) => organization.seats - this.params.numConfirmedMembers),
);
}
private setFormValidators(organization: Organization) {
@@ -469,7 +465,7 @@ export class MemberDialogComponent implements OnDestroy {
this.close(MemberDialogResult.Saved);
};
delete = async () => {
remove = async () => {
if (!this.editMode) {
return;
}
@@ -566,6 +562,39 @@ export class MemberDialogComponent implements OnDestroy {
this.close(MemberDialogResult.Restored);
};
delete = async () => {
if (!this.editMode) {
return;
}
const confirmed = await this.dialogService.openSimpleDialog({
title: {
key: "deleteOrganizationUser",
placeholders: [this.params.name],
},
content: { key: "deleteOrganizationUserWarning" },
type: "warning",
acceptButtonText: { key: "delete" },
cancelButtonText: { key: "cancel" },
});
if (!confirmed) {
return false;
}
await this.organizationUserApiService.deleteOrganizationUser(
this.params.organizationId,
this.params.organizationUserId,
);
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("organizationUserDeleted", this.params.name),
});
this.close(MemberDialogResult.Deleted);
};
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();

View File

@@ -320,6 +320,17 @@
<i aria-hidden="true" class="bwi bwi-close"></i> {{ "remove" | i18n }}
</span>
</button>
<button
*ngIf="u.managedByOrganization === true"
type="button"
bitMenuItem
(click)="deleteUser(u)"
>
<span class="tw-text-danger">
<i class="bwi bwi-fw bwi-trash" aria-hidden="true"></i>
{{ "delete" | i18n }}
</span>
</button>
</bit-menu>
</td>
</tr>

View File

@@ -486,7 +486,7 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
const enableUpgradePasswordManagerSub = await firstValueFrom(
this.enableUpgradePasswordManagerSub$,
);
if (enableUpgradePasswordManagerSub) {
if (enableUpgradePasswordManagerSub && this.organization.canEditSubscription) {
const reference = openChangePlanDialog(this.dialogService, {
data: {
organizationId: this.organization.id,
@@ -518,6 +518,7 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
isOnSecretsManagerStandalone: this.orgIsOnSecretsManagerStandalone,
initialTab: initialTab,
numConfirmedMembers: this.dataSource.confirmedUserCount,
managedByOrganization: user?.managedByOrganization,
},
});
@@ -725,6 +726,40 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
return true;
}
async deleteUser(user: OrganizationUserView) {
const confirmed = await this.dialogService.openSimpleDialog({
title: {
key: "deleteOrganizationUser",
placeholders: [this.userNamePipe.transform(user)],
},
content: { key: "deleteOrganizationUserWarning" },
type: "warning",
acceptButtonText: { key: "delete" },
cancelButtonText: { key: "cancel" },
});
if (!confirmed) {
return false;
}
this.actionPromise = this.organizationUserApiService.deleteOrganizationUser(
this.organization.id,
user.id,
);
try {
await this.actionPromise;
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("organizationUserDeleted", this.userNamePipe.transform(user)),
});
this.dataSource.removeUser(user);
} catch (e) {
this.validationService.showError(e);
}
this.actionPromise = null;
}
private async noMasterPasswordConfirmationDialog(user: OrganizationUserView) {
return this.dialogService.openSimpleDialog({
title: {

View File

@@ -21,7 +21,13 @@
>
{{ "purgeVault" | i18n }}
</button>
<button type="button" bitButton buttonType="danger" [bitAction]="deleteAccount">
<button
*ngIf="showDeleteAccount$ | async"
type="button"
bitButton
buttonType="danger"
[bitAction]="deleteAccount"
>
{{ "deleteAccount" | i18n }}
</button>
</app-danger-zone>

View File

@@ -23,6 +23,7 @@ export class AccountComponent implements OnInit {
showChangeEmail$: Observable<boolean>;
showPurgeVault$: Observable<boolean>;
showDeleteAccount$: Observable<boolean>;
constructor(
private modalService: ModalService,
@@ -63,6 +64,16 @@ export class AccountComponent implements OnInit {
!isAccountDeprovisioningEnabled || !userIsManagedByOrganization,
),
);
this.showDeleteAccount$ = combineLatest([
isAccountDeprovisioningEnabled$,
userIsManagedByOrganization$,
]).pipe(
map(
([isAccountDeprovisioningEnabled, userIsManagedByOrganization]) =>
!isAccountDeprovisioningEnabled || !userIsManagedByOrganization,
),
);
}
async deauthorizeSessions() {

View File

@@ -26,7 +26,7 @@
title="{{ 'customColor' | i18n }}"
[ngClass]="{
'!tw-outline-[3px] tw-outline-primary-600 hover:tw-outline-[3px] hover:tw-outline-primary-600':
customColorSelected,
customColorSelected
}"
class="tw-relative tw-flex tw-h-24 tw-w-24 tw-cursor-pointer tw-place-content-center tw-content-center tw-justify-center tw-rounded-full tw-border tw-border-solid tw-border-secondary-600 tw-outline tw-outline-0 tw-outline-offset-1 hover:tw-outline-1 hover:tw-outline-primary-300 focus:tw-outline-2 focus:tw-outline-primary-600"
[style.background-color]="customColor$ | async"

View File

@@ -34,7 +34,7 @@
[organizationInfo]="{
name: orgInfoFormGroup.value.name,
email: orgInfoFormGroup.value.billingEmail,
type: trialOrganizationType,
type: trialOrganizationType
}"
[subscriptionProduct]="
product === ProductType.SecretsManager

View File

@@ -33,7 +33,7 @@
[organizationInfo]="{
name: formGroup.get('name').value,
email: formGroup.get('email').value,
type: productType,
type: productType
}"
[subscriptionProduct]="SubscriptionProduct.SecretsManager"
(steppedBack)="steppedBack()"

View File

@@ -102,7 +102,7 @@
[organizationInfo]="{
name: orgInfoFormGroup.get('name').value,
email: orgInfoFormGroup.get('email').value,
type: trialOrganizationType,
type: trialOrganizationType
}"
[subscriptionProduct]="SubscriptionProduct.PasswordManager"
(steppedBack)="previousStep()"

View File

@@ -6,7 +6,7 @@
[disabled]="disabled"
class="tw-flex tw-w-full tw-items-center tw-border-none tw-bg-transparent"
[ngClass]="{
'hover:tw-bg-secondary-100': !disabled && step.editable,
'hover:tw-bg-secondary-100': !disabled && step.editable
}"
[attr.aria-expanded]="selected"
>
@@ -16,7 +16,7 @@
[ngClass]="{
'tw-bg-primary-600 tw-text-contrast': selected,
'tw-bg-secondary-300 tw-text-main': !selected && !disabled && step.editable,
'tw-bg-transparent tw-text-muted': disabled,
'tw-bg-transparent tw-text-muted': disabled
}"
>
{{ stepNumber }}
@@ -30,13 +30,13 @@
<div
class="tw-txt-main tw-mt-3.5 tw-h-12 tw-text-left tw-leading-snug"
[ngClass]="{
'tw-font-bold': selected,
'tw-font-bold': selected
}"
>
<p
class="main-label text tw-mb-1 tw-text-main"
[ngClass]="{
'tw-mt-1': !step.subLabel,
'tw-mt-1': !step.subLabel
}"
>
{{ step.label }}

View File

@@ -3,7 +3,7 @@
class="tw-inline-block tw-w-11/12 tw-pl-7"
[ngClass]="{
'tw-border-0 tw-border-l tw-border-solid tw-border-secondary-300': applyBorder,
'tw-pt-6': addSubLabelSpacing,
'tw-pt-6': addSubLabelSpacing
}"
>
<ng-content></ng-content>

View File

@@ -73,7 +73,7 @@
class="tw-bg-secondary-100 tw-text-center !tw-border-0 tw-text-sm tw-font-bold tw-py-1"
[ngClass]="{
'tw-bg-primary-700 !tw-text-contrast': selectableProduct === selectedPlan,
'tw-bg-secondary-100': !(selectableProduct === selectedPlan),
'tw-bg-secondary-100': !(selectableProduct === selectedPlan)
}"
>
{{ "recommended" | i18n }}
@@ -82,7 +82,7 @@
class="tw-px-2 tw-pb-[4px]"
[ngClass]="{
'tw-py-1': !(selectableProduct === selectedPlan),
'tw-py-0': selectableProduct === selectedPlan,
'tw-py-0': selectableProduct === selectedPlan
}"
>
<h3

View File

@@ -261,7 +261,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
];
this.discountPercentageFromSub = this.isSecretsManagerTrial()
? 0
: (this.sub?.customerDiscount?.percentOff ?? 0);
: this.sub?.customerDiscount?.percentOff ?? 0;
this.setInitialPlanSelection();
this.loading = false;

View File

@@ -3,7 +3,7 @@
class="-tw-m-6 tw-mb-3 tw-flex tw-flex-col tw-p-6"
[ngClass]="{
'tw-border-0 tw-border-b tw-border-solid tw-border-secondary-300 tw-bg-background-alt tw-pb-0':
tabsContainer.childElementCount !== 0,
tabsContainer.childElementCount !== 0
}"
>
<div class="tw-flex">

View File

@@ -5,7 +5,7 @@
<section
[ngStyle]="{
'--num-products': products.bento.length,
'grid-template-columns': 'repeat(min(var(--num-products,1),3),auto)',
'grid-template-columns': 'repeat(min(var(--num-products,1),3),auto)'
}"
class="tw-grid tw-gap-2"
>

View File

@@ -2,7 +2,7 @@
<i class="bwi bwi-fw !tw-mr-4" [ngClass]="completed ? 'bwi-check tw-text-success' : icon"></i
><span
[ngClass]="{
'tw-text-primary-700 tw-line-through tw-decoration-primary-700 tw-opacity-50': completed,
'tw-text-primary-700 tw-line-through tw-decoration-primary-700 tw-opacity-50': completed
}"
>{{ title }}<i class="bwi bwi-angle-right tw-ml-1"></i
></span>

View File

@@ -15,7 +15,7 @@
{{ "refresh" | i18n }}
</a>
</div>
<bit-tab-group [(selectedIndex)]="tabIndex">
<bit-tab-group [(selectedIndex)]="tabIndex" (selectedIndexChange)="onTabChange($event)">
<bit-tab label="{{ 'allApplicationsWithCount' | i18n: apps.length }}">
<tools-all-applications></tools-all-applications>
</bit-tab>

View File

@@ -1,8 +1,7 @@
import { CommonModule } from "@angular/common";
import { Component } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { ActivatedRoute } from "@angular/router";
import { first } from "rxjs";
import { ActivatedRoute, Router } from "@angular/router";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { AsyncActionsModule, ButtonModule, TabsModule } from "@bitwarden/components";
@@ -18,7 +17,7 @@ import { PasswordHealthComponent } from "./password-health.component";
export enum AccessIntelligenceTabType {
AllApps = 0,
PriorityApps = 1,
CriticalApps = 1,
NotifiedMembers = 2,
}
@@ -58,8 +57,19 @@ export class AccessIntelligenceComponent {
);
}
constructor(route: ActivatedRoute) {
route.queryParams.pipe(takeUntilDestroyed(), first()).subscribe(({ tabIndex }) => {
onTabChange = async (newIndex: number) => {
await this.router.navigate([], {
relativeTo: this.route,
queryParams: { tabIndex: newIndex },
queryParamsHandling: "merge",
});
};
constructor(
protected route: ActivatedRoute,
private router: Router,
) {
route.queryParams.pipe(takeUntilDestroyed()).subscribe(({ tabIndex }) => {
this.tabIndex = !isNaN(tabIndex) ? tabIndex : AccessIntelligenceTabType.AllApps;
});
}

View File

@@ -14,13 +14,15 @@
</h2>
</ng-container>
<ng-container slot="description">
<p class="tw-text-muted">
{{ "noAppsInOrgDescription" | i18n }}
<div class="tw-flex tw-flex-col tw-mb-2">
<span class="tw-text-muted">
{{ "noAppsInOrgDescription" | i18n }}
</span>
<a class="text-primary" routerLink="/login">{{ "learnMore" | i18n }}</a>
</p>
</div>
</ng-container>
<ng-container slot="button">
<button bitButton buttonType="primary" type="button">
<button (click)="goToCreateNewLoginItem()" bitButton buttonType="primary" type="button">
{{ "createNewLoginItem" | i18n }}
</button>
</ng-container>
@@ -50,7 +52,15 @@
class="tw-grow"
[formControl]="searchControl"
></bit-search>
<button class="tw-rounded-lg" type="button" buttonType="secondary" bitButton>
<button
class="tw-rounded-lg"
type="button"
buttonType="secondary"
bitButton
[disabled]="!selectedIds.size"
[loading]="markingAsCritical"
(click)="markAppsAsCritical()"
>
<i class="bwi bwi-star-f tw-mr-2"></i>
{{ "markAppAsCritical" | i18n }}
</button>

View File

@@ -40,6 +40,7 @@ export class AllApplicationsComponent implements OnInit {
protected loading = false;
protected organization: Organization;
noItemsIcon = Icons.Security;
protected markingAsCritical = false;
// MOCK DATA
protected mockData = applicationTableMockData;
@@ -76,8 +77,18 @@ export class AllApplicationsComponent implements OnInit {
.subscribe((v) => (this.dataSource.filter = v));
}
goToCreateNewLoginItem = async () => {
// TODO: implement
this.toastService.showToast({
variant: "warning",
title: null,
message: "Not yet implemented",
});
};
markAppsAsCritical = async () => {
// TODO: Send to API once implemented
this.markingAsCritical = true;
return new Promise((resolve) => {
setTimeout(() => {
this.selectedIds.clear();
@@ -87,6 +98,7 @@ export class AllApplicationsComponent implements OnInit {
message: this.i18nService.t("appsMarkedAsCritical"),
});
resolve(true);
this.markingAsCritical = false;
}, 1000);
});
};

View File

@@ -19,7 +19,9 @@
</p>
</ng-container>
<ng-container slot="button">
<button bitButton buttonType="primary" type="button">{{ "markCriticalApps" | i18n }}</button>
<button (click)="goToAllAppsTab()" bitButton buttonType="primary" type="button">
{{ "markCriticalApps" | i18n }}
</button>
</ng-container>
</bit-no-items>
</div>

View File

@@ -1,7 +1,7 @@
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 { ActivatedRoute, Router } from "@angular/router";
import { debounceTime, map } from "rxjs";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@@ -12,6 +12,7 @@ import { HeaderModule } from "../../layouts/header/header.module";
import { SharedModule } from "../../shared";
import { PipesModule } from "../../vault/individual-vault/pipes/pipes.module";
import { AccessIntelligenceTabType } from "./access-intelligence.component";
import { applicationTableMockData } from "./application-table.mock";
@Component({
@@ -26,8 +27,10 @@ export class CriticalApplicationsComponent implements OnInit {
protected searchControl = new FormControl("", { nonNullable: true });
private destroyRef = inject(DestroyRef);
protected loading = false;
protected organizationId: string;
noItemsIcon = Icons.Security;
// MOCK DATA
protected mockData = applicationTableMockData;
protected mockAtRiskMembersCount = 0;
protected mockAtRiskAppsCount = 0;
protected mockTotalMembersCount = 0;
@@ -38,18 +41,26 @@ export class CriticalApplicationsComponent implements OnInit {
.pipe(
takeUntilDestroyed(this.destroyRef),
map(async (params) => {
// const organizationId = params.get("organizationId");
this.organizationId = params.get("organizationId");
// TODO: use organizationId to fetch data
}),
)
.subscribe();
}
goToAllAppsTab = async () => {
await this.router.navigate([`organizations/${this.organizationId}/access-intelligence`], {
queryParams: { tabIndex: AccessIntelligenceTabType.AllApps },
queryParamsHandling: "merge",
});
};
constructor(
protected i18nService: I18nService,
protected activatedRoute: ActivatedRoute,
protected router: Router,
) {
this.dataSource.data = applicationTableMockData;
this.dataSource.data = []; //applicationTableMockData;
this.searchControl.valueChanges
.pipe(debounceTime(200), takeUntilDestroyed())
.subscribe((v) => (this.dataSource.filter = v));

View File

@@ -497,7 +497,7 @@
aria-hidden="true"
[ngClass]="{
'bwi-eye': !showCardNumber,
'bwi-eye-slash': showCardNumber,
'bwi-eye-slash': showCardNumber
}"
></i>
</button>

View File

@@ -66,7 +66,7 @@ export class RoutedVaultFilterService implements OnDestroy {
collectionId: filter.collectionId ?? null,
folderId: filter.folderId ?? null,
organizationId:
filter.organizationIdParamType === "path" ? null : (filter.organizationId ?? null),
filter.organizationIdParamType === "path" ? null : filter.organizationId ?? null,
type: filter.type ?? null,
},
queryParamsHandling: "merge",

View File

@@ -25,7 +25,7 @@
>
<h3
[ngClass]="{
active: isAllVaultsSelected || isNodeSelected(headerNode),
active: isAllVaultsSelected || isNodeSelected(headerNode)
}"
>
&nbsp;{{ headerNode.node.name | i18n }}
@@ -44,7 +44,7 @@
<li
*ngFor="let f of filters"
[ngClass]="{
active: isNodeSelected(f),
active: isNodeSelected(f)
}"
class="filter-option"
>
@@ -62,7 +62,7 @@
class="bwi bwi-fw"
[ngClass]="{
'bwi-angle-right': isCollapsed(f.node),
'bwi-angle-down': !isCollapsed(f.node),
'bwi-angle-down': !isCollapsed(f.node)
}"
aria-hidden="true"
></i>

View File

@@ -3218,9 +3218,6 @@
}
}
},
"inviteSingleEmailDesc": {
"message": "You have 1 invite remaining."
},
"userUsingTwoStep": {
"message": "This user is using two-step login to protect their account."
},
@@ -9557,5 +9554,31 @@
},
"single-org-revoked-user-warning": {
"message": "Non-compliant members will be revoked. Administrators can restore members once they leave all other organizations."
},
"deleteOrganizationUser": {
"message": "Delete $NAME$",
"placeholders": {
"name": {
"content": "$1",
"example": "John Doe"
},
"description": "Title for the delete organization user dialog"
}
},
"deleteOrganizationUserWarning": {
"message": "When a member is deleted, their Bitwarden account and individual vault data will be permanently deleted. Collection data will remain in the organization. To reinstate them they must create an account and be onboarded again.",
"description": "Warning for the delete organization user dialog"
},
"organizationUserDeleted": {
"message": "Deleted $NAME$",
"placeholders": {
"name": {
"content": "$1",
"example": "John Doe"
}
}
},
"organizationUserDeletedDesc": {
"message": "The user was removed from the organization and all associated user data has been deleted."
}
}

View File

@@ -1,10 +1,9 @@
import "core-js/stable";
require("zone.js/dist/zone");
import "zone.js";
if (process.env.NODE_ENV === "production") {
// Production
} else {
// Development and test
Error["stackTraceLimit"] = Infinity;
require("zone.js/dist/long-stack-trace-zone");
}