1
0
mirror of https://github.com/bitwarden/server.git synced 2025-04-05 05:00:19 -05:00

[PM-15485] Add provider plan details to provider Admin pages (#5326)

* Add Provider Plan details to Provider Admin pages

* Run dotnet format

* Thomas' feedback

* Updated code ownership

* Robert's feedback

* Thomas' feedback
This commit is contained in:
Alex Morask 2025-02-14 12:03:09 -05:00 committed by GitHub
parent 288f08da2a
commit 5709ea36f4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 128 additions and 6 deletions

View File

@ -16,7 +16,6 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Folder Include="Billing\Controllers\" /> <Folder Include="Billing\Controllers\" />
<Folder Include="Billing\Models\" />
</ItemGroup> </ItemGroup>
<Choose> <Choose>

View File

@ -235,7 +235,8 @@ public class ProvidersController : Controller
var users = await _providerUserRepository.GetManyDetailsByProviderAsync(id); var users = await _providerUserRepository.GetManyDetailsByProviderAsync(id);
var providerOrganizations = await _providerOrganizationRepository.GetManyDetailsByProviderAsync(id); var providerOrganizations = await _providerOrganizationRepository.GetManyDetailsByProviderAsync(id);
return View(new ProviderViewModel(provider, users, providerOrganizations)); var providerPlans = await _providerPlanRepository.GetByProviderId(id);
return View(new ProviderViewModel(provider, users, providerOrganizations, providerPlans.ToList()));
} }
[SelfHosted(NotSelfHostedOnly = true)] [SelfHosted(NotSelfHostedOnly = true)]

View File

