Re architect/review revision restructure (#7390)

* Add alert for displaying page errors, add logic for manual reviews

* Make Associated PRs and Revies Dependent on APIRevision ID

* Update Database name

* Disable Pipeline Tests
This commit is contained in:
Chidozie Ononiwu 2023-12-05 10:49:08 -08:00 коммит произвёл GitHub
Родитель 8a10ab0e04
Коммит c660060dda
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
19 изменённых файлов: 398 добавлений и 201 удалений

Просмотреть файл

@ -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 }));
}

Просмотреть файл

@ -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();

Просмотреть файл

@ -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 = "<br><span>.....</span><br>", HashSet<int> 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;
}

Просмотреть файл

@ -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<string> PreferredApprovers = new HashSet<string>();
public HashSet<GithubUser> TaggableUsers { get; set; }
public bool PageHasLoadableSections { get; set; }
public string NotificationMessage { get; set; }
public ReviewContentModelDirective Directive { get; set; }
}
}

Просмотреть файл

@ -373,7 +373,7 @@ namespace APIViewWeb.Managers
/// <param name="language"></param>
/// <param name="awaitComputeDiff"></param>
/// <returns></returns>
public async Task AddAPIRevisionAsync(
public async Task<APIRevisionListItemModel> 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);
}

Просмотреть файл

@ -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
/// </summary>
/// <param name="apiRevisionId"></param>
/// <param name="originalName"></param>
/// <param name="fileStream"></param>
/// <param name="runAnalysis"></param>
/// <param name="fileStream"></param>
/// <param name="language"></param>
/// <returns></returns>
public async Task<APICodeFileModel> 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
/// </summary>
/// <param name="originalName"></param>
/// <param name="fileStream"></param>
/// <param name="runAnalysis"></param>
/// <param name="memoryStream"></param>
/// <param name="fileStream"></param>
/// <param name="language"></param>
/// <returns></returns>
public async Task<CodeFile> 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)));

Просмотреть файл

@ -22,7 +22,7 @@ namespace APIViewWeb.Managers.Interfaces
string label = null, int? prNumber = null, string createdBy = "azure-sdk");
public Task<bool> 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<APIRevisionListItemModel> AddAPIRevisionAsync(ClaimsPrincipal user, ReviewListItemModel review, APIRevisionType apiRevisionType, string name, string label, Stream fileStream, string language, bool awaitComputeDiff = false);
public Task RunAPIRevisionGenerationPipeline(List<APIRevisionGenerationPipelineParamModel> reviewGenParams, string language);
public Task SoftDeleteAPIRevisionAsync(ClaimsPrincipal user, string reviewId, string revisionId);
public Task SoftDeleteAPIRevisionAsync(ClaimsPrincipal user, APIRevisionListItemModel apiRevision);

Просмотреть файл

@ -9,8 +9,8 @@ namespace APIViewWeb.Managers.Interfaces
{
public Task<CodeFile> 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<APICodeFileModel> CreateCodeFileAsync(string apiRevisionId, string originalName, Stream fileStream, bool runAnalysis, string language);
public Task<CodeFile> CreateCodeFileAsync(string originalName, Stream fileStream, bool runAnalysis, MemoryStream memoryStream, string language = null);
public Task<APICodeFileModel> CreateCodeFileAsync(string apiRevisionId, string originalName, bool runAnalysis, Stream fileStream = null, string language = null);
public Task<CodeFile> CreateCodeFileAsync(string originalName, bool runAnalysis, MemoryStream memoryStream, Stream fileStream = null, string language = null);
public Task<APICodeFileModel> CreateReviewCodeFileModel(string apiRevisionId, MemoryStream memoryStream, CodeFile codeFile);
public bool IsAPICodeFilesTheSame(RenderedCodeFile codeFileA, RenderedCodeFile codeFileB);
}

Просмотреть файл

