diff --git a/src/dotnet/APIView/APIViewIntegrationTests/ReviewManagerTests.cs b/src/dotnet/APIView/APIViewIntegrationTests/ReviewManagerTests.cs index 5bf457f68..906a4e717 100644 --- a/src/dotnet/APIView/APIViewIntegrationTests/ReviewManagerTests.cs +++ b/src/dotnet/APIView/APIViewIntegrationTests/ReviewManagerTests.cs @@ -55,7 +55,7 @@ namespace APIViewIntegrationTests var reviewManager = testsBaseFixture.ReviewManager; var apiRevisionsManager = testsBaseFixture.APIRevisionManager; var user = testsBaseFixture.User; - var review = await testsBaseFixture.ReviewManager.CreateReviewAsync(packageName: "testPackageA", language: "Swagger", isClosed:false); + var review = await testsBaseFixture.ReviewManager.CreateReviewAsync(packageName: "testPackageA", language: "Swagger", isClosed: false); await apiRevisionsManager.AddAPIRevisionAsync(user: user, reviewId: review.Id, apiRevisionType: APIRevisionType.Automatic, name: fileNameA, label: "Revision1", fileStream: fileStreamA, language: "Swagger", awaitComputeDiff: true); @@ -64,7 +64,7 @@ namespace APIViewIntegrationTests var apiRevisions = (await apiRevisionsManager.GetAPIRevisionsAsync(review.Id)).ToList(); - var headingWithDiffInSections = apiRevisions[0].HeadingsOfSectionsWithDiff[apiRevisions[1].Id]; + var headingWithDiffInSections = apiRevisions[1].HeadingsOfSectionsWithDiff[apiRevisions[0].Id]; Assert.All(headingWithDiffInSections, item => Assert.Contains(item, new int[] { 2, 16 })); } @@ -85,7 +85,7 @@ namespace APIViewIntegrationTests var apiRevisions = (await apiRevisionsManager.GetAPIRevisionsAsync(review.Id)).ToList(); - var headingWithDiffInSections = apiRevisions[0].HeadingsOfSectionsWithDiff[apiRevisions[1].Id]; + var headingWithDiffInSections = apiRevisions[1].HeadingsOfSectionsWithDiff[apiRevisions[0].Id]; Assert.All(headingWithDiffInSections, item => Assert.Contains(item, new int[] { 20, 275 })); } diff --git a/src/dotnet/APIView/APIViewIntegrationTests/TestsBaseFixture.cs b/src/dotnet/APIView/APIViewIntegrationTests/TestsBaseFixture.cs index 8e5afc5cb..2d7b075a1 100644 --- a/src/dotnet/APIView/APIViewIntegrationTests/TestsBaseFixture.cs +++ b/src/dotnet/APIView/APIViewIntegrationTests/TestsBaseFixture.cs @@ -72,7 +72,7 @@ namespace APIViewIntegrationTests User = TestUser.GetTestuser(); _cosmosClient = new CosmosClient(config["Cosmos:ConnectionString"]); - var dataBaseResponse = _cosmosClient.CreateDatabaseIfNotExistsAsync("APIView").Result; + var dataBaseResponse = _cosmosClient.CreateDatabaseIfNotExistsAsync("APIViewV2").Result; dataBaseResponse.Database.CreateContainerIfNotExistsAsync("Reviews", "/id").Wait(); dataBaseResponse.Database.CreateContainerIfNotExistsAsync("APIRevisions", "/ReviewId").Wait(); dataBaseResponse.Database.CreateContainerIfNotExistsAsync("Comments", "/ReviewId").Wait(); diff --git a/src/dotnet/APIView/APIViewWeb/Helpers/PageModelHelpers.cs b/src/dotnet/APIView/APIViewWeb/Helpers/PageModelHelpers.cs index 90c1b0d6e..3826ac8db 100644 --- a/src/dotnet/APIView/APIViewWeb/Helpers/PageModelHelpers.cs +++ b/src/dotnet/APIView/APIViewWeb/Helpers/PageModelHelpers.cs @@ -17,6 +17,7 @@ using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.Configuration; using Microsoft.OpenApi.Any; using Microsoft.AspNetCore.Http; +using Microsoft.VisualStudio.Services.ClientNotification; namespace APIViewWeb.Helpers { @@ -240,12 +241,18 @@ namespace APIViewWeb.Helpers string revisionId = null, string diffRevisionId = null, bool showDocumentation = false, bool showDiffOnly = false, int diffContextSize = 3, string diffContextSeperator = "
.....
", HashSet headingsOfSectionsWithDiff = null) { + var reviewPageContent = new ReviewContentModel() + { + Directive = ReviewContentModelDirective.ProceedWithPageLoad + }; + var userId = user.GetGitHubLogin(); var review = await reviewManager.GetReviewAsync(user, reviewId); if (review == null) { - return default(ReviewContentModel); + reviewPageContent.Directive = ReviewContentModelDirective.TryGetlegacyReview; + return reviewPageContent; } var apiRevisions = await reviewRevisionsManager.GetAPIRevisionsAsync(reviewId); @@ -254,8 +261,9 @@ namespace APIViewWeb.Helpers var activeRevision = await reviewRevisionsManager.GetLatestAPIRevisionsAsync(reviewId, apiRevisions, APIRevisionType.Automatic); if (activeRevision == null) { - var notifcation = new NotificationModel() { Message = $"This review has no valid apiRevisons", Level = NotificatonLevel.Warning }; - await signalRHubContext.Clients.Group(userId).SendAsync("RecieveNotification", notifcation); + reviewPageContent.Directive = ReviewContentModelDirective.ErrorDueToInvalidAPIRevison; + reviewPageContent.NotificationMessage = $"Review with ID {reviewId} has no valid APIRevisons"; + return reviewPageContent; } APIRevisionListItemModel diffRevision = null; @@ -266,8 +274,9 @@ namespace APIViewWeb.Helpers } else { - var notifcation = new NotificationModel() { Message = $"A revision with ID {revisionId} does not exist for this review.", Level = NotificatonLevel.Warning }; - await signalRHubContext.Clients.Group(userId).SendAsync("RecieveNotification", notifcation); + reviewPageContent.NotificationMessage = $"A revision with ID {revisionId} does not exist for review with id {reviewId}"; + reviewPageContent.Directive = ReviewContentModelDirective.ErrorDueToInvalidAPIRevison; + return reviewPageContent; } } var comments = await commentManager.GetReviewCommentsAsync(reviewId); @@ -278,6 +287,8 @@ namespace APIViewWeb.Helpers var activeRevisionHtmlLines = activeRevisionRenderableCodeFile.Render(showDocumentation: showDocumentation); var codeLines = new CodeLineModel[0]; + var getCodeLines = false; + if (!string.IsNullOrEmpty(diffRevisionId)) @@ -297,17 +308,20 @@ namespace APIViewWeb.Helpers if (!codeLines.Any()) { - var notifcation = new NotificationModel() { Message = $"There is no diff between the two revisions. {activeRevision.Id} : {diffRevisionId}", Level = NotificatonLevel.Info }; - await signalRHubContext.Clients.Group(userId).SendAsync("RecieveNotification", notifcation); + getCodeLines = true; + reviewPageContent.NotificationMessage = $"There is no diff between the two revisions. {activeRevision.Id} : {diffRevisionId}"; + reviewPageContent.Directive = ReviewContentModelDirective.ErrorDueToInvalidAPIRevison; } } else { - var notifcation = new NotificationModel() { Message = $"A diffRevision with ID {diffRevisionId} does not exist for this review.", Level = NotificatonLevel.Warning }; - await signalRHubContext.Clients.Group(userId).SendAsync("RecieveNotification", notifcation); + getCodeLines = true; + reviewPageContent.NotificationMessage = $"A diffRevision with ID {diffRevisionId} does not exist for this review."; + reviewPageContent.Directive = ReviewContentModelDirective.ErrorDueToInvalidAPIRevison; } } - else + + if (string.IsNullOrEmpty(diffRevisionId) || getCodeLines) { codeLines = CreateLines(diagnostics: fileDiagnostics, lines: activeRevisionHtmlLines, comments: comments); } @@ -346,21 +360,19 @@ namespace APIViewWeb.Helpers } } - var reviewPageContent = new ReviewContentModel - { - Review = review, - Navigation = activeRevisionRenderableCodeFile.CodeFile.Navigation, - codeLines = codeLines, - APIRevisionsGrouped = apiRevisions.OrderByDescending(c => c.CreatedOn).GroupBy(r => r.APIRevisionType).ToDictionary(r => r.Key.ToString(), r => r.ToList()), - ActiveAPIRevision = activeRevision, - DiffAPIRevision = diffRevision, - TotalActiveConversiations = comments.Threads.Count(t => !t.IsResolved), - ActiveConversationsInActiveAPIRevision = ComputeActiveConversationsInActiveRevision(activeRevisionHtmlLines, comments), - ActiveConversationsInSampleRevisions = comments.Threads.Count(t => t.Comments.FirstOrDefault()?.CommentType == CommentType.SamplesRevision), - PreferredApprovers = preferredApprovers, - TaggableUsers = commentManager.GetTaggableUsers(), - PageHasLoadableSections = activeRevisionReviewCodeFile.LeafSections?.Any() ?? false - }; + reviewPageContent.Review = review; + reviewPageContent.Navigation = activeRevisionRenderableCodeFile.CodeFile.Navigation; + reviewPageContent.codeLines = codeLines; + reviewPageContent.APIRevisionsGrouped = apiRevisions.OrderByDescending(c => c.CreatedOn).GroupBy(r => r.APIRevisionType).ToDictionary(r => r.Key.ToString(), r => r.ToList()); + reviewPageContent.ActiveAPIRevision = activeRevision; + reviewPageContent.DiffAPIRevision = diffRevision; + reviewPageContent.TotalActiveConversiations = comments.Threads.Count(t => !t.IsResolved); + reviewPageContent.ActiveConversationsInActiveAPIRevision = ComputeActiveConversationsInActiveRevision(activeRevisionHtmlLines, comments); + reviewPageContent.ActiveConversationsInSampleRevisions = comments.Threads.Count(t => t.Comments.FirstOrDefault()?.CommentType == CommentType.SamplesRevision); + reviewPageContent.PreferredApprovers = preferredApprovers; + reviewPageContent.TaggableUsers = commentManager.GetTaggableUsers(); + reviewPageContent.PageHasLoadableSections = activeRevisionReviewCodeFile.LeafSections?.Any() ?? false; + return reviewPageContent; } diff --git a/src/dotnet/APIView/APIViewWeb/LeanModels/ReviewRevisionPageModels.cs b/src/dotnet/APIView/APIViewWeb/LeanModels/ReviewRevisionPageModels.cs index df8510728..d0735206b 100644 --- a/src/dotnet/APIView/APIViewWeb/LeanModels/ReviewRevisionPageModels.cs +++ b/src/dotnet/APIView/APIViewWeb/LeanModels/ReviewRevisionPageModels.cs @@ -5,6 +5,13 @@ using APIViewWeb.Models; namespace APIViewWeb.LeanModels { + public enum ReviewContentModelDirective + { + ProceedWithPageLoad = 0, + TryGetlegacyReview, + ErrorDueToInvalidAPIRevison + } + public class ReviewContentModel { public ReviewListItemModel Review { get; set; } @@ -19,5 +26,7 @@ namespace APIViewWeb.LeanModels public HashSet PreferredApprovers = new HashSet(); public HashSet TaggableUsers { get; set; } public bool PageHasLoadableSections { get; set; } + public string NotificationMessage { get; set; } + public ReviewContentModelDirective Directive { get; set; } } } diff --git a/src/dotnet/APIView/APIViewWeb/Managers/APIRevisionsManager.cs b/src/dotnet/APIView/APIViewWeb/Managers/APIRevisionsManager.cs index 027bdfa3d..ce8c77e81 100644 --- a/src/dotnet/APIView/APIViewWeb/Managers/APIRevisionsManager.cs +++ b/src/dotnet/APIView/APIViewWeb/Managers/APIRevisionsManager.cs @@ -373,7 +373,7 @@ namespace APIViewWeb.Managers /// /// /// - public async Task AddAPIRevisionAsync( + public async Task AddAPIRevisionAsync( ClaimsPrincipal user, ReviewListItemModel review, APIRevisionType apiRevisionType, @@ -383,48 +383,49 @@ namespace APIViewWeb.Managers string language, bool awaitComputeDiff = false) { - var revision = GetNewAPIRevisionAsync( + var apiRevision = GetNewAPIRevisionAsync( reviewId: review.Id, apiRevisionType: apiRevisionType, packageName: review.PackageName, language: review.Language, - createdBy: review.CreatedBy, + createdBy: user.GetGitHubLogin(), label: label); var codeFile = await _codeFileManager.CreateCodeFileAsync( - revision.Id, + apiRevision.Id, name, - fileStream, true, + fileStream, language); - revision.Files.Add(codeFile); + apiRevision.Files.Add(codeFile); var languageService = language != null ? _languageServices.FirstOrDefault(l => l.Name == language) : _languageServices.FirstOrDefault(s => s.IsSupportedFile(name)); // Run pipeline to generate the review if sandbox is enabled if (languageService != null && languageService.IsReviewGenByPipeline) { // Run offline review gen for review and reviewCodeFileModel - await GenerateAPIRevisionInExternalResource(review, revision.Id, codeFile.FileId, name, language); + await GenerateAPIRevisionInExternalResource(review, apiRevision.Id, codeFile.FileId, name, language); } // auto subscribe revision creation user await _notificationManager.SubscribeAsync(review, user); await _reviewsRepository.UpsertReviewAsync(review); - await _apiRevisionsRepository.UpsertAPIRevisionAsync(revision); - await _notificationManager.NotifySubscribersOnNewRevisionAsync(review, revision, user); + await _apiRevisionsRepository.UpsertAPIRevisionAsync(apiRevision); + await _notificationManager.NotifySubscribersOnNewRevisionAsync(review, apiRevision, user); if (!String.IsNullOrEmpty(review.Language) && review.Language == "Swagger") { if (awaitComputeDiff) { - await GetLineNumbersOfHeadingsOfSectionsWithDiff(review.Id, revision); + await GetLineNumbersOfHeadingsOfSectionsWithDiff(review.Id, apiRevision); } else { - _ = Task.Run(async () => await GetLineNumbersOfHeadingsOfSectionsWithDiff(review.Id, revision)); + _ = Task.Run(async () => await GetLineNumbersOfHeadingsOfSectionsWithDiff(review.Id, apiRevision)); } } + return apiRevision; //await GenerateAIReview(review, revision); } diff --git a/src/dotnet/APIView/APIViewWeb/Managers/CodeFileManager.cs b/src/dotnet/APIView/APIViewWeb/Managers/CodeFileManager.cs index 196d0ce0f..18c1dd844 100644 --- a/src/dotnet/APIView/APIViewWeb/Managers/CodeFileManager.cs +++ b/src/dotnet/APIView/APIViewWeb/Managers/CodeFileManager.cs @@ -60,7 +60,7 @@ namespace APIViewWeb.Managers { // backward compatibility until all languages moved to sandboxing of codefile to pipeline stream = await _devopsArtifactRepository.DownloadPackageArtifact(repoName, buildId, artifactName, originalFileName, format: "file", project: project); - codeFile = await CreateCodeFileAsync(Path.GetFileName(originalFileName), stream, false, originalFileStream); + codeFile = await CreateCodeFileAsync(originalName: Path.GetFileName(originalFileName), fileStream: stream, runAnalysis: false, memoryStream: originalFileStream); } else { @@ -93,19 +93,20 @@ namespace APIViewWeb.Managers /// /// /// - /// /// + /// /// /// public async Task CreateCodeFileAsync( string apiRevisionId, string originalName, - Stream fileStream, bool runAnalysis, - string language) + Stream fileStream = null, + string language = null) { using var memoryStream = new MemoryStream(); - var codeFile = await CreateCodeFileAsync(originalName, fileStream, runAnalysis, memoryStream, language); + var codeFile = await CreateCodeFileAsync(originalName: originalName, runAnalysis: runAnalysis, + memoryStream: memoryStream, fileStream: fileStream, language: language); var reviewCodeFileModel = await CreateReviewCodeFileModel(apiRevisionId, memoryStream, codeFile); reviewCodeFileModel.FileName = originalName; return reviewCodeFileModel; @@ -115,16 +116,16 @@ namespace APIViewWeb.Managers /// Create Code File /// /// - /// /// /// + /// /// /// public async Task CreateCodeFileAsync( string originalName, - Stream fileStream, bool runAnalysis, MemoryStream memoryStream, + Stream fileStream = null, string language = null) { var languageService = _languageServices.FirstOrDefault(s => (language != null ? s.Name == language : s.IsSupportedFile(originalName))); diff --git a/src/dotnet/APIView/APIViewWeb/Managers/Interfaces/IAPIRevisionsManager.cs b/src/dotnet/APIView/APIViewWeb/Managers/Interfaces/IAPIRevisionsManager.cs index cfe4b466f..af85dd3bf 100644 --- a/src/dotnet/APIView/APIViewWeb/Managers/Interfaces/IAPIRevisionsManager.cs +++ b/src/dotnet/APIView/APIViewWeb/Managers/Interfaces/IAPIRevisionsManager.cs @@ -22,7 +22,7 @@ namespace APIViewWeb.Managers.Interfaces string label = null, int? prNumber = null, string createdBy = "azure-sdk"); public Task ToggleAPIRevisionApprovalAsync(ClaimsPrincipal user, string id, string revisionId = null, APIRevisionListItemModel apiRevision = null, string notes = ""); public Task AddAPIRevisionAsync(ClaimsPrincipal user, string reviewId, APIRevisionType apiRevisionType, string name, string label, Stream fileStream, string language = "", bool awaitComputeDiff = false); - public Task AddAPIRevisionAsync(ClaimsPrincipal user, ReviewListItemModel review, APIRevisionType apiRevisionType, string name, string label, Stream fileStream, string language, bool awaitComputeDiff = false); + public Task AddAPIRevisionAsync(ClaimsPrincipal user, ReviewListItemModel review, APIRevisionType apiRevisionType, string name, string label, Stream fileStream, string language, bool awaitComputeDiff = false); public Task RunAPIRevisionGenerationPipeline(List reviewGenParams, string language); public Task SoftDeleteAPIRevisionAsync(ClaimsPrincipal user, string reviewId, string revisionId); public Task SoftDeleteAPIRevisionAsync(ClaimsPrincipal user, APIRevisionListItemModel apiRevision); diff --git a/src/dotnet/APIView/APIViewWeb/Managers/Interfaces/ICodeFileManager.cs b/src/dotnet/APIView/APIViewWeb/Managers/Interfaces/ICodeFileManager.cs index af5b95a0e..4c031fcec 100644 --- a/src/dotnet/APIView/APIViewWeb/Managers/Interfaces/ICodeFileManager.cs +++ b/src/dotnet/APIView/APIViewWeb/Managers/Interfaces/ICodeFileManager.cs @@ -9,8 +9,8 @@ namespace APIViewWeb.Managers.Interfaces { public Task GetCodeFileAsync(string repoName, string buildId, string artifactName, string packageName, string originalFileName, string codeFileName, MemoryStream originalFileStream, string baselineCodeFileName = "", MemoryStream baselineStream = null, string project = "public"); - public Task CreateCodeFileAsync(string apiRevisionId, string originalName, Stream fileStream, bool runAnalysis, string language); - public Task CreateCodeFileAsync(string originalName, Stream fileStream, bool runAnalysis, MemoryStream memoryStream, string language = null); + public Task CreateCodeFileAsync(string apiRevisionId, string originalName, bool runAnalysis, Stream fileStream = null, string language = null); + public Task CreateCodeFileAsync(string originalName, bool runAnalysis, MemoryStream memoryStream, Stream fileStream = null, string language = null); public Task CreateReviewCodeFileModel(string apiRevisionId, MemoryStream memoryStream, CodeFile codeFile); public bool IsAPICodeFilesTheSame(RenderedCodeFile codeFileA, RenderedCodeFile codeFileB); } diff --git a/src/dotnet/APIView/APIViewWeb/Managers/Interfaces/IPullRequestManager.cs b/src/dotnet/APIView/APIViewWeb/Managers/Interfaces/IPullRequestManager.cs index 4557b593e..506b89f0a 100644 --- a/src/dotnet/APIView/APIViewWeb/Managers/Interfaces/IPullRequestManager.cs +++ b/src/dotnet/APIView/APIViewWeb/Managers/Interfaces/IPullRequestManager.cs @@ -6,7 +6,7 @@ namespace APIViewWeb.Managers { public interface IPullRequestManager { - public Task> GetPullRequestsModelAsync(string reviewId); + public Task> GetPullRequestsModelAsync(string reviewId, string apiRevisionId = null); public Task> GetPullRequestsModelAsync(int pullRequestNumber, string repoName); public Task GetPullRequestModelAsync(int prNumber, string repoName, string packageName, string originalFile, string language); public Task CreateOrUpdateCommentsOnPR(List pullRequests, string repoOwner, string repoName, int prNumber, string hostName); diff --git a/src/dotnet/APIView/APIViewWeb/Managers/PullRequestManager.cs b/src/dotnet/APIView/APIViewWeb/Managers/PullRequestManager.cs index b363a6c3f..10ae04f25 100644 --- a/src/dotnet/APIView/APIViewWeb/Managers/PullRequestManager.cs +++ b/src/dotnet/APIView/APIViewWeb/Managers/PullRequestManager.cs @@ -53,8 +53,8 @@ namespace APIViewWeb.Managers _pullRequestCleanupDays = int.Parse(pullRequestReviewCloseAfter); } - public async Task> GetPullRequestsModelAsync(string reviewId) { - return await _pullRequestsRepository.GetPullRequestsAsync(reviewId); + public async Task> GetPullRequestsModelAsync(string reviewId, string apiRevisionId = null) { + return await _pullRequestsRepository.GetPullRequestsAsync(reviewId, apiRevisionId); } public async Task> GetPullRequestsModelAsync(int pullRequestNumber, string repoName) diff --git a/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/Index.cshtml b/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/Index.cshtml index 514473424..28ed43b0a 100644 --- a/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/Index.cshtml +++ b/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/Index.cshtml @@ -71,8 +71,74 @@ if (userPreference.HideIndexPageOptions.HasValue && userPreference.HideIndexPageOptions == true) mainContainerClass = String.Empty; } +
+ + + +
+ @if (!string.IsNullOrEmpty(Model.NotificationMessage)) + { + + }
diff --git a/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/Index.cshtml.cs b/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/Index.cshtml.cs index e496203cf..376640738 100644 --- a/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/Index.cshtml.cs +++ b/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/Index.cshtml.cs @@ -8,37 +8,52 @@ using APIViewWeb.Repositories; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using APIViewWeb.Managers; -using Microsoft.TeamFoundation.Common; using APIViewWeb.Helpers; using Microsoft.VisualStudio.Services.Common; using APIViewWeb.Hubs; using Microsoft.AspNetCore.SignalR; -using System.Text; using APIViewWeb.LeanModels; +using System.Text; +using APIViewWeb.Managers.Interfaces; +using System.IO; +using ApiView; +using Microsoft.AspNetCore.Http; namespace APIViewWeb.Pages.Assemblies { public class IndexPageModel : PageModel { private readonly IReviewManager _reviewManager; + private readonly IAPIRevisionsManager _apiRevisionsManager; private readonly IHubContext _notificationHubContext; public readonly UserPreferenceCache _preferenceCache; public readonly IUserProfileManager _userProfileManager; + private readonly ICodeFileManager _codeFileManager; + public const int _defaultPageSize = 50; public const string _defaultSortField = "LastUpdatedOn"; - public IndexPageModel(IReviewManager reviewManager, IUserProfileManager userProfileManager, UserPreferenceCache preferenceCache, IHubContext notificationHub) + public IndexPageModel(IReviewManager reviewManager, IAPIRevisionsManager apiRevisionsManager, IUserProfileManager userProfileManager, + UserPreferenceCache preferenceCache, IHubContext notificationHub, ICodeFileManager codeFileManager) { _notificationHubContext = notificationHub; _reviewManager = reviewManager; + _apiRevisionsManager = apiRevisionsManager; _preferenceCache = preferenceCache; _userProfileManager = userProfileManager; + _codeFileManager = codeFileManager; } + [FromForm] + public UploadModel Upload { get; set; } + [FromForm] + public string Label { get; set; } public ReviewsProperties ReviewsProperties { get; set; } = new ReviewsProperties(); public (IEnumerable Reviews, int TotalCount, int TotalPages, int CurrentPage, int? PreviousPage, int? NextPage) PagedResults { get; set; } + [BindProperty(Name = "notificationMessage", SupportsGet = true)] + public string NotificationMessage { get; set; } public async Task OnGetAsync( IEnumerable search, IEnumerable languages, IEnumerable state, @@ -66,6 +81,79 @@ namespace APIViewWeb.Pages.Assemblies return Partial("_ReviewsPartial", PagedResults); } + public async Task OnPostUploadAsync() + { + if (!ModelState.IsValid) + { + var errors = new StringBuilder(); + foreach (var modelState in ModelState.Values) + { + foreach (var error in modelState.Errors) + { + errors.AppendLine(error.ErrorMessage); + } + } + var notifcation = new NotificationModel() { Message = errors.ToString(), Level = NotificatonLevel.Error }; + await _notificationHubContext.Clients.Group(User.GetGitHubLogin()).SendAsync("RecieveNotification", notifcation); + return new NoContentResult(); + } + + var file = Upload.Files?.SingleOrDefault(); + var review = await GetOrCreateReview(file, Upload.FilePath); + + if (review != null) + { + APIRevisionListItemModel apiRevision = null; + + if (file != null) + { + using (var openReadStream = file.OpenReadStream()) + { + apiRevision = await _apiRevisionsManager.AddAPIRevisionAsync(user: User, review: review, apiRevisionType: APIRevisionType.Manual, + name: file.FileName, label: Label, fileStream: openReadStream, language: Upload.Language); + } + } + else if (!string.IsNullOrEmpty(Upload.FilePath)) + { + apiRevision = await _apiRevisionsManager.AddAPIRevisionAsync(user: User, review: review, apiRevisionType: APIRevisionType.Manual, + name: file.FileName, label: Label, fileStream: null, language: Upload.Language); + } + return RedirectToPage("Review", new { id = review.Id, revisionId = apiRevision.Id }); + } + return RedirectToPage(); + } + + private async Task GetOrCreateReview(IFormFile file, string filePath) + { + CodeFile codeFile = null; + ReviewListItemModel review = null; + + using var memoryStream = new MemoryStream(); + if (file != null) + { + using (var openReadStream = file.OpenReadStream()) + { + codeFile = await _codeFileManager.CreateCodeFileAsync( + originalName: file?.FileName, fileStream: openReadStream, runAnalysis: Upload.RunAnalysis, memoryStream: memoryStream, language: Upload.Language); + } + } + else if (!string.IsNullOrEmpty(filePath)) + { + codeFile = await _codeFileManager.CreateCodeFileAsync( + originalName: Upload.FilePath, runAnalysis: Upload.RunAnalysis, memoryStream: memoryStream, language: Upload.Language); + } + + if (codeFile != null) + { + review = await _reviewManager.GetReviewAsync(packageName: codeFile.PackageName, language: codeFile.Language); + if (review == null) + { + review = await _reviewManager.CreateReviewAsync(packageName: codeFile.PackageName, language: codeFile.Language, isClosed: false); + } + } + return review; + } + private async Task RunGetRequest(IEnumerable search, IEnumerable languages, IEnumerable state, IEnumerable status, int pageNo, int pageSize, string sortField, bool fromUrl = true) { diff --git a/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/Review.cshtml b/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/Review.cshtml index af17db399..062a13a78 100644 --- a/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/Review.cshtml +++ b/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/Review.cshtml @@ -262,8 +262,8 @@ } @if (Model.ReviewContent.ActiveAPIRevision.APIRevisionType == APIRevisionType.PullRequest) { - var prsOfAssociatedReviews = await Model.GetPRsOfAssoicatedReviews(); - @if (prsOfAssociatedReviews != null && prsOfAssociatedReviews.Count() > 1) + var prsOfAssociatedAPIRevisions = await Model.GetPRsOfAssoicatedReviews(); + @if (prsOfAssociatedAPIRevisions != null && prsOfAssociatedAPIRevisions.Count() > 1) { var associatedReviewsState = String.Empty; if (Request.Cookies.ContainsKey("associatedReviewsCollapse")) @@ -272,16 +272,17 @@ associatedReviewsState = " show"; }

- Associated Reviews   + Associated APIRevisions  

    - @foreach (var pr in prsOfAssociatedReviews) + @foreach (var pr in prsOfAssociatedAPIRevisions) { if (pr.ReviewId != Model.ReviewContent.Review.Id) { var url = @Url.ActionLink("Review", "Assemblies", new { id = pr.ReviewId, + revisionId = pr.APIRevisionId });
  • @pr.Language/@pr.PackageName @@ -325,21 +326,6 @@
-
  • -
    -
    - @if (Model.ReviewContent.Review.IsClosed) - { - - } - else - { - - } - -
    -
    -
  • Page Settings   @@ -607,6 +593,13 @@

    + @if (!string.IsNullOrEmpty(Model.NotificationMessage)) + { + + } @foreach (var line in Model.ReviewContent.codeLines) diff --git a/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/Review.cshtml.cs b/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/Review.cshtml.cs index d01b1bee7..a6caf9aee 100644 --- a/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/Review.cshtml.cs +++ b/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/Review.cshtml.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Net; using System.Threading.Tasks; using APIViewWeb.Helpers; using APIViewWeb.Hubs; @@ -18,6 +19,7 @@ using Microsoft.AspNetCore.SignalR; using Microsoft.CodeAnalysis; using Microsoft.Extensions.Configuration; using Microsoft.TeamFoundation.Common; +using Microsoft.VisualStudio.Services.ClientNotification; namespace APIViewWeb.Pages.Assemblies @@ -70,6 +72,8 @@ namespace APIViewWeb.Pages.Assemblies public bool ShowDocumentation { get; set; } [BindProperty(Name = "diffOnly", SupportsGet = true)] public bool ShowDiffOnly { get; set; } + [BindProperty(Name = "notificationMessage", SupportsGet = true)] + public string NotificationMessage { get; set; } /// /// Handler for loading page @@ -90,7 +94,7 @@ namespace APIViewWeb.Pages.Assemblies showDocumentation: ShowDocumentation, showDiffOnly: ShowDiffOnly, diffContextSize: REVIEW_DIFF_CONTEXT_SIZE, diffContextSeperator: DIFF_CONTEXT_SEPERATOR); - if (ReviewContent == default(ReviewContentModel)) + if (ReviewContent.Directive == ReviewContentModelDirective.TryGetlegacyReview) { // Check if you can get review from legacy data var legacyReview = await _reviewManager.GetLegacyReviewAsync(User, id); @@ -101,7 +105,7 @@ namespace APIViewWeb.Pages.Assemblies if (legacyRevision == null) { - return NotFound(); + return RedirectToPage("Index", new { notificationMessage = $"Review with ID : {id} was not found." }); } var review = await _reviewManager.GetReviewAsync(language: legacyRevision.Files[0].Language, @@ -116,10 +120,15 @@ namespace APIViewWeb.Pages.Assemblies } else { - return NotFound(); + return RedirectToPage("Index", new { notificationMessage = $"Review with ID : {id} was not found." }); } } + if (ReviewContent.Directive == ReviewContentModelDirective.ErrorDueToInvalidAPIRevison) + { + NotificationMessage = ReviewContent.NotificationMessage; + } + if (!ReviewContent.APIRevisionsGrouped.Any()) { return RedirectToPage("LegacyReview", new { id = id }); @@ -314,7 +323,7 @@ namespace APIViewWeb.Pages.Assemblies /// public async Task> GetAssociatedPullRequest() { - return await _pullRequestManager.GetPullRequestsModelAsync(ReviewContent.Review.Id); + return await _pullRequestManager.GetPullRequestsModelAsync(reviewId: ReviewContent.Review.Id, apiRevisionId: ReviewContent.ActiveAPIRevision.Id); } /// @@ -323,8 +332,12 @@ namespace APIViewWeb.Pages.Assemblies /// public async Task> GetPRsOfAssoicatedReviews() { - var creatingPR = (await _pullRequestManager.GetPullRequestsModelAsync(ReviewContent.Review.Id)).FirstOrDefault(); - return await _pullRequestManager.GetPullRequestsModelAsync(creatingPR.PullRequestNumber, creatingPR.RepoName);; + var creatingPR = (await _pullRequestManager.GetPullRequestsModelAsync(reviewId: ReviewContent.Review.Id, apiRevisionId: ReviewContent.ActiveAPIRevision.Id)).FirstOrDefault(); + if (creatingPR != null) + { + return await _pullRequestManager.GetPullRequestsModelAsync(creatingPR.PullRequestNumber, creatingPR.RepoName); + } + return new List(); } /// diff --git a/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/Revisions.cshtml b/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/Revisions.cshtml index f46fb96f4..674cdfa6b 100644 --- a/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/Revisions.cshtml +++ b/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/Revisions.cshtml @@ -24,7 +24,7 @@
    -
    +
    diff --git a/src/dotnet/APIView/APIViewWeb/Repositories/CosmosPullRequestsRepository.cs b/src/dotnet/APIView/APIViewWeb/Repositories/CosmosPullRequestsRepository.cs index 929338437..b95495136 100644 --- a/src/dotnet/APIView/APIViewWeb/Repositories/CosmosPullRequestsRepository.cs +++ b/src/dotnet/APIView/APIViewWeb/Repositories/CosmosPullRequestsRepository.cs @@ -51,8 +51,13 @@ namespace APIViewWeb return await GetPullRequestFromQueryAsync(query); } - public async Task> GetPullRequestsAsync(string reviewId) { + public async Task> GetPullRequestsAsync(string reviewId, string apiRevisionId = null) { var query = $"SELECT * FROM PullRequests c WHERE c.ReviewId = '{reviewId}' AND c.IsDeleted = false"; + if (!string.IsNullOrEmpty(apiRevisionId)) + { + query += $" AND c.APIRevisionId = '{apiRevisionId}'"; + } + return await GetPullRequestFromQueryAsync(query); } diff --git a/src/dotnet/APIView/APIViewWeb/Repositories/CosmosReviewRepository.cs b/src/dotnet/APIView/APIViewWeb/Repositories/CosmosReviewRepository.cs index 0dee26144..df656fd55 100644 --- a/src/dotnet/APIView/APIViewWeb/Repositories/CosmosReviewRepository.cs +++ b/src/dotnet/APIView/APIViewWeb/Repositories/CosmosReviewRepository.cs @@ -69,7 +69,16 @@ namespace APIViewWeb public async Task GetLegacyReviewAsync(string reviewId) { - return await _legacyReviewsContainer.ReadItemAsync(reviewId, new PartitionKey(reviewId)); + var review = default(LegacyReviewModel); + try + { + review = await _legacyReviewsContainer.ReadItemAsync(reviewId, new PartitionKey(reviewId)); + } + catch (CosmosException ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound) + { + return review; + } + return review; } public async Task GetReviewAsync(string language, string packageName, bool? isClosed = false) diff --git a/src/dotnet/APIView/APIViewWeb/Repositories/Interfaces/ICosmosPullRequestsRepository.cs b/src/dotnet/APIView/APIViewWeb/Repositories/Interfaces/ICosmosPullRequestsRepository.cs index d4e0fa09a..cc609d93b 100644 --- a/src/dotnet/APIView/APIViewWeb/Repositories/Interfaces/ICosmosPullRequestsRepository.cs +++ b/src/dotnet/APIView/APIViewWeb/Repositories/Interfaces/ICosmosPullRequestsRepository.cs @@ -7,7 +7,7 @@ namespace APIViewWeb.Repositories public interface ICosmosPullRequestsRepository { public Task GetPullRequestAsync(int pullRequestNumber, string repoName, string packageName, string language = null); - public Task> GetPullRequestsAsync(string reviewId); + public Task> GetPullRequestsAsync(string reviewId, string apiRevisionId = null); public Task UpsertPullRequestAsync(PullRequestModel pullRequestModel); public Task> GetPullRequestsAsync(bool isOpen); public Task> GetPullRequestsAsync(int pullRequestNumber, string repoName); diff --git a/src/dotnet/APIView/apiview.yml b/src/dotnet/APIView/apiview.yml index e3c05f982..41adacb16 100644 --- a/src/dotnet/APIView/apiview.yml +++ b/src/dotnet/APIView/apiview.yml @@ -141,119 +141,119 @@ stages: artifactName: 'APIView' -# - job: 'Test' -# -# pool: -# name: azsdk-pool-mms-win-2022-general -# vmImage: windows-2022 -# -# steps: -# - template: /eng/common/pipelines/templates/steps/cosmos-emulator.yml -# parameters: -# StartParameters: '/noexplorer /noui /enablepreview /disableratelimiting /enableaadauthentication /partitioncount=50 /consistency=Strong' -# -# - script: | -# npm install -g azurite -# displayName: 'Install Azurite' -# -# - task: Powershell@2 -# inputs: -# workingDirectory: $(Agent.TempDirectory) -# filePath: $(Build.SourcesDirectory)/eng/scripts/Start-LocalHostApp.ps1 -# arguments: > -# -Process "azurite.cmd" -# -ArgumentList "--silent" -# -Port "10000" -# pwsh: true -# displayName: 'Start Azurite' -# -# - template: /eng/pipelines/templates/steps/install-dotnet.yml -# -# - pwsh: | -# dotnet --list-runtimes -# dotnet --version -# displayName: 'List .NET run times' -# -# - task: GoTool@0 -# inputs: -# version: '$(GoVersion)' -# displayName: "Use Go $(GoVersion)" -# -# - script: | -# go test ./... -v -# workingDirectory: $(GoParserPackagePath) -# displayName: 'Test Go parser' -# -# - script: >- -# dotnet test src/dotnet/APIView/APIViewUnitTests/APIViewUnitTests.csproj -# --logger trx --collect:"XPlat Code Coverage" -# displayName: "Build & Test (Unit)" -# env: -# DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 -# DOTNET_CLI_TELEMETRY_OPTOUT: 1 -# DOTNET_MULTILEVEL_LOOKUP: 0 -# -# - task: Palmmedia.reportgenerator.reportgenerator-build-release-task.reportgenerator@4 -# condition: and(succeededOrFailed(), eq(variables['CollectCoverage'], 'true')) -# displayName: Generate Code Coverage Reports -# inputs: -# reports: $(Build.SourcesDirectory)\src\dotnet\APIView\APIViewUnitTests\**\coverage.cobertura.xml -# targetdir: $(Build.ArtifactStagingDirectory)\coverage -# reporttypes: Cobertura -# filefilters: +$(Build.SourcesDirectory)\src\dotnet\APIView\** -# verbosity: Verbose -# -# - task: PublishCodeCoverageResults@1 -# condition: and(succeededOrFailed(), eq(variables['CollectCoverage'], 'true')) -# displayName: Publish Code Coverage Reports -# inputs: -# codeCoverageTool: Cobertura -# summaryFileLocation: $(Build.ArtifactStagingDirectory)\coverage\Cobertura.xml -# -# - script: >- -# dotnet test src/dotnet/APIView/APIViewIntegrationTests/APIViewIntegrationTests.csproj -# --logger trx -# displayName: "Build & Test (Integration)" -# env: -# DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 -# DOTNET_CLI_TELEMETRY_OPTOUT: 1 -# DOTNET_MULTILEVEL_LOOKUP: 0 -# APIVIEW_ENDPOINT: "http://localhost:5000" -# APIVIEW_BLOB__CONNECTIONSTRING: $(AzuriteConnectionString) -# APIVIEW_COSMOS__CONNECTIONSTRING: $(CosmosEmulatorConnectionString) -# -# - script: | -# npm install -# workingDirectory: $(WebClientProjectDirectory) -# displayName: "Install Client Dependencies" -# -# - script: | -# npx playwright install --with-deps -# workingDirectory: $(WebClientProjectDirectory) -# displayName: "Install Playwright Browsers" -# -# - script: | -# npx playwright test --project=unit-tests -# workingDirectory: $(WebClientProjectDirectory) -# displayName: "Run Client-Side Unit Tests" -# -# - task: PublishBuildArtifacts@1 -# inputs: -# pathtoPublish: '$(Build.SourcesDirectory)\src\dotnet\APIView\APIViewWeb\Client\playwright-report' -# artifactName: 'Client-Side Unit Test Reports' -# -# - ${{ if and(ne(variables['Build.Reason'], 'PullRequest'), eq(variables['System.TeamProject'], 'internal')) }}: -# - template: /eng/pipelines/templates/steps/apiview-ui-tests.yml -# parameters: -# NodeVersion: $(NodeVersion) -# WebClientProjectDirectory: $(WebClientProjectDirectory) -# AzuriteConnectionString: $(AzuriteConnectionString) -# CosmosEmulatorConnectionString: $(CosmosEmulatorConnectionString) -# -# - task: PublishTestResults@2 -# condition: succeededOrFailed() -# inputs: -# testResultsFiles: '**/*.trx' -# testRunTitle: 'Tests against Windows .NET' -# testResultsFormat: 'VSTest' -# mergeTestResults: true + - job: 'Test' + + pool: + name: azsdk-pool-mms-win-2022-general + vmImage: windows-2022 + + steps: + - template: /eng/common/pipelines/templates/steps/cosmos-emulator.yml + parameters: + StartParameters: '/noexplorer /noui /enablepreview /disableratelimiting /enableaadauthentication /partitioncount=50 /consistency=Strong' + + - script: | + npm install -g azurite + displayName: 'Install Azurite' + + - task: Powershell@2 + inputs: + workingDirectory: $(Agent.TempDirectory) + filePath: $(Build.SourcesDirectory)/eng/scripts/Start-LocalHostApp.ps1 + arguments: > + -Process "azurite.cmd" + -ArgumentList "--silent" + -Port "10000" + pwsh: true + displayName: 'Start Azurite' + + - template: /eng/pipelines/templates/steps/install-dotnet.yml + + - pwsh: | + dotnet --list-runtimes + dotnet --version + displayName: 'List .NET run times' + + - task: GoTool@0 + inputs: + version: '$(GoVersion)' + displayName: "Use Go $(GoVersion)" + + - script: | + go test ./... -v + workingDirectory: $(GoParserPackagePath) + displayName: 'Test Go parser' + + - script: >- + dotnet test src/dotnet/APIView/APIViewUnitTests/APIViewUnitTests.csproj + --logger trx --collect:"XPlat Code Coverage" + displayName: "Build & Test (Unit)" + env: + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 + DOTNET_CLI_TELEMETRY_OPTOUT: 1 + DOTNET_MULTILEVEL_LOOKUP: 0 + + - task: Palmmedia.reportgenerator.reportgenerator-build-release-task.reportgenerator@4 + condition: and(succeededOrFailed(), eq(variables['CollectCoverage'], 'true')) + displayName: Generate Code Coverage Reports + inputs: + reports: $(Build.SourcesDirectory)\src\dotnet\APIView\APIViewUnitTests\**\coverage.cobertura.xml + targetdir: $(Build.ArtifactStagingDirectory)\coverage + reporttypes: Cobertura + filefilters: +$(Build.SourcesDirectory)\src\dotnet\APIView\** + verbosity: Verbose + + - task: PublishCodeCoverageResults@1 + condition: and(succeededOrFailed(), eq(variables['CollectCoverage'], 'true')) + displayName: Publish Code Coverage Reports + inputs: + codeCoverageTool: Cobertura + summaryFileLocation: $(Build.ArtifactStagingDirectory)\coverage\Cobertura.xml + + #- script: >- + # dotnet test src/dotnet/APIView/APIViewIntegrationTests/APIViewIntegrationTests.csproj + # --logger trx + # displayName: "Build & Test (Integration)" + # env: + # DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 + # DOTNET_CLI_TELEMETRY_OPTOUT: 1 + # DOTNET_MULTILEVEL_LOOKUP: 0 + # APIVIEW_ENDPOINT: "http://localhost:5000" + # APIVIEW_BLOB__CONNECTIONSTRING: $(AzuriteConnectionString) + # APIVIEW_COSMOS__CONNECTIONSTRING: $(CosmosEmulatorConnectionString) + + - script: | + npm install + workingDirectory: $(WebClientProjectDirectory) + displayName: "Install Client Dependencies" + + - script: | + npx playwright install --with-deps + workingDirectory: $(WebClientProjectDirectory) + displayName: "Install Playwright Browsers" + + - script: | + npx playwright test --project=unit-tests + workingDirectory: $(WebClientProjectDirectory) + displayName: "Run Client-Side Unit Tests" + + - task: PublishBuildArtifacts@1 + inputs: + pathtoPublish: '$(Build.SourcesDirectory)\src\dotnet\APIView\APIViewWeb\Client\playwright-report' + artifactName: 'Client-Side Unit Test Reports' + + - ${{ if and(ne(variables['Build.Reason'], 'PullRequest'), eq(variables['System.TeamProject'], 'internal')) }}: + - template: /eng/pipelines/templates/steps/apiview-ui-tests.yml + parameters: + NodeVersion: $(NodeVersion) + WebClientProjectDirectory: $(WebClientProjectDirectory) + AzuriteConnectionString: $(AzuriteConnectionString) + CosmosEmulatorConnectionString: $(CosmosEmulatorConnectionString) + + - task: PublishTestResults@2 + condition: succeededOrFailed() + inputs: + testResultsFiles: '**/*.trx' + testRunTitle: 'Tests against Windows .NET' + testResultsFormat: 'VSTest' + mergeTestResults: true