@ -19,7 +19,7 @@ public class ProviderEditModel : ProviderViewModel, IValidatableObject
IEnumerable<ProviderOrganizationOrganizationDetails> organizations, IEnumerable<ProviderOrganizationOrganizationDetails> organizations,
IReadOnlyCollection<ProviderPlan> providerPlans, IReadOnlyCollection<ProviderPlan> providerPlans,
string gatewayCustomerUrl = null, string gatewayCustomerUrl = null,
string gatewaySubscriptionUrl = null) : base(provider, providerUsers, organizations) string gatewaySubscriptionUrl = null) : base(provider, providerUsers, organizations, providerPlans)
{ {
Name = provider.DisplayName(); Name = provider.DisplayName();
BusinessName = provider.DisplayBusinessName(); BusinessName = provider.DisplayBusinessName();

View File

@ -1,6 +1,9 @@
using Bit.Core.AdminConsole.Entities.Provider; using Bit.Admin.Billing.Models;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Models.Data.Provider; using Bit.Core.AdminConsole.Models.Data.Provider;
using Bit.Core.Billing.Entities;
using Bit.Core.Billing.Enums;
namespace Bit.Admin.AdminConsole.Models; namespace Bit.Admin.AdminConsole.Models;
@ -8,17 +11,57 @@ public class ProviderViewModel
{ {
public ProviderViewModel() { } public ProviderViewModel() { }
public ProviderViewModel(Provider provider, IEnumerable<ProviderUserUserDetails> providerUsers, IEnumerable<ProviderOrganizationOrganizationDetails> organizations) public ProviderViewModel(
Provider provider,
IEnumerable<ProviderUserUserDetails> providerUsers,
IEnumerable<ProviderOrganizationOrganizationDetails> organizations,
IReadOnlyCollection<ProviderPlan> providerPlans)
{ {
Provider = provider; Provider = provider;
UserCount = providerUsers.Count(); UserCount = providerUsers.Count();
ProviderAdmins = providerUsers.Where(u => u.Type == ProviderUserType.ProviderAdmin); ProviderAdmins = providerUsers.Where(u => u.Type == ProviderUserType.ProviderAdmin);
ProviderOrganizations = organizations.Where(o => o.ProviderId == provider.Id); ProviderOrganizations = organizations.Where(o => o.ProviderId == provider.Id);
if (Provider.Type == ProviderType.Msp)
{
var usedTeamsSeats = ProviderOrganizations.Where(po => po.PlanType == PlanType.TeamsMonthly)
.Sum(po => po.OccupiedSeats) ?? 0;
var teamsProviderPlan = providerPlans.FirstOrDefault(plan => plan.PlanType == PlanType.TeamsMonthly);
if (teamsProviderPlan != null && teamsProviderPlan.IsConfigured())
{
ProviderPlanViewModels.Add(new ProviderPlanViewModel("Teams (Monthly) Subscription", teamsProviderPlan, usedTeamsSeats));
}
var usedEnterpriseSeats = ProviderOrganizations.Where(po => po.PlanType == PlanType.EnterpriseMonthly)
.Sum(po => po.OccupiedSeats) ?? 0;
var enterpriseProviderPlan = providerPlans.FirstOrDefault(plan => plan.PlanType == PlanType.EnterpriseMonthly);
if (enterpriseProviderPlan != null && enterpriseProviderPlan.IsConfigured())
{
ProviderPlanViewModels.Add(new ProviderPlanViewModel("Enterprise (Monthly) Subscription", enterpriseProviderPlan, usedEnterpriseSeats));
}
}
else if (Provider.Type == ProviderType.MultiOrganizationEnterprise)
{
var usedEnterpriseSeats = ProviderOrganizations.Where(po => po.PlanType == PlanType.EnterpriseMonthly)
.Sum(po => po.OccupiedSeats).GetValueOrDefault(0);
var enterpriseProviderPlan = providerPlans.FirstOrDefault();
if (enterpriseProviderPlan != null && enterpriseProviderPlan.IsConfigured())
{
var planLabel = enterpriseProviderPlan.PlanType switch
{
PlanType.EnterpriseMonthly => "Enterprise (Monthly) Subscription",
PlanType.EnterpriseAnnually => "Enterprise (Annually) Subscription",
_ => string.Empty
};
ProviderPlanViewModels.Add(new ProviderPlanViewModel(planLabel, enterpriseProviderPlan, usedEnterpriseSeats));
}
}
} }
public int UserCount { get; set; } public int UserCount { get; set; }
public Provider Provider { get; set; } public Provider Provider { get; set; }
public IEnumerable<ProviderUserUserDetails> ProviderAdmins { get; set; } public IEnumerable<ProviderUserUserDetails> ProviderAdmins { get; set; }
public IEnumerable<ProviderOrganizationOrganizationDetails> ProviderOrganizations { get; set; } public IEnumerable<ProviderOrganizationOrganizationDetails> ProviderOrganizations { get; set; }
public List<ProviderPlanViewModel> ProviderPlanViewModels { get; set; } = [];
} }

View File

@ -17,6 +17,10 @@
<h2>Provider Information</h2> <h2>Provider Information</h2>
@await Html.PartialAsync("_ViewInformation", Model) @await Html.PartialAsync("_ViewInformation", Model)
@if (Model.ProviderPlanViewModels.Any())
{
@await Html.PartialAsync("~/Billing/Views/Providers/ProviderPlans.cshtml", Model.ProviderPlanViewModels)
}
@await Html.PartialAsync("Admins", Model) @await Html.PartialAsync("Admins", Model)
<form method="post" id="edit-form"> <form method="post" id="edit-form">
<div asp-validation-summary="All" class="alert alert-danger"></div> <div asp-validation-summary="All" class="alert alert-danger"></div>

View File

@ -7,5 +7,9 @@
<h2>Information</h2> <h2>Information</h2>
@await Html.PartialAsync("_ViewInformation", Model) @await Html.PartialAsync("_ViewInformation", Model)
@if (Model.ProviderPlanViewModels.Any())
{
@await Html.PartialAsync("ProviderPlans", Model.ProviderPlanViewModels)
}
@await Html.PartialAsync("Admins", Model) @await Html.PartialAsync("Admins", Model)
@await Html.PartialAsync("Organizations", Model) @await Html.PartialAsync("Organizations", Model)

View File

@ -0,0 +1,26 @@
using Bit.Core.Billing.Entities;
namespace Bit.Admin.Billing.Models;
public class ProviderPlanViewModel
{
public string Name { get; set; }
public int PurchasedSeats { get; set; }
public int AssignedSeats { get; set; }
public int UsedSeats { get; set; }
public int RemainingSeats { get; set; }
public ProviderPlanViewModel(
string name,
ProviderPlan providerPlan,
int usedSeats)
{
var purchasedSeats = (providerPlan.SeatMinimum ?? 0) + (providerPlan.PurchasedSeats ?? 0);
Name = name;
PurchasedSeats = purchasedSeats;
AssignedSeats = providerPlan.AllocatedSeats ?? 0;
UsedSeats = usedSeats;
RemainingSeats = purchasedSeats - AssignedSeats;
}
}

View File

@ -0,0 +1,18 @@
@model List<Bit.Admin.Billing.Models.ProviderPlanViewModel>
@foreach (var plan in Model)
{
<h2>@plan.Name</h2>
<dl class="row">
<dt class="col-sm-4 col-lg-3">Purchased Seats</dt>
<dd class="col-sm-8 col-lg-9">@plan.PurchasedSeats</dd>
<dt class="col-sm-4 col-lg-3">Assigned Seats</dt>
<dd class="col-sm-8 col-lg-9">@plan.AssignedSeats</dd>
<dt class="col-sm-4 col-lg-3">Used Seats</dt>
<dd class="col-sm-8 col-lg-9">@plan.UsedSeats</dd>
<dt class="col-sm-4 col-lg-3">Remaining Seats</dt>
<dd class="col-sm-8 col-lg-9">@plan.RemainingSeats</dd>
</dl>
}

View File

@ -1,5 +1,6 @@
using System.Net; using System.Net;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using Bit.Core.Billing.Enums;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Utilities; using Bit.Core.Utilities;
@ -23,6 +24,7 @@ public class ProviderOrganizationOrganizationDetails
public int? OccupiedSeats { get; set; } public int? OccupiedSeats { get; set; }
public int? Seats { get; set; } public int? Seats { get; set; }
public string Plan { get; set; } public string Plan { get; set; }
public PlanType PlanType { get; set; }
public OrganizationStatusType Status { get; set; } public OrganizationStatusType Status { get; set; }
/// <summary> /// <summary>

View File

@ -35,6 +35,7 @@ public class ProviderOrganizationOrganizationDetailsReadByProviderIdQuery : IQue
OccupiedSeats = x.o.OrganizationUsers.Count(ou => ou.Status >= 0), OccupiedSeats = x.o.OrganizationUsers.Count(ou => ou.Status >= 0),
Seats = x.o.Seats, Seats = x.o.Seats,
Plan = x.o.Plan, Plan = x.o.Plan,
PlanType = x.o.PlanType,
Status = x.o.Status Status = x.o.Status
}); });
} }