@ -6,7 +6,7 @@ namespace APIViewWeb.Managers
{
public interface IPullRequestManager
{
public Task<IEnumerable<PullRequestModel>> GetPullRequestsModelAsync(string reviewId);
public Task<IEnumerable<PullRequestModel>> GetPullRequestsModelAsync(string reviewId, string apiRevisionId = null);
public Task<IEnumerable<PullRequestModel>> GetPullRequestsModelAsync(int pullRequestNumber, string repoName);
public Task<PullRequestModel> GetPullRequestModelAsync(int prNumber, string repoName, string packageName, string originalFile, string language);
public Task CreateOrUpdateCommentsOnPR(List<PullRequestModel> pullRequests, string repoOwner, string repoName, int prNumber, string hostName);

Просмотреть файл

@ -53,8 +53,8 @@ namespace APIViewWeb.Managers
_pullRequestCleanupDays = int.Parse(pullRequestReviewCloseAfter);
}
public async Task<IEnumerable<PullRequestModel>> GetPullRequestsModelAsync(string reviewId) {
return await _pullRequestsRepository.GetPullRequestsAsync(reviewId);
public async Task<IEnumerable<PullRequestModel>> GetPullRequestsModelAsync(string reviewId, string apiRevisionId = null) {
return await _pullRequestsRepository.GetPullRequestsAsync(reviewId, apiRevisionId);
}
public async Task<IEnumerable<PullRequestModel>> GetPullRequestsModelAsync(int pullRequestNumber, string repoName)

Просмотреть файл

@ -71,8 +71,74 @@
if (userPreference.HideIndexPageOptions.HasValue && userPreference.HideIndexPageOptions == true)
mainContainerClass = String.Empty;
}
<div class="container-fluid mb-5@(mainContainerClass)" id="index-main-container">
<div class="modal fade" id="uploadModel" tabindex="-1" role="dialog">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<form asp-page-handler="Upload" method="post" enctype="multipart/form-data" id="review-upload-form">
<div class="modal-header">
<h5 class="modal-title">Create Review</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="container-fluid">
<div class="row">
<div class="form-group mb-3">
<p class="small mb-0">Select Language</p>
<select asp-for="Upload.Language" id="review-language-select">
<option selected disabled value="">None Selected</option>
@foreach (string lang in UploadModel.SupportedLanguages)
{
<option value=@lang data-content=@lang>@lang</option>
}
</select>
</div>
<div class="input-group mb-3 custom-file-label" id="create-review-via-upload">
<label for="uploadReviewFile" class="input-group-text small mb-0">Select File to Include in API Review</label>
<input asp-for="Upload.Files" type="file" class="form-control" id="uploadReviewFile">
</div>
<div class="form-group d-none mb-3" id="create-review-via-path">
<input asp-for="Upload.FilePath" class="form-control" type="text" placeholder="Package root e.g https://github.com/Azure/azure-rest-api-specs/specification/cognitiveservices/AnomalyDetector/">
</div>
<div class="form-group mb-3 d-none">
<div class="form-check">
<input class="form-check-input" hidden asp-for="Upload.RunAnalysis" checked>
<label class="form-check-label" hidden>
Run static analysis
</label>
</div>
</div>
<div class="form-group mb-3">
<input asp-for="Label" class="form-control" type="text" placeholder="Enter an optional review label">
</div>
<div class="form-group">
<partial name="_ReviewUploadHelp" />
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Close</button>
<button id="review-upload-submit-btn" class="btn btn-primary">
<i class="fas fa-cloud-upload-alt"></i> Upload
</button>
</div>
</form>
</div>
</div>
</div>
<button type="button" class="btn btn-primary" id="create-review-button" data-bs-toggle="modal" data-bs-target="#uploadModel" data-bs-backdrop="false"><small>CREATE</small><br><i class="fas fa-plus"></i><br><small>REVIEW</small></button>
<div class="mx-5 row">
@if (!string.IsNullOrEmpty(Model.NotificationMessage))
{
<div class="alert alert-warning alert-dismissible fade show mt-1" role="alert">
@Model.NotificationMessage
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
}
<div class="col-md-12" id="reviews-filter-partial">
<partial name="_ReviewsPartial" model="Model.PagedResults" />
</div>

Просмотреть файл

@ -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<SignalRHub> _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<SignalRHub> notificationHub)
public IndexPageModel(IReviewManager reviewManager, IAPIRevisionsManager apiRevisionsManager, IUserProfileManager userProfileManager,
UserPreferenceCache preferenceCache, IHubContext<SignalRHub> 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<ReviewListItemModel> 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<string> search, IEnumerable<string> languages, IEnumerable<string> state,
@ -66,6 +81,79 @@ namespace APIViewWeb.Pages.Assemblies
return Partial("_ReviewsPartial", PagedResults);
}
public async Task<IActionResult> 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<ReviewListItemModel> 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<string> search, IEnumerable<string> languages,
IEnumerable<string> state, IEnumerable<string> status, int pageNo, int pageSize, string sortField, bool fromUrl = true)
{

Просмотреть файл

@ -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";
}
<p class="h6">
<a data-bs-toggle="collapse" href="#associatedReviewsCollapse" aria-expanded="true" aria-controls="associatedReviewsCollapse">Associated Reviews&nbsp;&nbsp;<i class="fa-solid fa-ellipsis"></i></a>
<a data-bs-toggle="collapse" href="#associatedReviewsCollapse" aria-expanded="true" aria-controls="associatedReviewsCollapse">Associated APIRevisions&nbsp;&nbsp;<i class="fa-solid fa-ellipsis"></i></a>
</p>
<ul class="list-group collapse mb-3@(associatedReviewsState)" id="associatedReviewsCollapse">
@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
});
<li class="list-group-item">
<a href="@url" target="_blank">@pr.Language/@pr.PackageName</a>
@ -325,21 +326,6 @@
</form>
</div>
</li>
<li class="list-group-item">
<div class="form-check form-switch">
<form asp-resource="@Model.ReviewContent.Review" class="form-inline" id="reviewCloseForm" method="post" asp-page-handler="ToggleClosed">
@if (Model.ReviewContent.Review.IsClosed)
{
<input class="form-check-input" checked type="checkbox" role="switch" id="reviewCloseSwitch">
}
else
{
<input class="form-check-input" type="checkbox" role="switch" id="reviewCloseSwitch">
}
<label class="form-check-label" for="reviewCloseSwitch">Close Review</label>
</form>
</div>
</li>
</ul>
<p class="h6">
<a data-bs-toggle="collapse" href="#pageSettingsCollapse" aria-expanded="true" aria-controls="approvalCollapse">Page Settings&nbsp;&nbsp;<i class="fa-solid fa-ellipsis"></i></a>
@ -607,6 +593,13 @@
</div>
<div id="review-right" class="col-@reviewRightSize @reviewApprovedClass">
@if (!string.IsNullOrEmpty(Model.NotificationMessage))
{
<div class="alert alert-warning alert-dismissible fade show m-1" role="alert">
@Model.NotificationMessage
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
}
<table class="code-window">
<tbody>
@foreach (var line in Model.ReviewContent.codeLines)

