mirror of
https://github.com/bitwarden/browser.git
synced 2024-11-25 12:15:18 +01:00
[AC-2485] Add redirects to clients components based on FF and provider status (#8839)
* Add provider clients redirects based on FF and provider status * Fixing broken test
This commit is contained in:
parent
e516eec200
commit
cbf7c292f3
@ -0,0 +1,130 @@
|
|||||||
|
import { SelectionModel } from "@angular/cdk/collections";
|
||||||
|
import { Directive, OnDestroy, OnInit } from "@angular/core";
|
||||||
|
import { ActivatedRoute } from "@angular/router";
|
||||||
|
import { BehaviorSubject, from, Subject, switchMap } from "rxjs";
|
||||||
|
import { first, takeUntil } from "rxjs/operators";
|
||||||
|
|
||||||
|
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||||
|
import { ProviderOrganizationOrganizationDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-organization.response";
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||||
|
import { DialogService, TableDataSource, ToastService } from "@bitwarden/components";
|
||||||
|
|
||||||
|
import { WebProviderService } from "../services/web-provider.service";
|
||||||
|
|
||||||
|
@Directive()
|
||||||
|
export abstract class BaseClientsComponent implements OnInit, OnDestroy {
|
||||||
|
protected destroy$ = new Subject<void>();
|
||||||
|
|
||||||
|
private searchText$ = new BehaviorSubject<string>("");
|
||||||
|
|
||||||
|
get searchText() {
|
||||||
|
return this.searchText$.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
set searchText(value: string) {
|
||||||
|
this.searchText$.next(value);
|
||||||
|
this.selection.clear();
|
||||||
|
this.dataSource.filter = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private searching = false;
|
||||||
|
protected scrolled = false;
|
||||||
|
protected pageSize = 100;
|
||||||
|
private pagedClientsCount = 0;
|
||||||
|
protected selection = new SelectionModel<string>(true, []);
|
||||||
|
|
||||||
|
protected clients: ProviderOrganizationOrganizationDetailsResponse[];
|
||||||
|
protected pagedClients: ProviderOrganizationOrganizationDetailsResponse[];
|
||||||
|
protected dataSource = new TableDataSource<ProviderOrganizationOrganizationDetailsResponse>();
|
||||||
|
|
||||||
|
abstract providerId: string;
|
||||||
|
|
||||||
|
protected constructor(
|
||||||
|
protected activatedRoute: ActivatedRoute,
|
||||||
|
protected dialogService: DialogService,
|
||||||
|
private i18nService: I18nService,
|
||||||
|
private searchService: SearchService,
|
||||||
|
private toastService: ToastService,
|
||||||
|
private validationService: ValidationService,
|
||||||
|
private webProviderService: WebProviderService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
abstract load(): Promise<void>;
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.activatedRoute.queryParams
|
||||||
|
.pipe(first(), takeUntil(this.destroy$))
|
||||||
|
.subscribe((queryParams) => {
|
||||||
|
this.searchText = queryParams.search;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.searchText$
|
||||||
|
.pipe(
|
||||||
|
switchMap((searchText) => from(this.searchService.isSearchable(searchText))),
|
||||||
|
takeUntil(this.destroy$),
|
||||||
|
)
|
||||||
|
.subscribe((isSearchable) => {
|
||||||
|
this.searching = isSearchable;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
this.destroy$.next();
|
||||||
|
this.destroy$.complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
isPaging() {
|
||||||
|
if (this.searching && this.scrolled) {
|
||||||
|
this.resetPaging();
|
||||||
|
}
|
||||||
|
return !this.searching && this.clients && this.clients.length > this.pageSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
resetPaging() {
|
||||||
|
this.pagedClients = [];
|
||||||
|
this.loadMore();
|
||||||
|
}
|
||||||
|
|
||||||
|
loadMore() {
|
||||||
|
if (!this.clients || this.clients.length <= this.pageSize) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const pagedLength = this.pagedClients.length;
|
||||||
|
let pagedSize = this.pageSize;
|
||||||
|
if (pagedLength === 0 && this.pagedClientsCount > this.pageSize) {
|
||||||
|
pagedSize = this.pagedClientsCount;
|
||||||
|
}
|
||||||
|
if (this.clients.length > pagedLength) {
|
||||||
|
this.pagedClients = this.pagedClients.concat(
|
||||||
|
this.clients.slice(pagedLength, pagedLength + pagedSize),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.pagedClientsCount = this.pagedClients.length;
|
||||||
|
this.scrolled = this.pagedClients.length > this.pageSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
async remove(organization: ProviderOrganizationOrganizationDetailsResponse) {
|
||||||
|
const confirmed = await this.dialogService.openSimpleDialog({
|
||||||
|
title: organization.organizationName,
|
||||||
|
content: { key: "detachOrganizationConfirmation" },
|
||||||
|
type: "warning",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.webProviderService.detachOrganization(this.providerId, organization.id);
|
||||||
|
this.toastService.showToast({
|
||||||
|
variant: "success",
|
||||||
|
title: null,
|
||||||
|
message: this.i18nService.t("detachedOrganization", organization.organizationName),
|
||||||
|
});
|
||||||
|
await this.load();
|
||||||
|
} catch (e) {
|
||||||
|
this.validationService.showError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,29 +1,26 @@
|
|||||||
import { Component, OnInit } from "@angular/core";
|
import { Component } from "@angular/core";
|
||||||
import { ActivatedRoute, Router } from "@angular/router";
|
import { ActivatedRoute, Router } from "@angular/router";
|
||||||
import { BehaviorSubject, Subject, firstValueFrom, from } from "rxjs";
|
import { combineLatest, firstValueFrom, from } from "rxjs";
|
||||||
import { first, switchMap, takeUntil } from "rxjs/operators";
|
import { concatMap, switchMap, takeUntil } from "rxjs/operators";
|
||||||
|
|
||||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service";
|
import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service";
|
||||||
import { ProviderUserType } from "@bitwarden/common/admin-console/enums";
|
import { ProviderStatusType, ProviderUserType } from "@bitwarden/common/admin-console/enums";
|
||||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||||
import { ProviderOrganizationOrganizationDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-organization.response";
|
|
||||||
import { PlanType } from "@bitwarden/common/billing/enums";
|
import { PlanType } from "@bitwarden/common/billing/enums";
|
||||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
|
||||||
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||||
import { DialogService } from "@bitwarden/components";
|
import { DialogService, ToastService } from "@bitwarden/components";
|
||||||
|
|
||||||
import { WebProviderService } from "../services/web-provider.service";
|
import { WebProviderService } from "../services/web-provider.service";
|
||||||
|
|
||||||
import { AddOrganizationComponent } from "./add-organization.component";
|
import { AddOrganizationComponent } from "./add-organization.component";
|
||||||
|
import { BaseClientsComponent } from "./base-clients.component";
|
||||||
|
|
||||||
const DisallowedPlanTypes = [
|
const DisallowedPlanTypes = [
|
||||||
PlanType.Free,
|
PlanType.Free,
|
||||||
@ -36,90 +33,76 @@ const DisallowedPlanTypes = [
|
|||||||
@Component({
|
@Component({
|
||||||
templateUrl: "clients.component.html",
|
templateUrl: "clients.component.html",
|
||||||
})
|
})
|
||||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
export class ClientsComponent extends BaseClientsComponent {
|
||||||
export class ClientsComponent implements OnInit {
|
|
||||||
providerId: string;
|
providerId: string;
|
||||||
addableOrganizations: Organization[];
|
addableOrganizations: Organization[];
|
||||||
loading = true;
|
loading = true;
|
||||||
manageOrganizations = false;
|
manageOrganizations = false;
|
||||||
showAddExisting = false;
|
showAddExisting = false;
|
||||||
|
|
||||||
clients: ProviderOrganizationOrganizationDetailsResponse[];
|
protected consolidatedBillingEnabled$ = this.configService.getFeatureFlag$(
|
||||||
pagedClients: ProviderOrganizationOrganizationDetailsResponse[];
|
|
||||||
|
|
||||||
protected didScroll = false;
|
|
||||||
protected pageSize = 100;
|
|
||||||
protected actionPromise: Promise<unknown>;
|
|
||||||
private pagedClientsCount = 0;
|
|
||||||
|
|
||||||
protected enableConsolidatedBilling$ = this.configService.getFeatureFlag$(
|
|
||||||
FeatureFlag.EnableConsolidatedBilling,
|
FeatureFlag.EnableConsolidatedBilling,
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
private destroy$ = new Subject<void>();
|
|
||||||
private _searchText$ = new BehaviorSubject<string>("");
|
|
||||||
private isSearching: boolean = false;
|
|
||||||
|
|
||||||
get searchText() {
|
|
||||||
return this._searchText$.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
set searchText(value: string) {
|
|
||||||
this._searchText$.next(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private route: ActivatedRoute,
|
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private providerService: ProviderService,
|
private providerService: ProviderService,
|
||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
private searchService: SearchService,
|
|
||||||
private platformUtilsService: PlatformUtilsService,
|
|
||||||
private i18nService: I18nService,
|
|
||||||
private validationService: ValidationService,
|
|
||||||
private webProviderService: WebProviderService,
|
|
||||||
private logService: LogService,
|
|
||||||
private modalService: ModalService,
|
|
||||||
private organizationService: OrganizationService,
|
private organizationService: OrganizationService,
|
||||||
private organizationApiService: OrganizationApiServiceAbstraction,
|
private organizationApiService: OrganizationApiServiceAbstraction,
|
||||||
private dialogService: DialogService,
|
|
||||||
private configService: ConfigService,
|
private configService: ConfigService,
|
||||||
) {}
|
activatedRoute: ActivatedRoute,
|
||||||
|
dialogService: DialogService,
|
||||||
|
i18nService: I18nService,
|
||||||
|
searchService: SearchService,
|
||||||
|
toastService: ToastService,
|
||||||
|
validationService: ValidationService,
|
||||||
|
webProviderService: WebProviderService,
|
||||||
|
) {
|
||||||
|
super(
|
||||||
|
activatedRoute,
|
||||||
|
dialogService,
|
||||||
|
i18nService,
|
||||||
|
searchService,
|
||||||
|
toastService,
|
||||||
|
validationService,
|
||||||
|
webProviderService,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async ngOnInit() {
|
ngOnInit() {
|
||||||
const enableConsolidatedBilling = await firstValueFrom(this.enableConsolidatedBilling$);
|
this.activatedRoute.parent.params
|
||||||
|
.pipe(
|
||||||
if (enableConsolidatedBilling) {
|
switchMap((params) => {
|
||||||
await this.router.navigate(["../manage-client-organizations"], { relativeTo: this.route });
|
this.providerId = params.providerId;
|
||||||
} else {
|
return combineLatest([
|
||||||
this.route.parent.params
|
this.providerService.get(this.providerId),
|
||||||
.pipe(
|
this.consolidatedBillingEnabled$,
|
||||||
switchMap((params) => {
|
]).pipe(
|
||||||
this.providerId = params.providerId;
|
concatMap(([provider, consolidatedBillingEnabled]) => {
|
||||||
return from(this.load());
|
if (
|
||||||
}),
|
consolidatedBillingEnabled &&
|
||||||
takeUntil(this.destroy$),
|
provider.providerStatus === ProviderStatusType.Billable
|
||||||
)
|
) {
|
||||||
.subscribe();
|
return from(
|
||||||
|
this.router.navigate(["../manage-client-organizations"], {
|
||||||
this.route.queryParams.pipe(first(), takeUntil(this.destroy$)).subscribe((qParams) => {
|
relativeTo: this.activatedRoute,
|
||||||
this.searchText = qParams.search;
|
}),
|
||||||
});
|
);
|
||||||
|
} else {
|
||||||
this._searchText$
|
return from(this.load());
|
||||||
.pipe(
|
}
|
||||||
switchMap((searchText) => from(this.searchService.isSearchable(searchText))),
|
}),
|
||||||
takeUntil(this.destroy$),
|
);
|
||||||
)
|
}),
|
||||||
.subscribe((isSearchable) => {
|
takeUntil(this.destroy$),
|
||||||
this.isSearching = isSearchable;
|
)
|
||||||
});
|
.subscribe();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
this.destroy$.next();
|
super.ngOnDestroy();
|
||||||
this.destroy$.complete();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async load() {
|
async load() {
|
||||||
@ -141,37 +124,6 @@ export class ClientsComponent implements OnInit {
|
|||||||
this.loading = false;
|
this.loading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
isPaging() {
|
|
||||||
const searching = this.isSearching;
|
|
||||||
if (searching && this.didScroll) {
|
|
||||||
this.resetPaging();
|
|
||||||
}
|
|
||||||
return !searching && this.clients && this.clients.length > this.pageSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
resetPaging() {
|
|
||||||
this.pagedClients = [];
|
|
||||||
this.loadMore();
|
|
||||||
}
|
|
||||||
|
|
||||||
loadMore() {
|
|
||||||
if (!this.clients || this.clients.length <= this.pageSize) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const pagedLength = this.pagedClients.length;
|
|
||||||
let pagedSize = this.pageSize;
|
|
||||||
if (pagedLength === 0 && this.pagedClientsCount > this.pageSize) {
|
|
||||||
pagedSize = this.pagedClientsCount;
|
|
||||||
}
|
|
||||||
if (this.clients.length > pagedLength) {
|
|
||||||
this.pagedClients = this.pagedClients.concat(
|
|
||||||
this.clients.slice(pagedLength, pagedLength + pagedSize),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
this.pagedClientsCount = this.pagedClients.length;
|
|
||||||
this.didScroll = this.pagedClients.length > this.pageSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
async addExistingOrganization() {
|
async addExistingOrganization() {
|
||||||
const dialogRef = AddOrganizationComponent.open(this.dialogService, {
|
const dialogRef = AddOrganizationComponent.open(this.dialogService, {
|
||||||
providerId: this.providerId,
|
providerId: this.providerId,
|
||||||
@ -182,33 +134,4 @@ export class ClientsComponent implements OnInit {
|
|||||||
await this.load();
|
await this.load();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async remove(organization: ProviderOrganizationOrganizationDetailsResponse) {
|
|
||||||
const confirmed = await this.dialogService.openSimpleDialog({
|
|
||||||
title: organization.organizationName,
|
|
||||||
content: { key: "detachOrganizationConfirmation" },
|
|
||||||
type: "warning",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!confirmed) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.actionPromise = this.webProviderService.detachOrganization(
|
|
||||||
this.providerId,
|
|
||||||
organization.id,
|
|
||||||
);
|
|
||||||
try {
|
|
||||||
await this.actionPromise;
|
|
||||||
this.platformUtilsService.showToast(
|
|
||||||
"success",
|
|
||||||
null,
|
|
||||||
this.i18nService.t("detachedOrganization", organization.organizationName),
|
|
||||||
);
|
|
||||||
await this.load();
|
|
||||||
} catch (e) {
|
|
||||||
this.validationService.showError(e);
|
|
||||||
}
|
|
||||||
this.actionPromise = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,12 @@
|
|||||||
<bit-nav-item
|
<bit-nav-item
|
||||||
icon="bwi-bank"
|
icon="bwi-bank"
|
||||||
[text]="'clients' | i18n"
|
[text]="'clients' | i18n"
|
||||||
[route]="(enableConsolidatedBilling$ | async) ? 'manage-client-organizations' : 'clients'"
|
[route]="
|
||||||
|
(enableConsolidatedBilling$ | async) &&
|
||||||
|
provider.providerStatus === ProviderStatusType.Billable
|
||||||
|
? 'manage-client-organizations'
|
||||||
|
: 'clients'
|
||||||
|
"
|
||||||
></bit-nav-item>
|
></bit-nav-item>
|
||||||
<bit-nav-group icon="bwi-sliders" [text]="'manage' | i18n" route="manage" *ngIf="showManageTab">
|
<bit-nav-group icon="bwi-sliders" [text]="'manage' | i18n" route="manage" *ngIf="showManageTab">
|
||||||
<bit-nav-item
|
<bit-nav-item
|
||||||
|
@ -4,6 +4,7 @@ import { ActivatedRoute, RouterModule } from "@angular/router";
|
|||||||
|
|
||||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service";
|
import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service";
|
||||||
|
import { ProviderStatusType } from "@bitwarden/common/admin-console/enums";
|
||||||
import { Provider } from "@bitwarden/common/admin-console/models/domain/provider";
|
import { Provider } from "@bitwarden/common/admin-console/models/domain/provider";
|
||||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||||
@ -83,4 +84,6 @@ export class ProvidersLayoutComponent {
|
|||||||
return "manage/events";
|
return "manage/events";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected readonly ProviderStatusType = ProviderStatusType;
|
||||||
}
|
}
|
||||||
|
@ -11,14 +11,7 @@
|
|||||||
<bit-label>
|
<bit-label>
|
||||||
{{ "assignedSeats" | i18n }}
|
{{ "assignedSeats" | i18n }}
|
||||||
</bit-label>
|
</bit-label>
|
||||||
<input
|
<input id="assignedSeats" type="number" bitInput required [(ngModel)]="assignedSeats" />
|
||||||
id="assignedSeats"
|
|
||||||
type="number"
|
|
||||||
appAutoFocus
|
|
||||||
bitInput
|
|
||||||
required
|
|
||||||
[(ngModel)]="assignedSeats"
|
|
||||||
/>
|
|
||||||
</bit-form-field>
|
</bit-form-field>
|
||||||
<ng-container *ngIf="remainingOpenSeats > 0">
|
<ng-container *ngIf="remainingOpenSeats > 0">
|
||||||
<p>
|
<p>
|
||||||
|
@ -1,21 +1,23 @@
|
|||||||
import { SelectionModel } from "@angular/cdk/collections";
|
import { Component } from "@angular/core";
|
||||||
import { Component, OnDestroy, OnInit } from "@angular/core";
|
import { ActivatedRoute, Router } from "@angular/router";
|
||||||
import { ActivatedRoute } from "@angular/router";
|
import { combineLatest, firstValueFrom, from, lastValueFrom } from "rxjs";
|
||||||
import { BehaviorSubject, firstValueFrom, from, lastValueFrom, Subject } from "rxjs";
|
import { concatMap, switchMap, takeUntil } from "rxjs/operators";
|
||||||
import { first, switchMap, takeUntil } from "rxjs/operators";
|
|
||||||
|
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||||
import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service";
|
import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service";
|
||||||
import { ProviderUserType } from "@bitwarden/common/admin-console/enums";
|
import { ProviderStatusType, ProviderUserType } from "@bitwarden/common/admin-console/enums";
|
||||||
|
import { Provider } from "@bitwarden/common/admin-console/models/domain/provider";
|
||||||
import { ProviderOrganizationOrganizationDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-organization.response";
|
import { ProviderOrganizationOrganizationDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-organization.response";
|
||||||
import { BillingApiServiceAbstraction as BillingApiService } from "@bitwarden/common/billing/abstractions/billilng-api.service.abstraction";
|
import { BillingApiServiceAbstraction as BillingApiService } from "@bitwarden/common/billing/abstractions/billilng-api.service.abstraction";
|
||||||
import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response";
|
import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response";
|
||||||
|
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||||
|
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
|
||||||
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||||
import { DialogService, TableDataSource } from "@bitwarden/components";
|
import { DialogService, ToastService } from "@bitwarden/components";
|
||||||
|
|
||||||
|
import { BaseClientsComponent } from "../../../admin-console/providers/clients/base-clients.component";
|
||||||
import { WebProviderService } from "../../../admin-console/providers/services/web-provider.service";
|
import { WebProviderService } from "../../../admin-console/providers/services/web-provider.service";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -27,127 +29,91 @@ import { ManageClientOrganizationSubscriptionComponent } from "./manage-client-o
|
|||||||
@Component({
|
@Component({
|
||||||
templateUrl: "manage-client-organizations.component.html",
|
templateUrl: "manage-client-organizations.component.html",
|
||||||
})
|
})
|
||||||
|
export class ManageClientOrganizationsComponent extends BaseClientsComponent {
|
||||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
|
||||||
export class ManageClientOrganizationsComponent implements OnInit, OnDestroy {
|
|
||||||
providerId: string;
|
providerId: string;
|
||||||
|
provider: Provider;
|
||||||
|
|
||||||
loading = true;
|
loading = true;
|
||||||
manageOrganizations = false;
|
manageOrganizations = false;
|
||||||
|
|
||||||
private destroy$ = new Subject<void>();
|
private consolidatedBillingEnabled$ = this.configService.getFeatureFlag$(
|
||||||
private _searchText$ = new BehaviorSubject<string>("");
|
FeatureFlag.EnableConsolidatedBilling,
|
||||||
private isSearching: boolean = false;
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
get searchText() {
|
|
||||||
return this._searchText$.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
set searchText(search: string) {
|
|
||||||
this._searchText$.value;
|
|
||||||
|
|
||||||
this.selection.clear();
|
|
||||||
this.dataSource.filter = search;
|
|
||||||
}
|
|
||||||
|
|
||||||
clients: ProviderOrganizationOrganizationDetailsResponse[];
|
|
||||||
pagedClients: ProviderOrganizationOrganizationDetailsResponse[];
|
|
||||||
|
|
||||||
protected didScroll = false;
|
|
||||||
protected pageSize = 100;
|
|
||||||
protected actionPromise: Promise<unknown>;
|
|
||||||
private pagedClientsCount = 0;
|
|
||||||
selection = new SelectionModel<string>(true, []);
|
|
||||||
protected dataSource = new TableDataSource<ProviderOrganizationOrganizationDetailsResponse>();
|
|
||||||
protected plans: PlanResponse[];
|
protected plans: PlanResponse[];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private route: ActivatedRoute,
|
|
||||||
private providerService: ProviderService,
|
|
||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
private searchService: SearchService,
|
|
||||||
private platformUtilsService: PlatformUtilsService,
|
|
||||||
private i18nService: I18nService,
|
|
||||||
private validationService: ValidationService,
|
|
||||||
private webProviderService: WebProviderService,
|
|
||||||
private dialogService: DialogService,
|
|
||||||
private billingApiService: BillingApiService,
|
private billingApiService: BillingApiService,
|
||||||
) {}
|
private configService: ConfigService,
|
||||||
|
private providerService: ProviderService,
|
||||||
async ngOnInit() {
|
private router: Router,
|
||||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
activatedRoute: ActivatedRoute,
|
||||||
this.route.parent.params.subscribe(async (params) => {
|
dialogService: DialogService,
|
||||||
this.providerId = params.providerId;
|
i18nService: I18nService,
|
||||||
|
searchService: SearchService,
|
||||||
await this.load();
|
toastService: ToastService,
|
||||||
|
validationService: ValidationService,
|
||||||
/* eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe, rxjs/no-nested-subscribe */
|
webProviderService: WebProviderService,
|
||||||
this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
|
) {
|
||||||
this.searchText = qParams.search;
|
super(
|
||||||
});
|
activatedRoute,
|
||||||
});
|
dialogService,
|
||||||
|
i18nService,
|
||||||
this._searchText$
|
searchService,
|
||||||
.pipe(
|
toastService,
|
||||||
switchMap((searchText) => from(this.searchService.isSearchable(searchText))),
|
validationService,
|
||||||
takeUntil(this.destroy$),
|
webProviderService,
|
||||||
)
|
);
|
||||||
.subscribe((isSearchable) => {
|
|
||||||
this.isSearching = isSearchable;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnInit() {
|
||||||
this.destroy$.next();
|
this.activatedRoute.parent.params
|
||||||
this.destroy$.complete();
|
.pipe(
|
||||||
|
switchMap((params) => {
|
||||||
|
this.providerId = params.providerId;
|
||||||
|
return combineLatest([
|
||||||
|
this.providerService.get(this.providerId),
|
||||||
|
this.consolidatedBillingEnabled$,
|
||||||
|
]).pipe(
|
||||||
|
concatMap(([provider, consolidatedBillingEnabled]) => {
|
||||||
|
if (
|
||||||
|
!consolidatedBillingEnabled ||
|
||||||
|
provider.providerStatus !== ProviderStatusType.Billable
|
||||||
|
) {
|
||||||
|
return from(
|
||||||
|
this.router.navigate(["../clients"], {
|
||||||
|
relativeTo: this.activatedRoute,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.provider = provider;
|
||||||
|
this.manageOrganizations = this.provider.type === ProviderUserType.ProviderAdmin;
|
||||||
|
return from(this.load());
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
takeUntil(this.destroy$),
|
||||||
|
)
|
||||||
|
.subscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
super.ngOnDestroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
async load() {
|
async load() {
|
||||||
const clientsResponse = await this.apiService.getProviderClients(this.providerId);
|
this.clients = (await this.apiService.getProviderClients(this.providerId)).data;
|
||||||
this.clients =
|
|
||||||
clientsResponse.data != null && clientsResponse.data.length > 0 ? clientsResponse.data : [];
|
|
||||||
this.dataSource.data = this.clients;
|
|
||||||
this.manageOrganizations =
|
|
||||||
(await this.providerService.get(this.providerId)).type === ProviderUserType.ProviderAdmin;
|
|
||||||
|
|
||||||
const plansResponse = await this.billingApiService.getPlans();
|
this.dataSource.data = this.clients;
|
||||||
this.plans = plansResponse.data;
|
|
||||||
|
this.plans = (await this.billingApiService.getPlans()).data;
|
||||||
|
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
isPaging() {
|
|
||||||
const searching = this.isSearching;
|
|
||||||
if (searching && this.didScroll) {
|
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
this.resetPaging();
|
|
||||||
}
|
|
||||||
return !searching && this.clients && this.clients.length > this.pageSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
async resetPaging() {
|
|
||||||
this.pagedClients = [];
|
|
||||||
this.loadMore();
|
|
||||||
}
|
|
||||||
|
|
||||||
loadMore() {
|
|
||||||
if (!this.clients || this.clients.length <= this.pageSize) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const pagedLength = this.pagedClients.length;
|
|
||||||
let pagedSize = this.pageSize;
|
|
||||||
if (pagedLength === 0 && this.pagedClientsCount > this.pageSize) {
|
|
||||||
pagedSize = this.pagedClientsCount;
|
|
||||||
}
|
|
||||||
if (this.clients.length > pagedLength) {
|
|
||||||
this.pagedClients = this.pagedClients.concat(
|
|
||||||
this.clients.slice(pagedLength, pagedLength + pagedSize),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
this.pagedClientsCount = this.pagedClients.length;
|
|
||||||
this.didScroll = this.pagedClients.length > this.pageSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
async manageSubscription(organization: ProviderOrganizationOrganizationDetailsResponse) {
|
async manageSubscription(organization: ProviderOrganizationOrganizationDetailsResponse) {
|
||||||
if (organization == null) {
|
if (organization == null) {
|
||||||
return;
|
return;
|
||||||
@ -161,35 +127,6 @@ export class ManageClientOrganizationsComponent implements OnInit, OnDestroy {
|
|||||||
await this.load();
|
await this.load();
|
||||||
}
|
}
|
||||||
|
|
||||||
async remove(organization: ProviderOrganizationOrganizationDetailsResponse) {
|
|
||||||
const confirmed = await this.dialogService.openSimpleDialog({
|
|
||||||
title: organization.organizationName,
|
|
||||||
content: { key: "detachOrganizationConfirmation" },
|
|
||||||
type: "warning",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!confirmed) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.actionPromise = this.webProviderService.detachOrganization(
|
|
||||||
this.providerId,
|
|
||||||
organization.id,
|
|
||||||
);
|
|
||||||
try {
|
|
||||||
await this.actionPromise;
|
|
||||||
this.platformUtilsService.showToast(
|
|
||||||
"success",
|
|
||||||
null,
|
|
||||||
this.i18nService.t("detachedOrganization", organization.organizationName),
|
|
||||||
);
|
|
||||||
await this.load();
|
|
||||||
} catch (e) {
|
|
||||||
this.validationService.showError(e);
|
|
||||||
}
|
|
||||||
this.actionPromise = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
createClientOrganization = async () => {
|
createClientOrganization = async () => {
|
||||||
const reference = openCreateClientOrganizationDialog(this.dialogService, {
|
const reference = openCreateClientOrganizationDialog(this.dialogService, {
|
||||||
data: {
|
data: {
|
||||||
|
@ -7,3 +7,4 @@ export * from "./provider-type.enum";
|
|||||||
export * from "./provider-user-status-type.enum";
|
export * from "./provider-user-status-type.enum";
|
||||||
export * from "./provider-user-type.enum";
|
export * from "./provider-user-type.enum";
|
||||||
export * from "./scim-provider-type.enum";
|
export * from "./scim-provider-type.enum";
|
||||||
|
export * from "./provider-status-type.enum";
|
||||||
|
@ -0,0 +1,5 @@
|
|||||||
|
export enum ProviderStatusType {
|
||||||
|
Pending = 0,
|
||||||
|
Created = 1,
|
||||||
|
Billable = 2,
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
import { ProviderUserStatusType, ProviderUserType } from "../../enums";
|
import { ProviderStatusType, ProviderUserStatusType, ProviderUserType } from "../../enums";
|
||||||
import { ProfileProviderResponse } from "../response/profile-provider.response";
|
import { ProfileProviderResponse } from "../response/profile-provider.response";
|
||||||
|
|
||||||
export class ProviderData {
|
export class ProviderData {
|
||||||
@ -9,6 +9,7 @@ export class ProviderData {
|
|||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
userId: string;
|
userId: string;
|
||||||
useEvents: boolean;
|
useEvents: boolean;
|
||||||
|
providerStatus: ProviderStatusType;
|
||||||
|
|
||||||
constructor(response: ProfileProviderResponse) {
|
constructor(response: ProfileProviderResponse) {
|
||||||
this.id = response.id;
|
this.id = response.id;
|
||||||
@ -18,5 +19,6 @@ export class ProviderData {
|
|||||||
this.enabled = response.enabled;
|
this.enabled = response.enabled;
|
||||||
this.userId = response.userId;
|
this.userId = response.userId;
|
||||||
this.useEvents = response.useEvents;
|
this.useEvents = response.useEvents;
|
||||||
|
this.providerStatus = response.providerStatus;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { ProviderUserStatusType, ProviderUserType } from "../../enums";
|
import { ProviderStatusType, ProviderUserStatusType, ProviderUserType } from "../../enums";
|
||||||
import { ProviderData } from "../data/provider.data";
|
import { ProviderData } from "../data/provider.data";
|
||||||
|
|
||||||
export class Provider {
|
export class Provider {
|
||||||
@ -9,6 +9,7 @@ export class Provider {
|
|||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
userId: string;
|
userId: string;
|
||||||
useEvents: boolean;
|
useEvents: boolean;
|
||||||
|
providerStatus: ProviderStatusType;
|
||||||
|
|
||||||
constructor(obj?: ProviderData) {
|
constructor(obj?: ProviderData) {
|
||||||
if (obj == null) {
|
if (obj == null) {
|
||||||
@ -22,6 +23,7 @@ export class Provider {
|
|||||||
this.enabled = obj.enabled;
|
this.enabled = obj.enabled;
|
||||||
this.userId = obj.userId;
|
this.userId = obj.userId;
|
||||||
this.useEvents = obj.useEvents;
|
this.useEvents = obj.useEvents;
|
||||||
|
this.providerStatus = obj.providerStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
get canAccess() {
|
get canAccess() {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { BaseResponse } from "../../../models/response/base.response";
|
import { BaseResponse } from "../../../models/response/base.response";
|
||||||
import { ProviderUserStatusType, ProviderUserType } from "../../enums";
|
import { ProviderStatusType, ProviderUserStatusType, ProviderUserType } from "../../enums";
|
||||||
import { PermissionsApi } from "../api/permissions.api";
|
import { PermissionsApi } from "../api/permissions.api";
|
||||||
|
|
||||||
export class ProfileProviderResponse extends BaseResponse {
|
export class ProfileProviderResponse extends BaseResponse {
|
||||||
@ -12,6 +12,7 @@ export class ProfileProviderResponse extends BaseResponse {
|
|||||||
permissions: PermissionsApi;
|
permissions: PermissionsApi;
|
||||||
userId: string;
|
userId: string;
|
||||||
useEvents: boolean;
|
useEvents: boolean;
|
||||||
|
providerStatus: ProviderStatusType;
|
||||||
|
|
||||||
constructor(response: any) {
|
constructor(response: any) {
|
||||||
super(response);
|
super(response);
|
||||||
@ -24,5 +25,6 @@ export class ProfileProviderResponse extends BaseResponse {
|
|||||||
this.permissions = new PermissionsApi(this.getResponseProperty("permissions"));
|
this.permissions = new PermissionsApi(this.getResponseProperty("permissions"));
|
||||||
this.userId = this.getResponseProperty("UserId");
|
this.userId = this.getResponseProperty("UserId");
|
||||||
this.useEvents = this.getResponseProperty("UseEvents");
|
this.useEvents = this.getResponseProperty("UseEvents");
|
||||||
|
this.providerStatus = this.getResponseProperty("ProviderStatus");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ import { FakeAccountService, FakeStateProvider, mockAccountServiceWith } from ".
|
|||||||
import { FakeActiveUserState, FakeSingleUserState } from "../../../spec/fake-state";
|
import { FakeActiveUserState, FakeSingleUserState } from "../../../spec/fake-state";
|
||||||
import { Utils } from "../../platform/misc/utils";
|
import { Utils } from "../../platform/misc/utils";
|
||||||
import { UserId } from "../../types/guid";
|
import { UserId } from "../../types/guid";
|
||||||
import { ProviderUserStatusType, ProviderUserType } from "../enums";
|
import { ProviderStatusType, ProviderUserStatusType, ProviderUserType } from "../enums";
|
||||||
import { ProviderData } from "../models/data/provider.data";
|
import { ProviderData } from "../models/data/provider.data";
|
||||||
import { Provider } from "../models/domain/provider";
|
import { Provider } from "../models/domain/provider";
|
||||||
|
|
||||||
@ -64,6 +64,7 @@ describe("PROVIDERS key definition", () => {
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
userId: "string",
|
userId: "string",
|
||||||
useEvents: true,
|
useEvents: true,
|
||||||
|
providerStatus: ProviderStatusType.Pending,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const result = sut.deserializer(JSON.parse(JSON.stringify(expectedResult)));
|
const result = sut.deserializer(JSON.parse(JSON.stringify(expectedResult)));
|
||||||
|
Loading…
Reference in New Issue
Block a user