View File

@ -13,6 +13,7 @@ SELECT
(SELECT COUNT(1) FROM [dbo].[OrganizationUser] OU WHERE OU.OrganizationId = PO.OrganizationId AND OU.Status >= 0) OccupiedSeats, (SELECT COUNT(1) FROM [dbo].[OrganizationUser] OU WHERE OU.OrganizationId = PO.OrganizationId AND OU.Status >= 0) OccupiedSeats,
O.[Seats], O.[Seats],
O.[Plan], O.[Plan],
O.[PlanType],
O.[Status] O.[Status]
FROM FROM
[dbo].[ProviderOrganization] PO [dbo].[ProviderOrganization] PO

View File

@ -0,0 +1,23 @@
-- Add column 'PlanType'
CREATE OR AlTER VIEW [dbo].[ProviderOrganizationOrganizationDetailsView]
AS
SELECT
PO.[Id],
PO.[ProviderId],
PO.[OrganizationId],
O.[Name] OrganizationName,
PO.[Key],
PO.[Settings],
PO.[CreationDate],
PO.[RevisionDate],
(SELECT COUNT(1) FROM [dbo].[OrganizationUser] OU WHERE OU.OrganizationId = PO.OrganizationId AND OU.Status = 2) UserCount,
(SELECT COUNT(1) FROM [dbo].[OrganizationUser] OU WHERE OU.OrganizationId = PO.OrganizationId AND OU.Status >= 0) OccupiedSeats,
O.[Seats],
O.[Plan],
O.[PlanType],
O.[Status]
FROM
[dbo].[ProviderOrganization] PO
LEFT JOIN
[dbo].[Organization] O ON O.[Id] = PO.[OrganizationId]
GO