Просмотреть файл

@ -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; }
/// <summary>
/// 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
/// <returns></returns>
public async Task<IEnumerable<PullRequestModel>> GetAssociatedPullRequest()
{
return await _pullRequestManager.GetPullRequestsModelAsync(ReviewContent.Review.Id);
return await _pullRequestManager.GetPullRequestsModelAsync(reviewId: ReviewContent.Review.Id, apiRevisionId: ReviewContent.ActiveAPIRevision.Id);
}
/// <summary>
@ -323,8 +332,12 @@ namespace APIViewWeb.Pages.Assemblies
/// <returns></returns>
public async Task<IEnumerable<PullRequestModel>> 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<PullRequestModel>();
}
/// <summary>

Просмотреть файл

@ -24,7 +24,7 @@
</div>
<div class="container" id="revisions-main-container">
<div class="row" asp-resource="@Model.LatestAPIRevision" asp-requirement="@AutoAPIRevisionModifierRequirement.Instance">
<div class="row">
<div id="add-revision-button">
<button type="button" class="btn btn-primary" id="add-revision-button" data-bs-toggle="modal" data-bs-target="#uploadModel"><small>ADD</small><br><i class="fas fa-sm fa-plus"></i><br><small>REVISION</small></button>
</div>

Просмотреть файл

@ -51,8 +51,13 @@ namespace APIViewWeb
return await GetPullRequestFromQueryAsync(query);
}
public async Task<IEnumerable<PullRequestModel>> GetPullRequestsAsync(string reviewId) {
public async Task<IEnumerable<PullRequestModel>> 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);
}

Просмотреть файл

@ -69,7 +69,16 @@ namespace APIViewWeb
public async Task<LegacyReviewModel> GetLegacyReviewAsync(string reviewId)
{
return await _legacyReviewsContainer.ReadItemAsync<LegacyReviewModel>(reviewId, new PartitionKey(reviewId));
var review = default(LegacyReviewModel);
try
{
review = await _legacyReviewsContainer.ReadItemAsync<LegacyReviewModel>(reviewId, new PartitionKey(reviewId));
}
catch (CosmosException ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
{
return review;
}
return review;
}
public async Task<ReviewListItemModel> GetReviewAsync(string language, string packageName, bool? isClosed = false)

Просмотреть файл

@ -7,7 +7,7 @@ namespace APIViewWeb.Repositories
public interface ICosmosPullRequestsRepository
{
public Task<PullRequestModel> GetPullRequestAsync(int pullRequestNumber, string repoName, string packageName, string language = null);
public Task<IEnumerable<PullRequestModel>> GetPullRequestsAsync(string reviewId);
public Task<IEnumerable<PullRequestModel>> GetPullRequestsAsync(string reviewId, string apiRevisionId = null);
public Task UpsertPullRequestAsync(PullRequestModel pullRequestModel);
public Task<IEnumerable<PullRequestModel>> GetPullRequestsAsync(bool isOpen);
public Task<List<PullRequestModel>> GetPullRequestsAsync(int pullRequestNumber, string repoName);

Просмотреть файл

@ -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