diff --git a/src/NuGetGallery/Content/Site.css b/src/NuGetGallery/Content/Site.css index 78e77706e7..97e0af77b5 100644 --- a/src/NuGetGallery/Content/Site.css +++ b/src/NuGetGallery/Content/Site.css @@ -476,6 +476,10 @@ fieldset.search { /* List Package */ +.package-list-lastupdated { + font-size: 14pt; +} + section.package { border-top: 1px solid #ccc; padding-top: 10px; diff --git a/src/NuGetGallery/Controllers/CuratedFeedsController.cs b/src/NuGetGallery/Controllers/CuratedFeedsController.cs index b4f5f21070..77a077a886 100644 --- a/src/NuGetGallery/Controllers/CuratedFeedsController.cs +++ b/src/NuGetGallery/Controllers/CuratedFeedsController.cs @@ -81,6 +81,7 @@ public virtual async Task ListPackages(string curatedFeedName, str var viewModel = new PackageListViewModel( results.Data, + results.IndexTimestampUtc, q, totalHits, page - 1, diff --git a/src/NuGetGallery/Controllers/PackagesController.cs b/src/NuGetGallery/Controllers/PackagesController.cs index ed27399c2f..bfc971aca6 100644 --- a/src/NuGetGallery/Controllers/PackagesController.cs +++ b/src/NuGetGallery/Controllers/PackagesController.cs @@ -303,6 +303,7 @@ public virtual async Task ListPackages(string q, int page = 1) var viewModel = new PackageListViewModel( results.Data, + results.IndexTimestampUtc, q, totalHits, page - 1, diff --git a/src/NuGetGallery/ExtensionMethods.cs b/src/NuGetGallery/ExtensionMethods.cs index 248875469c..c012c658f2 100644 --- a/src/NuGetGallery/ExtensionMethods.cs +++ b/src/NuGetGallery/ExtensionMethods.cs @@ -23,6 +23,11 @@ namespace NuGetGallery { public static class ExtensionMethods { + public static string ToJavaScriptUTC(this DateTime self) + { + return self.ToString("yyyy-MM-dd HH:mm:ss UTC", CultureInfo.CurrentCulture); + } + public static string ToNuGetShortDateTimeString(this DateTime self) { return self.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.CurrentCulture); diff --git a/src/NuGetGallery/Infrastructure/Lucene/ExternalSearchService.cs b/src/NuGetGallery/Infrastructure/Lucene/ExternalSearchService.cs index 9cd6a327c1..23b4773811 100644 --- a/src/NuGetGallery/Infrastructure/Lucene/ExternalSearchService.cs +++ b/src/NuGetGallery/Infrastructure/Lucene/ExternalSearchService.cs @@ -111,12 +111,13 @@ private async Task SearchCore(SearchFilter filter, bool raw) var content = await result.ReadContent(); if (filter.CountOnly || content.TotalHits == 0) { - results = new SearchResults(content.TotalHits); + results = new SearchResults(content.TotalHits, content.IndexTimestamp); } else { results = new SearchResults( content.TotalHits, + content.IndexTimestamp, content.Data.Select(ReadPackage).AsQueryable()); } } diff --git a/src/NuGetGallery/Infrastructure/Lucene/LuceneSearchService.cs b/src/NuGetGallery/Infrastructure/Lucene/LuceneSearchService.cs index fbf95c8cfa..ee1f84715b 100644 --- a/src/NuGetGallery/Infrastructure/Lucene/LuceneSearchService.cs +++ b/src/NuGetGallery/Infrastructure/Lucene/LuceneSearchService.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.IO; using System.Linq; using System.Threading.Tasks; using Lucene.Net.Analysis; @@ -49,6 +50,9 @@ public Task Search(SearchFilter searchFilter) private SearchResults SearchCore(SearchFilter searchFilter) { + // Get index timestamp + DateTime timestamp = File.GetLastWriteTimeUtc(LuceneCommon.GetIndexMetadataPath()); + int numRecords = searchFilter.Skip + searchFilter.Take; var searcher = new IndexSearcher(_directory, readOnly: true); @@ -77,7 +81,7 @@ private SearchResults SearchCore(SearchFilter searchFilter) if (results.TotalHits == 0 || searchFilter.CountOnly) { - return new SearchResults(results.TotalHits); + return new SearchResults(results.TotalHits, timestamp); } var packages = results.ScoreDocs @@ -86,6 +90,7 @@ private SearchResults SearchCore(SearchFilter searchFilter) .ToList(); return new SearchResults( results.TotalHits, + timestamp, packages.AsQueryable()); } diff --git a/src/NuGetGallery/NuGetGallery.csproj b/src/NuGetGallery/NuGetGallery.csproj index 9d7fa48edb..c08ef77301 100644 --- a/src/NuGetGallery/NuGetGallery.csproj +++ b/src/NuGetGallery/NuGetGallery.csproj @@ -271,13 +271,13 @@ False ..\..\packages\NuGet.Core.2.8.1\lib\net40-Client\NuGet.Core.dll - + False - ..\..\packages\NuGet.Services.Platform.Client.3.0.3-rel-0008\lib\portable-net45+wp80+win\NuGet.Services.Platform.Client.dll + ..\..\packages\NuGet.Services.Platform.Client.3.0.11-r-master\lib\portable-net45+wp80+win\NuGet.Services.Platform.Client.dll - + False - ..\..\packages\NuGet.Services.Search.Client.3.0.3-rel-0015\lib\portable-net45+wp80+win\NuGet.Services.Search.Client.dll + ..\..\packages\NuGet.Services.Search.Client.3.0.16-r-master\lib\portable-net45+wp80+win\NuGet.Services.Search.Client.dll ..\..\packages\ODataNullPropagationVisitor.0.5.4237.2641\lib\net40\ODataNullPropagationVisitor.dll diff --git a/src/NuGetGallery/Services/ISearchService.cs b/src/NuGetGallery/Services/ISearchService.cs index 0b117355aa..f6a19d4489 100644 --- a/src/NuGetGallery/Services/ISearchService.cs +++ b/src/NuGetGallery/Services/ISearchService.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System; +using System.Linq; using System.Threading.Tasks; namespace NuGetGallery @@ -31,17 +32,19 @@ public interface IRawSearchService public class SearchResults { public int Hits { get; private set; } + public DateTime? IndexTimestampUtc { get; private set; } public IQueryable Data { get; private set; } - public SearchResults(int hits) - : this(hits, Enumerable.Empty().AsQueryable()) + public SearchResults(int hits, DateTime? indexTimestampUtc) + : this(hits, indexTimestampUtc, Enumerable.Empty().AsQueryable()) { } - public SearchResults(int hits, IQueryable data) + public SearchResults(int hits, DateTime? indexTimestampUtc, IQueryable data) { Hits = hits; Data = data; + IndexTimestampUtc = indexTimestampUtc; } } } \ No newline at end of file diff --git a/src/NuGetGallery/ViewModels/PackageListViewModel.cs b/src/NuGetGallery/ViewModels/PackageListViewModel.cs index 75b73935d2..7e6a2ee843 100644 --- a/src/NuGetGallery/ViewModels/PackageListViewModel.cs +++ b/src/NuGetGallery/ViewModels/PackageListViewModel.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using System.Web.Mvc; @@ -8,6 +9,7 @@ public class PackageListViewModel { public PackageListViewModel( IQueryable packages, + DateTime? indexTimestampUtc, string searchTerm, int totalCount, int pageIndex, @@ -17,6 +19,7 @@ public class PackageListViewModel // TODO: Implement actual sorting IEnumerable items = packages.ToList().Select(pv => new ListPackageItemViewModel(pv)); PageIndex = pageIndex; + IndexTimestampUtc = indexTimestampUtc; PageSize = pageSize; TotalCount = totalCount; SearchTerm = searchTerm; @@ -49,5 +52,7 @@ public class PackageListViewModel public int PageIndex { get; private set; } public int PageSize { get; private set; } + + public DateTime? IndexTimestampUtc { get; private set; } } } \ No newline at end of file diff --git a/src/NuGetGallery/Views/Shared/ListPackages.cshtml b/src/NuGetGallery/Views/Shared/ListPackages.cshtml index abb69c0384..abdc9d1b8b 100644 --- a/src/NuGetGallery/Views/Shared/ListPackages.cshtml +++ b/src/NuGetGallery/Views/Shared/ListPackages.cshtml @@ -27,10 +27,14 @@ There are @Model.TotalCount packages } } - @if (@Model.LastResultIndex > 0) + @if (Model.LastResultIndex > 0) {

Displaying results @Model.FirstResultIndex - @Model.LastResultIndex.

} + @if(Model.IndexTimestampUtc.HasValue) + { +

Search Index last updated @Model.IndexTimestampUtc.Value.ToJavaScriptUTC()

+ } Sorted by Recent Installs diff --git a/src/NuGetGallery/packages.config b/src/NuGetGallery/packages.config index 1282e84f26..ae23b8484b 100644 --- a/src/NuGetGallery/packages.config +++ b/src/NuGetGallery/packages.config @@ -51,8 +51,8 @@ - - + + diff --git a/tests/NuGetGallery.Facts/Controllers/CuratedFeedsControllerFacts.cs b/tests/NuGetGallery.Facts/Controllers/CuratedFeedsControllerFacts.cs index 28d3cc01ae..54216cfef8 100644 --- a/tests/NuGetGallery.Facts/Controllers/CuratedFeedsControllerFacts.cs +++ b/tests/NuGetGallery.Facts/Controllers/CuratedFeedsControllerFacts.cs @@ -213,7 +213,7 @@ public async Task WillSearchForAPackage() controller.StubSearchService .Setup(stub => stub.Search(It.IsAny())) - .Returns(Task.FromResult(new SearchResults(mockPackages.Count(), mockPackages))); + .Returns(Task.FromResult(new SearchResults(mockPackages.Count(), DateTime.UtcNow, mockPackages))); var mockHttpContext = new Mock(); TestUtility.SetupHttpContextMockForUrlGeneration(mockHttpContext, controller); diff --git a/tests/NuGetGallery.Facts/Controllers/PackagesControllerFacts.cs b/tests/NuGetGallery.Facts/Controllers/PackagesControllerFacts.cs index 37576305d7..d9da6bc5dd 100644 --- a/tests/NuGetGallery.Facts/Controllers/PackagesControllerFacts.cs +++ b/tests/NuGetGallery.Facts/Controllers/PackagesControllerFacts.cs @@ -108,7 +108,7 @@ private static Mock CreateSearchService() { var searchService = new Mock(); searchService.Setup(s => s.Search(It.IsAny())).Returns( - (IQueryable p, string searchTerm) => Task.FromResult(new SearchResults(p.Count(), p))); + (IQueryable p, string searchTerm) => Task.FromResult(new SearchResults(p.Count(), DateTime.UtcNow, p))); return searchService; } @@ -574,7 +574,7 @@ public async Task TrimsSearchTerm() { var searchService = new Mock(); searchService.Setup(s => s.Search(It.IsAny())).Returns( - Task.FromResult(new SearchResults(0))); + Task.FromResult(new SearchResults(0, DateTime.UtcNow))); var controller = CreateController(searchService: searchService); controller.SetCurrentUser(TestUtility.FakeUser); diff --git a/tests/NuGetGallery.Facts/Services/FeedServiceFacts.cs b/tests/NuGetGallery.Facts/Services/FeedServiceFacts.cs index 9a2d65a218..f93564645c 100644 --- a/tests/NuGetGallery.Facts/Services/FeedServiceFacts.cs +++ b/tests/NuGetGallery.Facts/Services/FeedServiceFacts.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using System.Web; @@ -83,7 +84,7 @@ public void V1FeedSearchDoesNotReturnPrereleasePackages() configuration.Setup(c => c.GetSiteRoot(It.IsAny())).Returns("https://localhost:8081/"); var searchService = new Mock(MockBehavior.Strict); searchService.Setup(s => s.Search(It.IsAny())).Returns - , string>((_, __) => Task.FromResult(new SearchResults(_.Count(), _))); + , string>((_, __) => Task.FromResult(new SearchResults(_.Count(), DateTime.UtcNow, _))); searchService.Setup(s => s.ContainsAllVersions).Returns(false); var v1Service = new TestableV1Feed(repo.Object, configuration.Object, searchService.Object);