Skip to content

Commit

Permalink
[Admin] Correct IsLatest admin panel (#9752)
Browse files Browse the repository at this point in the history
* correct islatest admin panel

* CorrectIsLatestPackages test

* ReflowPackages tests

* Reflow tests for fail scenarios

* nit
  • Loading branch information
dannyjdev authored Dec 13, 2023
1 parent 0556af1 commit 361314b
Show file tree
Hide file tree
Showing 10 changed files with 936 additions and 172 deletions.
108 changes: 108 additions & 0 deletions src/NuGetGallery/Areas/Admin/Controllers/CorrectIsLatestController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using System.Web.Mvc;
using NuGetGallery.Areas.Admin.Models;
using NuGetGallery.Areas.Admin.ViewModels;

namespace NuGetGallery.Areas.Admin.Controllers
{
public class CorrectIsLatestController : AdminControllerBase
{
private readonly IPackageService _packageService;
private readonly IEntitiesContext _entitiesContext;
private readonly IPackageFileService _packageFileService;
private readonly ITelemetryService _telemetryService;

public CorrectIsLatestController(IPackageService packageService, IEntitiesContext entitiesContext, IPackageFileService packageFileService, ITelemetryService telemetryService)
{
_packageService = packageService ?? throw new ArgumentNullException(nameof(packageService));
_entitiesContext = entitiesContext ?? throw new ArgumentNullException(nameof(entitiesContext));
_packageFileService = packageFileService ?? throw new ArgumentNullException(nameof(packageFileService));
_telemetryService = telemetryService ?? throw new ArgumentNullException(nameof(telemetryService));
}

[HttpGet]
public ActionResult Index()
{
return View();
}

[HttpGet]
public ActionResult CorrectIsLatestPackages()
{
var result = _entitiesContext
.PackageRegistrations
.Where(pr => pr.Packages.Any(p => p.IsLatest || p.IsLatestStable || p.IsLatestSemVer2 || p.IsLatestStableSemVer2))
.Select(pr => new CorrectIsLatestPackage()
{
Id = pr.Id,
Version = pr.Packages
.Where(p => p.IsLatest || p.IsLatestStable || p.IsLatestSemVer2 || p.IsLatestStableSemVer2)
.FirstOrDefault()
.Version,
IsLatestCount = pr.Packages.Where(p => p.IsLatest).Count(),
IsLatestStableCount = pr.Packages.Where(p => p.IsLatestStable).Count(),
IsLatestSemVer2Count = pr.Packages.Where(p => p.IsLatestSemVer2).Count(),
IsLatestStableSemVer2Count = pr.Packages.Where(p => p.IsLatestStableSemVer2).Count(),
HasIsLatestUnlisted = pr.Packages.Any(p =>
!p.Listed
&& (p.IsLatest
|| p.IsLatestStable
|| p.IsLatestSemVer2
|| p.IsLatestStableSemVer2))
})
.Where(pr => pr.IsLatestCount > 1
|| pr.IsLatestStableCount > 1
|| pr.IsLatestSemVer2Count > 1
|| pr.IsLatestStableSemVer2Count > 1
|| pr.HasIsLatestUnlisted)
.OrderBy(pr => pr.Id)
.ToList();

return Json(result, JsonRequestBehavior.AllowGet);
}

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> ReflowPackages(CorrectIsLatestRequest request)
{
if (request == null || request.Packages == null || request.Packages.Count == 0)
{
return Json(HttpStatusCode.BadRequest, "Packages cannot be null or empty.", JsonRequestBehavior.AllowGet);
}

var reflowPackageService = new ReflowPackageService(
_entitiesContext,
(PackageService)_packageService,
_packageFileService,
_telemetryService);

var totalPackagesReflowed = 0;
var totalPackagesFailReflowed = 0;

foreach (var package in request.Packages)
{
try
{
await reflowPackageService.ReflowAsync(package.Id, package.Version);
totalPackagesReflowed++;
}
catch (Exception ex)
{
ex.Log();
totalPackagesFailReflowed++;
}
}

var reflowedPackagesMessage = totalPackagesReflowed == 1 ? $"{totalPackagesReflowed} package reflowed" : $"{totalPackagesReflowed} packages reflowed";
var failedPackagesMessage = totalPackagesFailReflowed == 1 ? $"{totalPackagesFailReflowed} package fail reflow" : $"{totalPackagesFailReflowed} packages fail reflow";

return Json(HttpStatusCode.OK, $"{reflowedPackagesMessage}, {failedPackagesMessage}.", JsonRequestBehavior.AllowGet);
}
}
}
16 changes: 16 additions & 0 deletions src/NuGetGallery/Areas/Admin/Models/CorrectIsLatestPackage.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

namespace NuGetGallery.Areas.Admin.Models
{
public class CorrectIsLatestPackage
{
public string Id { get; set; }
public string Version { get; set; }
public int IsLatestCount { get; set; }
public int IsLatestStableCount { get; set; }
public int IsLatestSemVer2Count { get; set; }
public int IsLatestStableSemVer2Count { get; set; }
public bool HasIsLatestUnlisted { get; set; }
}
}
19 changes: 19 additions & 0 deletions src/NuGetGallery/Areas/Admin/ViewModels/CorrectIsLatestRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.


using System.Collections.Generic;

namespace NuGetGallery.Areas.Admin.ViewModels
{
public class CorrectIsLatestRequest
{
public ICollection<CorrectIsLatestPackageRequest> Packages { get; set; }
}

public class CorrectIsLatestPackageRequest
{
public string Id { get; set; }
public string Version { get; set; }
}
}
193 changes: 193 additions & 0 deletions src/NuGetGallery/Areas/Admin/Views/CorrectIsLatest/Index.cshtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
@model IReadOnlyList<NuGetGallery.Areas.Admin.Models.CorrectIsLatestPackage>
@{
ViewBag.Title = "Correct IsLatest packages";
}
@ViewHelpers.AjaxAntiForgeryToken(Html)

<section role="main" class="container main-container">
<h2>Correct IsLatest packages</h2>

<button type="submit" data-bind="click: correctIsLatestPackages">Get correct IsLatest packages</button>

<div style="display:none" data-bind="visible: loadingCorrectIsLatestPackages">
@ViewHelpers.AlertInfo(@<text><span>Loading packages...</span></text>)
</div>
<div style="display: none" data-bind="visible: emptyCorrectIsLatestPackages">
<p>No packages with incorrect IsLatest found.</p>
</div>

@using (Html.BeginForm())
{
@Html.AntiForgeryToken()
<table class="table" id="correctIsLatestPackageResults" style="display: none" data-bind="visible: correctIsLatestPackageResults().length > 0" aria-label="Search results">
<thead>
<tr>
<th><input type="checkbox" data-bind="click: toggleSelectAll, checked: selectAllState" /></th>
<th>Package</th>
<th>Version</th>
<th>IsLatestCount</th>
<th>IsLatestStableCount</th>
<th>IsLatestSemVer2Count</th>
<th>IsLatestStableSemVer2Count</th>
<th>HasIsLatestUnlisted</th>
</tr>
</thead>
<tbody data-bind="foreach: correctIsLatestPackageResults">
<tr>
<td><input type="checkbox" data-bind="checked: Selected" /></td>
<td><a href="#" data-bind="text: Id, attr: { href: $parent.generatePackageUrl($data) }"></a></td>
<td><span data-bind="text: Version"></span></td>
<td><span data-bind="text: IsLatestCount"></span></td>
<td><span data-bind="text: IsLatestStableCount"></span></td>
<td><span data-bind="text: IsLatestSemVer2Count"></span></td>
<td><span data-bind="text: IsLatestStableSemVer2Count"></span></td>
<td><span data-bind="text: HasIsLatestUnlisted"></span></td>
</tr>
</tbody>
</table>
<div style="display:none" data-bind="visible: reflowPackagesSuccessful">
@ViewHelpers.Alert(@<text><span data-bind="text: reflowPackagesSuccessful"></span></text>, "info", "Info")
</div>
<div style="display:none" data-bind="visible: loadingReflowPackages">
@ViewHelpers.AlertInfo(@<text><span>The packages are being reflowed. It may take a while for this change to propagate through our system.</span></text>)
</div>
<div style="display:none" data-bind="visible: errorReflowPackages">
@ViewHelpers.AlertDanger(@<text><span data-bind="text: errorReflowPackages"></span></text>)
</div>
<div class="form-group" data-bind="visible: arePackagesSelected()">
<input type="submit" class="btn btn-danger form-control" data-bind="click: reflowPackages" value="Reflow packages" />
</div>
}
</section>

@section BottomScripts {
<script>
$(document).ready(function() {
var viewModel = function () {
var $self = this;
this.correctIsLatestPackageResults = ko.observableArray([]);
this.selectAllState = ko.observable(false);
this.errorCorrectIsLatestPackages = ko.observable('');
this.emptyCorrectIsLatestPackages = ko.observable(false);
this.loadingCorrectIsLatestPackages = ko.observable(false);
this.loadingReflowPackages = ko.observable(false);
this.errorReflowPackages = ko.observable('');
this.reflowPackagesSuccessful = ko.observable('');
var actionUrlCorrectIsLatestPackages = '@Url.Action("CorrectIsLatestPackages", "CorrectIsLatest", new {area = "Admin"})';
var actionUrlReflowPackages = '@Url.Action("ReflowPackages", "CorrectIsLatest", new { area = "Admin" })';
this.correctIsLatestPackages = function () {
$self.correctIsLatestPackageResults.removeAll();
$self.loadingCorrectIsLatestPackages(true);
$self.emptyCorrectIsLatestPackages(false);
$.ajax({
url: actionUrlCorrectIsLatestPackages,
cache: false,
dataType: 'json',
success: function (data) {
if (data.length == 0) {
$self.emptyCorrectIsLatestPackages(true);
}
for (var i = 0; i < data.length; i++) {
data[i].Selected = ko.observable(false);
}
$self.correctIsLatestPackageResults(data);
}
})
.fail(function (jqXhr, textStatus, errorThrown) {
if (jqXhr) {
$self.errorCorrectIsLatestPackages(jqXhr.responseJSON);
}
else {
alert('Error: ' + errorThrown);
}
})
.always(function () {
$self.loadingCorrectIsLatestPackages(false);
});
};
this.reflowPackages = function (model, event) {
$self.loadingReflowPackages(true);
$self.reflowPackagesSuccessful('');
$self.errorReflowPackages('');
if (!$self.correctIsLatestPackageResults()) {
return;
}
var generateRequestData = $self.correctIsLatestPackageResults()
.filter(function (package) {
return package.Selected();
})
.map(function (package) {
return {
Id: package.Id,
Version: package.Version
}
})
var data = {
Packages: generateRequestData
}
$.ajax({
url: actionUrlReflowPackages,
cache: false,
dataType: 'json',
type: 'POST',
data: window.nuget.addAjaxAntiForgeryToken(data),
success: function (data) {
$self.reflowPackagesSuccessful(data);
$self.correctIsLatestPackages();
}
})
.fail(function (jqXhr, textStatus, errorThrown) {
if (jqXhr) {
$self.errorReflowPackages(jqXhr.responseJSON);
}
else {
alert('Error: ' + errorThrown);
}
})
.always(function () {
$self.loadingReflowPackages(false);
});;
};
this.generatePackageUrl = function (result) {
return '/packages/' + result.Id + '/' + result.Version;
};
this.toggleSelectAll = function (e) {
$self.selectAllState(!$self.selectAllState());
return true;
};
this.arePackagesSelected = ko.computed(function() {
for (var i = 0; i < $self.correctIsLatestPackageResults().length; i++) {
if ($self.correctIsLatestPackageResults()[i].Selected()) {
return true;
}
}
return false;
});
this.selectAllState.subscribe(function() {
var state = $self.selectAllState();
ko.utils.arrayForEach($self.correctIsLatestPackageResults(), function(result) {
result.Selected(state);
});
});
};
ko.applyBindings(new viewModel(), $('.main-container').get(0));
});
</script>
}
11 changes: 11 additions & 0 deletions src/NuGetGallery/Areas/Admin/Views/Home/Index.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,17 @@
Transfer popularity from one package to another.
</p>
</li>
<li class="col-sm-6 col-xs-12">
<h2>
<a href="@Url.Action(actionName: "Index", controllerName: "CorrectIsLatest")">
<i class="ms-Icon ms-Icon--UpdateRestore"></i>
<span>Correct IsLatest</span>
</a>
</h2>
<p class="text-muted">
Correct IsLatest state from packages.
</p>
</li>
</ul>
</div>
</section>
6 changes: 6 additions & 0 deletions src/NuGetGallery/NuGetGallery.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@
<Compile Include="Areas\Admin\Controllers\DeleteAccountController.cs" />
<Compile Include="Areas\Admin\Controllers\DeleteController.cs" />
<Compile Include="Areas\Admin\Controllers\FeaturesController.cs" />
<Compile Include="Areas\Admin\Controllers\CorrectIsLatestController.cs" />
<Compile Include="Areas\Admin\Controllers\LockUserController.cs" />
<Compile Include="Areas\Admin\Controllers\LockPackageController.cs" />
<Compile Include="Areas\Admin\Controllers\PasswordAuthenticationController.cs" />
Expand Down Expand Up @@ -184,7 +185,9 @@
<Compile Include="Areas\Admin\Models\ValidateUsernameResult.cs" />
<Compile Include="Areas\Admin\Services\RevalidationAdminService.cs" />
<Compile Include="Areas\Admin\Services\ValidationAdminService.cs" />
<Compile Include="Areas\Admin\ViewModels\CorrectIsLatestRequest.cs" />
<Compile Include="Areas\Admin\ViewModels\ExceptionEmailListViewModel.cs" />
<Compile Include="Areas\Admin\Models\CorrectIsLatestPackage.cs" />
<Compile Include="Areas\Admin\ViewModels\PopularityTransferViewModel.cs" />
<Compile Include="Areas\Admin\ViewModels\UserCredential.cs" />
<Compile Include="Areas\Admin\ViewModels\UserCredentialSearchResult.cs" />
Expand Down Expand Up @@ -2334,6 +2337,9 @@
<ItemGroup>
<Content Include="Areas\Admin\Views\PopularityTransfer\Index.cshtml" />
</ItemGroup>
<ItemGroup>
<Content Include="Areas\Admin\Views\CorrectIsLatest\Index.cshtml" />
</ItemGroup>
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">10.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
Expand Down
Loading

0 comments on commit 361314b

Please sign in to comment.