diff --git a/src/Bootstrap/dist/css/bootstrap-theme.css b/src/Bootstrap/dist/css/bootstrap-theme.css index a4d6a8623b..8dc10abd34 100644 --- a/src/Bootstrap/dist/css/bootstrap-theme.css +++ b/src/Bootstrap/dist/css/bootstrap-theme.css @@ -916,14 +916,6 @@ img.reserved-indicator-icon { font-size: 25px; color: red; } -.page-package-details { - /* - .used-by-adjust-stars { - word-break: normal; - display: inline-block; - } - */ -} .page-package-details .no-border { border: 0; } @@ -1076,6 +1068,12 @@ img.reserved-indicator-icon { .page-package-details .used-by h3 strong { font-weight: 400; } +.page-package-details .used-by .reserved-indicator { + width: 14px; + margin-bottom: 3px; + margin-left: 2px; + vertical-align: middle; +} .page-package-details .used-by-adjust-table-head { word-break: normal; } diff --git a/src/Bootstrap/less/theme/page-display-package.less b/src/Bootstrap/less/theme/page-display-package.less index fb481019a2..7b6f396e9b 100644 --- a/src/Bootstrap/less/theme/page-display-package.less +++ b/src/Bootstrap/less/theme/page-display-package.less @@ -180,6 +180,13 @@ font-weight: 400; } } + + .reserved-indicator { + width: 14px; + margin-bottom: 3px; + margin-left: 2px; + vertical-align: middle; + } } .used-by-adjust-table-head { diff --git a/src/NuGetGallery.Services/Models/PackageDependent.cs b/src/NuGetGallery.Services/Models/PackageDependent.cs index f1402ffedc..bbd3fa3c07 100644 --- a/src/NuGetGallery.Services/Models/PackageDependent.cs +++ b/src/NuGetGallery.Services/Models/PackageDependent.cs @@ -10,9 +10,7 @@ public class PackageDependent public string Id { get; set; } public int DownloadCount { get; set; } public string Description { get; set; } - - // TODO Add verify checkmark - // https://github.com/NuGet/NuGetGallery/issues/4718 + public bool IsVerified { get; set; } } } \ No newline at end of file diff --git a/src/NuGetGallery.Services/PackageManagement/PackageService.cs b/src/NuGetGallery.Services/PackageManagement/PackageService.cs index 565ce26aff..64e8ad94b4 100644 --- a/src/NuGetGallery.Services/PackageManagement/PackageService.cs +++ b/src/NuGetGallery.Services/PackageManagement/PackageService.cs @@ -166,9 +166,9 @@ private IReadOnlyCollection GetListOfDependents(string id) join p in _entitiesContext.Packages on pd.PackageKey equals p.Key join pr in _entitiesContext.PackageRegistrations on p.PackageRegistrationKey equals pr.Key where p.IsLatestSemVer2 && pd.Id == id - group 1 by new { pr.Id, pr.DownloadCount, p.Description } into ng + group 1 by new { pr.Id, pr.DownloadCount, pr.IsVerified, p.Description } into ng orderby ng.Key.DownloadCount descending - select new PackageDependent { Id = ng.Key.Id, DownloadCount = ng.Key.DownloadCount, Description = ng.Key.Description } + select new PackageDependent { Id = ng.Key.Id, DownloadCount = ng.Key.DownloadCount, IsVerified = ng.Key.IsVerified, Description = ng.Key.Description } ).Take(packagesDisplayed).ToList(); return listPackages; diff --git a/src/NuGetGallery/Content/gallery/img/reserved-indicator-14x14.png b/src/NuGetGallery/Content/gallery/img/reserved-indicator-14x14.png new file mode 100644 index 0000000000..d0d6e7c21f Binary files /dev/null and b/src/NuGetGallery/Content/gallery/img/reserved-indicator-14x14.png differ diff --git a/src/NuGetGallery/Migrations/202006011927336_AddIndexToPackageDependencies.Designer.cs b/src/NuGetGallery/Migrations/202006011927336_AddIndexToPackageDependencies.Designer.cs new file mode 100644 index 0000000000..3c0453f765 --- /dev/null +++ b/src/NuGetGallery/Migrations/202006011927336_AddIndexToPackageDependencies.Designer.cs @@ -0,0 +1,29 @@ +// +namespace NuGetGallery.Migrations +{ + using System.CodeDom.Compiler; + using System.Data.Entity.Migrations; + using System.Data.Entity.Migrations.Infrastructure; + using System.Resources; + + [GeneratedCode("EntityFramework.Migrations", "6.4.0-preview3-19553-01")] + public sealed partial class AddIndexToPackageDependencies : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(AddIndexToPackageDependencies)); + + string IMigrationMetadata.Id + { + get { return "202006011927336_AddIndexToPackageDependencies"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/NuGetGallery/Migrations/202006011927336_AddIndexToPackageDependencies.cs b/src/NuGetGallery/Migrations/202006011927336_AddIndexToPackageDependencies.cs new file mode 100644 index 0000000000..0a36367146 --- /dev/null +++ b/src/NuGetGallery/Migrations/202006011927336_AddIndexToPackageDependencies.cs @@ -0,0 +1,31 @@ +namespace NuGetGallery.Migrations +{ + using System.Data.Entity.Migrations; + + public partial class AddIndexToPackageDependencies : DbMigration + { + public override void Up() + { + // "WITH (ONLINE = ON)" is not supported on all editions of SQL Server. We want to create the index in the background + // when we are deploying to our live environment on Azure (which supports online index creation). + // Editions: https://docs.microsoft.com/en-us/sql/t-sql/functions/serverproperty-transact-sql?view=sql-server-ver15#arguments + // We used sp_executesql because it is blocked on SQL that does not support "WITH (ONLINE = ON)". + Sql(@"IF SERVERPROPERTY ('edition') = 'SQL Azure' + BEGIN + EXECUTE sp_executesql N'CREATE NONCLUSTERED INDEX [IX_PackageDependencies_Id] ON [dbo].[PackageDependencies] ([Id]) + INCLUDE ([PackageKey]) + WITH (ONLINE = ON)' + END + ELSE + BEGIN + CREATE NONCLUSTERED INDEX [IX_PackageDependencies_Id] ON [dbo].[PackageDependencies] ([Id]) + INCLUDE ([PackageKey]) + END"); + } + + public override void Down() + { + DropIndex(table: "PackageDependencies", name: "IX_PackageDependencies_Id"); + } + } +} \ No newline at end of file diff --git a/src/NuGetGallery/Migrations/202006011927336_AddIndexToPackageDependencies.resx b/src/NuGetGallery/Migrations/202006011927336_AddIndexToPackageDependencies.resx new file mode 100644 index 0000000000..4d7b978715 --- /dev/null +++ b/src/NuGetGallery/Migrations/202006011927336_AddIndexToPackageDependencies.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 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 + + + dbo + + \ No newline at end of file diff --git a/src/NuGetGallery/NuGetGallery.csproj b/src/NuGetGallery/NuGetGallery.csproj index f028a789dc..34735971bd 100644 --- a/src/NuGetGallery/NuGetGallery.csproj +++ b/src/NuGetGallery/NuGetGallery.csproj @@ -294,6 +294,10 @@ 202004030548285_AddPackageRename.cs + + + 202006011927336_AddIndexToPackageDependencies.cs + @@ -1337,6 +1341,7 @@ + @@ -1614,6 +1619,9 @@ 202004030548285_AddPackageRename.cs + + 202006011927336_AddIndexToPackageDependencies.cs + diff --git a/src/NuGetGallery/Scripts/gallery/page-api-keys.js b/src/NuGetGallery/Scripts/gallery/page-api-keys.js index 57b149a340..284964b3c8 100644 --- a/src/NuGetGallery/Scripts/gallery/page-api-keys.js +++ b/src/NuGetGallery/Scripts/gallery/page-api-keys.js @@ -71,7 +71,7 @@ this.PackageOwners = packageOwners; this.packageViewModels = []; - + // Generic API key properties. this._SetPackageSelection = function (packages) { $.each(self.packageViewModels, function (i, m) { @@ -107,10 +107,12 @@ } this.PackageOwner(existingOwner); - } else { + + } else if (this.PackageOwners.length == 1) { this.PackageOwner(this.PackageOwners[0]); - } + } }; + this.Key = ko.observable(0); this.Type = ko.observable(); this.Value = ko.observable(); @@ -123,7 +125,7 @@ this.Scopes = ko.observableArray(); this.Packages = ko.observableArray(); this.GlobPattern = ko.observable(); - + // Properties used for the form this.PendingDescription = ko.observable(); @@ -133,6 +135,10 @@ return self.PackageOwner() && self.PackageOwner().Owner; }, this); this.PackageOwner.subscribe(function (newOwner) { + if (newOwner == null) { + return; + } + // When the package owner scope is changed, update the selected action scopes to those that are allowed on behalf of the new package owner. var isPushNewSelected = function () { return self.PushScope() === initialData.PackagePushScope; @@ -328,6 +334,10 @@ this.PackageOwner.subscribe(function (newValue) { // Initialize each package ID as a view model. This view model is used to track manual checkbox checks // and whether the glob pattern matches the ID. + if (newValue == null) { + return; + } + var packageIdToViewModel = {}; self.packageViewModels = []; $.each(newValue.PackageIds, function (i, packageId) { diff --git a/src/NuGetGallery/Views/Packages/DisplayPackage.cshtml b/src/NuGetGallery/Views/Packages/DisplayPackage.cshtml index 21aaff241a..5d950ebf95 100644 --- a/src/NuGetGallery/Views/Packages/DisplayPackage.cshtml +++ b/src/NuGetGallery/Views/Packages/DisplayPackage.cshtml @@ -574,6 +574,13 @@ @(item.Id) + @if (item.IsVerified) + { + + }
@(item.Description)
diff --git a/src/NuGetGallery/Views/Users/ApiKeys.cshtml b/src/NuGetGallery/Views/Users/ApiKeys.cshtml index f6a4977f52..31ea30085c 100644 --- a/src/NuGetGallery/Views/Users/ApiKeys.cshtml +++ b/src/NuGetGallery/Views/Users/ApiKeys.cshtml @@ -317,15 +317,19 @@ } -
+
- + options: PackageOwners, value: PackageOwner, optionsText: 'Owner', optionsCaption: 'Select an owner...' "> +
+ +
diff --git a/src/NuGetGallery/Web.config b/src/NuGetGallery/Web.config index 12d35bf6ed..08d633d2b1 100644 --- a/src/NuGetGallery/Web.config +++ b/src/NuGetGallery/Web.config @@ -654,4 +654,4 @@ - + \ No newline at end of file diff --git a/tests/NuGetGallery.Facts/Services/PackageServiceFacts.cs b/tests/NuGetGallery.Facts/Services/PackageServiceFacts.cs index e48dd74ec9..67303155e3 100644 --- a/tests/NuGetGallery.Facts/Services/PackageServiceFacts.cs +++ b/tests/NuGetGallery.Facts/Services/PackageServiceFacts.cs @@ -2142,7 +2142,7 @@ private void AddMemberToOrganization(Organization organization, User member) public class TheGetPackageDependentsMethod { [Fact] - public void ThereAreExactlyFivePackages() + public void ThereAreExactlyFivePackagesAndAllPackagesAreVerified() { string id = "foo"; int packageLimit = 5; @@ -2185,15 +2185,15 @@ public void ThereAreExactlyFivePackages() .Returns(entityContext.PackageRegistrations); var result = service.GetPackageDependents(id); + Assert.Equal(packageLimit, result.TotalPackageCount); Assert.Equal(packageLimit, result.TopPackages.Count); - var topPackage = result.TopPackages.ElementAt(0); - var runnerUpPackage = result.TopPackages.ElementAt(1); - Assert.True(topPackage.DownloadCount > runnerUpPackage.DownloadCount); + + PackageTestsWhereAllPackagesAreVerified(result, packageLimit); } [Fact] - public void ThereAreMoreThanFivePackages() + public void ThereAreMoreThanFivePackagesAndAllPackagesAreVerified() { string id = "foo"; @@ -2232,15 +2232,15 @@ public void ThereAreMoreThanFivePackages() .Returns(entityContext.PackageRegistrations); var result = service.GetPackageDependents(id); + Assert.Equal(6, result.TotalPackageCount); Assert.Equal(5, result.TopPackages.Count); - var topPackage = result.TopPackages.ElementAt(0); - var runnerUpPackage = result.TopPackages.ElementAt(1); - Assert.True(topPackage.DownloadCount > runnerUpPackage.DownloadCount); + + PackageTestsWhereAllPackagesAreVerified(result, result.TopPackages.Count); } [Fact] - public void ThereAreLessThanFivePackages() + public void ThereAreLessThanFivePackagesAndAllPackagesAreVerified() { string id = "foo"; int packageLimit = 3; @@ -2283,11 +2283,11 @@ public void ThereAreLessThanFivePackages() .Returns(entityContext.PackageRegistrations); var result = service.GetPackageDependents(id); + Assert.Equal(packageLimit, result.TotalPackageCount); Assert.Equal(packageLimit, result.TopPackages.Count); - var topPackage = result.TopPackages.ElementAt(0); - var runnerUpPackage = result.TopPackages.ElementAt(1); - Assert.True(topPackage.DownloadCount > runnerUpPackage.DownloadCount); + + PackageTestsWhereAllPackagesAreVerified(result, packageLimit); } [Fact] @@ -2356,10 +2356,157 @@ public void PackageIsNotLatestSemVer2() .Returns(entityContext.PackageRegistrations); var result = service.GetPackageDependents(id); + Assert.Equal(0, result.TotalPackageCount); Assert.Equal(0, result.TopPackages.Count); } + [Fact] + public void NoVerifiedPackages() + { + string id = "foo"; + + var context = new Mock(); + var entityContext = new FakeEntitiesContext(); + + var service = CreateService(context: context); + + var packageDependenciesList = SetupPackageDependency(id); + var packageList = SetupPackages(); + var packageRegistrationsList = SetupPackageRegistration(); + + foreach (var packageDependency in packageDependenciesList) + { + entityContext.PackageDependencies.Add(packageDependency); + } + + foreach (var package in packageList) + { + entityContext.Packages.Add(package); + } + + foreach (var packageRegistration in packageRegistrationsList) + { + packageRegistration.IsVerified = false; + entityContext.PackageRegistrations.Add(packageRegistration); + } + + context + .Setup(f => f.PackageDependencies) + .Returns(entityContext.PackageDependencies); + context + .Setup(f => f.Packages) + .Returns(entityContext.Packages); + context + .Setup(f => f.PackageRegistrations) + .Returns(entityContext.PackageRegistrations); + + var result = service.GetPackageDependents(id); + + Assert.Equal(6, result.TotalPackageCount); + Assert.Equal(5, result.TopPackages.Count); + + for (int i = 0; i < result.TopPackages.Count; i++) + { + var currentPackage = result.TopPackages.ElementAt(i); + var prevPackage = i > 0 ? result.TopPackages.ElementAt(i - 1) : null; + if (prevPackage != null) + { + Assert.True(currentPackage.DownloadCount <= prevPackage.DownloadCount); + } + Assert.False(currentPackage.IsVerified); + } + } + + [Fact] + public void MixtureOfVerifiedAndNonVerifiedPackages() + { + string id = "foo"; + int packageLimit = 5; + + var context = new Mock(); + var entityContext = new FakeEntitiesContext(); + + var service = CreateService(context: context); + + var packageDependenciesList = SetupPackageDependency(id); + var packageList = SetupPackages(); + var packageRegistrationsList = SetupPackageRegistration(); + + for (int i = 0; i < packageLimit; i++) + { + var packageDependency = packageDependenciesList[i]; + entityContext.PackageDependencies.Add(packageDependency); + } + + for (int i = 0; i < packageLimit; i++) + { + var package = packageList[i]; + entityContext.Packages.Add(package); + } + + for (int i = 0; i < packageLimit; i++) + { + var packageRegistration = packageRegistrationsList[i]; + + if (i % 2 == 0) + { + packageRegistration.IsVerified = false; + } + + entityContext.PackageRegistrations.Add(packageRegistration); + } + + context + .Setup(f => f.PackageDependencies) + .Returns(entityContext.PackageDependencies); + context + .Setup(f => f.Packages) + .Returns(entityContext.Packages); + context + .Setup(f => f.PackageRegistrations) + .Returns(entityContext.PackageRegistrations); + + var result = service.GetPackageDependents(id); + + Assert.Equal(packageLimit, result.TotalPackageCount); + Assert.Equal(packageLimit, result.TopPackages.Count); + + for (int i = 0; i < packageLimit; i++) + { + var currentPackage = result.TopPackages.ElementAt(i); + var prevPackage = i > 0 ? result.TopPackages.ElementAt(i - 1) : null; + if (prevPackage != null) + { + Assert.True(currentPackage.DownloadCount <= prevPackage.DownloadCount); + } + + if (i % 2 == 0) + { + Assert.False(currentPackage.IsVerified); + } + + else + { + Assert.True(currentPackage.IsVerified); + } + } + } + + private void PackageTestsWhereAllPackagesAreVerified(PackageDependents result, int packages) + { + for (int i = 0; i < packages; i++) + { + var currentPackage = result.TopPackages.ElementAt(i); + var prevPackage = i > 0 ? result.TopPackages.ElementAt(i - 1) : null; + if (prevPackage != null) + { + Assert.True(currentPackage.DownloadCount <= prevPackage.DownloadCount); + } + Assert.True(currentPackage.IsVerified); + } + } + private List SetupPackageDependency(string id) { var packageDependencyList = new List(); @@ -2480,41 +2627,47 @@ private List SetupPackageRegistration() { Key = 11, DownloadCount = 100, - Id = "p1" + Id = "p1", + IsVerified = true }; var prFoo2 = new PackageRegistration() { Key = 22, DownloadCount = 200, - Id = "p2" + Id = "p2", + IsVerified = true }; var prFoo3 = new PackageRegistration() { Key = 33, DownloadCount = 300, - Id = "p3" + Id = "p3", + IsVerified = true }; var prFoo4 = new PackageRegistration() { Key = 44, DownloadCount = 400, - Id = "p4" + Id = "p4", + IsVerified = true }; var prFoo5 = new PackageRegistration() { Key = 55, DownloadCount = 500, - Id = "p5" + Id = "p5", + IsVerified = true }; var prFoo6 = new PackageRegistration() { Key = 66, DownloadCount = 600, - Id = "p6" + Id = "p6", + IsVerified = true }; packageRegistrationList.Add(prFoo1);