mirror of
https://github.com/bitwarden/browser
synced 2025-12-23 19:53:43 +00:00
[PM-12443] Remove paging logic from base clients component and subclasses (#12250)
* remove ngx-infinite-scroll in provider clients components. * cleanup, fix redirect * cleanup * remove function call during interpolation * remove this in template * add router guard, cleanup * cleanup * fix row height for virtual scroller
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
<app-header>
|
||||
<bit-search
|
||||
class="tw-grow"
|
||||
[formControl]="searchControl"
|
||||
[placeholder]="'search' | i18n"
|
||||
></bit-search>
|
||||
<a bitButton routerLink="create" *ngIf="manageOrganizations" buttonType="primary">
|
||||
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
|
||||
{{ "newClient" | i18n }}
|
||||
</a>
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
(click)="addExistingOrganization()"
|
||||
*ngIf="manageOrganizations && showAddExisting"
|
||||
>
|
||||
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
|
||||
{{ "addExistingOrganization" | i18n }}
|
||||
</button>
|
||||
</app-header>
|
||||
|
||||
<ng-container *ngIf="loading">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="!loading">
|
||||
<p *ngIf="dataSource.data.length < 1">{{ "noClientsInList" | i18n }}</p>
|
||||
<ng-container *ngIf="dataSource.data.length >= 1">
|
||||
<bit-table-scroll
|
||||
[dataSource]="dataSource"
|
||||
[rowSize]="53"
|
||||
class="tw-table tw-w-full table-hover table-list"
|
||||
>
|
||||
<ng-container header>
|
||||
<th bitCell colspan="2" bitSortable="organizationName">{{ "name" | i18n }}</th>
|
||||
<th bitCell bitSortable="seats">{{ "numberOfUsers" | i18n }}</th>
|
||||
<th bitCell bitSortable="plan">{{ "billingPlan" | i18n }}</th>
|
||||
<th bitCell></th>
|
||||
</ng-container>
|
||||
<ng-template bitRowDef let-row>
|
||||
<td bitCell>
|
||||
<bit-avatar [text]="row.organizationName" [id]="row.id" size="small"></bit-avatar>
|
||||
</td>
|
||||
<td bitCell>
|
||||
<a [routerLink]="['/organizations', row.organizationId]">{{ row.organizationName }}</a>
|
||||
</td>
|
||||
<td bitCell>
|
||||
<span>{{ row.userCount }}</span>
|
||||
<span *ngIf="row.seats != null"> / {{ row.seats }}</span>
|
||||
</td>
|
||||
<td bitCell>
|
||||
<span>{{ row.plan }}</span>
|
||||
</td>
|
||||
<td class="table-list-options" *ngIf="manageOrganizations">
|
||||
<div class="dropdown" appListDropdown>
|
||||
<button
|
||||
class="btn btn-outline-secondary dropdown-toggle"
|
||||
type="button"
|
||||
data-toggle="dropdown"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
appA11yTitle="{{ 'options' | i18n }}"
|
||||
>
|
||||
<i class="bwi bwi-cog bwi-lg" aria-hidden="true"></i>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right">
|
||||
<a class="dropdown-item text-danger" href="#" appStopClick (click)="remove(row)">
|
||||
<i class="bwi bwi-fw bwi-close" aria-hidden="true"></i>
|
||||
{{ "remove" | i18n }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</ng-template>
|
||||
</bit-table-scroll>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
@@ -0,0 +1,167 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { FormControl } from "@angular/forms";
|
||||
import { ActivatedRoute, Router, RouterModule } from "@angular/router";
|
||||
import { firstValueFrom, from, map } from "rxjs";
|
||||
import { debounceTime, first, switchMap } from "rxjs/operators";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
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 { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service";
|
||||
import { ProviderStatusType, ProviderUserType } from "@bitwarden/common/admin-console/enums";
|
||||
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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||
import {
|
||||
AvatarModule,
|
||||
DialogService,
|
||||
TableDataSource,
|
||||
TableModule,
|
||||
ToastService,
|
||||
} from "@bitwarden/components";
|
||||
import { SharedOrganizationModule } from "@bitwarden/web-vault/app/admin-console/organizations/shared";
|
||||
import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.module";
|
||||
|
||||
import { WebProviderService } from "../services/web-provider.service";
|
||||
|
||||
import { AddOrganizationComponent } from "./add-organization.component";
|
||||
|
||||
const DisallowedPlanTypes = [
|
||||
PlanType.Free,
|
||||
PlanType.FamiliesAnnually2019,
|
||||
PlanType.FamiliesAnnually,
|
||||
PlanType.TeamsStarter2023,
|
||||
PlanType.TeamsStarter,
|
||||
];
|
||||
|
||||
@Component({
|
||||
templateUrl: "vnext-clients.component.html",
|
||||
standalone: true,
|
||||
imports: [
|
||||
SharedOrganizationModule,
|
||||
HeaderModule,
|
||||
CommonModule,
|
||||
JslibModule,
|
||||
AvatarModule,
|
||||
RouterModule,
|
||||
TableModule,
|
||||
],
|
||||
})
|
||||
export class vNextClientsComponent {
|
||||
providerId: string;
|
||||
addableOrganizations: Organization[];
|
||||
loading = true;
|
||||
manageOrganizations = false;
|
||||
showAddExisting = false;
|
||||
dataSource: TableDataSource<ProviderOrganizationOrganizationDetailsResponse> =
|
||||
new TableDataSource();
|
||||
protected searchControl = new FormControl("", { nonNullable: true });
|
||||
|
||||
constructor(
|
||||
private router: Router,
|
||||
private providerService: ProviderService,
|
||||
private apiService: ApiService,
|
||||
private organizationService: OrganizationService,
|
||||
private organizationApiService: OrganizationApiServiceAbstraction,
|
||||
private activatedRoute: ActivatedRoute,
|
||||
private dialogService: DialogService,
|
||||
private i18nService: I18nService,
|
||||
private toastService: ToastService,
|
||||
private validationService: ValidationService,
|
||||
private webProviderService: WebProviderService,
|
||||
) {
|
||||
this.activatedRoute.queryParams.pipe(first(), takeUntilDestroyed()).subscribe((queryParams) => {
|
||||
this.searchControl.setValue(queryParams.search);
|
||||
});
|
||||
|
||||
this.activatedRoute.parent.params
|
||||
.pipe(
|
||||
switchMap((params) => {
|
||||
this.providerId = params.providerId;
|
||||
return this.providerService.get$(this.providerId).pipe(
|
||||
map((provider) => provider?.providerStatus === ProviderStatusType.Billable),
|
||||
map((isBillable) => {
|
||||
if (isBillable) {
|
||||
return from(
|
||||
this.router.navigate(["../manage-client-organizations"], {
|
||||
relativeTo: this.activatedRoute,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
return from(this.load());
|
||||
}
|
||||
}),
|
||||
);
|
||||
}),
|
||||
takeUntilDestroyed(),
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
this.searchControl.valueChanges
|
||||
.pipe(debounceTime(200), takeUntilDestroyed())
|
||||
.subscribe((searchText) => {
|
||||
this.dataSource.filter = (data) =>
|
||||
data.organizationName.toLowerCase().indexOf(searchText.toLowerCase()) > -1;
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
async load() {
|
||||
const response = await this.apiService.getProviderClients(this.providerId);
|
||||
const clients = response.data != null && response.data.length > 0 ? response.data : [];
|
||||
this.dataSource.data = clients;
|
||||
this.manageOrganizations =
|
||||
(await this.providerService.get(this.providerId)).type === ProviderUserType.ProviderAdmin;
|
||||
const candidateOrgs = (await this.organizationService.getAll()).filter(
|
||||
(o) => o.isOwner && o.providerId == null,
|
||||
);
|
||||
const allowedOrgsIds = await Promise.all(
|
||||
candidateOrgs.map((o) => this.organizationApiService.get(o.id)),
|
||||
).then((orgs) =>
|
||||
orgs.filter((o) => !DisallowedPlanTypes.includes(o.planType)).map((o) => o.id),
|
||||
);
|
||||
this.addableOrganizations = candidateOrgs.filter((o) => allowedOrgsIds.includes(o.id));
|
||||
|
||||
this.showAddExisting = this.addableOrganizations.length !== 0;
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
async addExistingOrganization() {
|
||||
const dialogRef = AddOrganizationComponent.open(this.dialogService, {
|
||||
providerId: this.providerId,
|
||||
organizations: this.addableOrganizations,
|
||||
});
|
||||
|
||||
if (await firstValueFrom(dialogRef.closed)) {
|
||||
await this.load();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,10 @@ import { NgModule } from "@angular/core";
|
||||
import { RouterModule, Routes } from "@angular/router";
|
||||
|
||||
import { authGuard } from "@bitwarden/angular/auth/guards";
|
||||
import { featureFlaggedRoute } from "@bitwarden/angular/platform/utils/feature-flagged-route";
|
||||
import { AnonLayoutWrapperComponent } from "@bitwarden/auth/angular";
|
||||
import { Provider } from "@bitwarden/common/admin-console/models/domain/provider";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { FrontendLayoutComponent } from "@bitwarden/web-vault/app/layouts/frontend-layout.component";
|
||||
import { UserLayoutComponent } from "@bitwarden/web-vault/app/layouts/user-layout.component";
|
||||
|
||||
@@ -12,10 +14,12 @@ import {
|
||||
ProviderSubscriptionComponent,
|
||||
hasConsolidatedBilling,
|
||||
ProviderBillingHistoryComponent,
|
||||
vNextManageClientsComponent,
|
||||
} from "../../billing/providers";
|
||||
|
||||
import { ClientsComponent } from "./clients/clients.component";
|
||||
import { CreateOrganizationComponent } from "./clients/create-organization.component";
|
||||
import { vNextClientsComponent } from "./clients/vnext-clients.component";
|
||||
import { providerPermissionsGuard } from "./guards/provider-permissions.guard";
|
||||
import { AcceptProviderComponent } from "./manage/accept-provider.component";
|
||||
import { EventsComponent } from "./manage/events.component";
|
||||
@@ -82,13 +86,25 @@ const routes: Routes = [
|
||||
children: [
|
||||
{ path: "", pathMatch: "full", redirectTo: "clients" },
|
||||
{ path: "clients/create", component: CreateOrganizationComponent },
|
||||
{ path: "clients", component: ClientsComponent, data: { titleId: "clients" } },
|
||||
{
|
||||
path: "manage-client-organizations",
|
||||
canActivate: [hasConsolidatedBilling],
|
||||
component: ManageClientsComponent,
|
||||
data: { titleId: "clients" },
|
||||
},
|
||||
...featureFlaggedRoute({
|
||||
defaultComponent: ClientsComponent,
|
||||
flaggedComponent: vNextClientsComponent,
|
||||
featureFlag: FeatureFlag.PM12443RemovePagingLogic,
|
||||
routeOptions: {
|
||||
path: "clients",
|
||||
data: { titleId: "clients" },
|
||||
},
|
||||
}),
|
||||
...featureFlaggedRoute({
|
||||
defaultComponent: ManageClientsComponent,
|
||||
flaggedComponent: vNextManageClientsComponent,
|
||||
featureFlag: FeatureFlag.PM12443RemovePagingLogic,
|
||||
routeOptions: {
|
||||
path: "manage-client-organizations",
|
||||
data: { titleId: "clients" },
|
||||
canActivate: [hasConsolidatedBilling],
|
||||
},
|
||||
}),
|
||||
{
|
||||
path: "manage",
|
||||
children: [
|
||||
|
||||
Reference in New Issue
Block a user