Skip to content

Commit

Permalink
Add more data and features to the "Tracked PRs" page (#4341)
Browse files Browse the repository at this point in the history
  • Loading branch information
premun authored Jan 20, 2025
1 parent 3014143 commit 53f49d3
Show file tree
Hide file tree
Showing 22 changed files with 647 additions and 82 deletions.
5 changes: 5 additions & 0 deletions docs/DevGuide.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@
- Be sure to install the `Azure Development => .NET Aspire SDK (Preview)` optional workload in the VS installer
- Be sure to install the `ASP.NET and web development` => `.NET 8.0/9.0 WebAssembly Build Tools`
1. Install Docker Desktop: https://www.docker.com/products/docker-desktop
1. Configure git to support long paths:
```ps1
git config --system core.longpaths true # you will need elevated shell for this one
git config --global core.longpaths true
```
1. Install SQL Server Express: https://www.microsoft.com/en-us/sql-server/sql-server-downloads
1. Install Entity Framework Core CLI by running `dotnet tool install --global dotnet-ef`
1. Build the `src\Maestro\Maestro.Data\Maestro.Data.csproj` project (either from console or from IDE)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using Newtonsoft.Json;

namespace Microsoft.DotNet.ProductConstructionService.Client.Models
{
public partial class PullRequestUpdate
{
public PullRequestUpdate()
public PullRequestUpdate(Guid subscriptionId, int buildId)
{
SubscriptionId = subscriptionId;
BuildId = buildId;
}

[JsonProperty("sourceRepository")]
public string SourceRepository { get; set; }

[JsonProperty("subscriptionId")]
public Guid SubscriptionId { get; set; }

[JsonProperty("buildId")]
public int BuildId { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using Newtonsoft.Json;

namespace Microsoft.DotNet.ProductConstructionService.Client.Models
{
public partial class TrackedPullRequest
{
public TrackedPullRequest()
public TrackedPullRequest(bool sourceEnabled, DateTimeOffset lastUpdate, DateTimeOffset lastCheck)
{
SourceEnabled = sourceEnabled;
LastUpdate = lastUpdate;
LastCheck = lastCheck;
}

[JsonProperty("id")]
public string Id { get; set; }

[JsonProperty("url")]
public string Url { get; set; }

Expand All @@ -21,6 +28,18 @@ public TrackedPullRequest()
[JsonProperty("targetBranch")]
public string TargetBranch { get; set; }

[JsonProperty("sourceEnabled")]
public bool SourceEnabled { get; set; }

[JsonProperty("lastUpdate")]
public DateTimeOffset LastUpdate { get; set; }

[JsonProperty("lastCheck")]
public DateTimeOffset LastCheck { get; set; }

[JsonProperty("nextCheck")]
public DateTimeOffset? NextCheck { get; set; }

[JsonProperty("updates")]
public List<PullRequestUpdate> Updates { get; set; }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ public partial interface IPullRequest
CancellationToken cancellationToken = default
);

Task UntrackPullRequestAsync(
string id,
CancellationToken cancellationToken = default
);

}

internal partial class PullRequest : IServiceOperations<ProductConstructionServiceApi>, IPullRequest
Expand Down Expand Up @@ -100,5 +105,66 @@ internal async Task OnGetTrackedPullRequestsFailed(Request req, Response res)
Client.OnFailedRequest(ex);
throw ex;
}

partial void HandleFailedUntrackPullRequestRequest(RestApiException ex);

public async Task UntrackPullRequestAsync(
string id,
CancellationToken cancellationToken = default
)
{

const string apiVersion = "2020-02-20";

var _baseUri = Client.Options.BaseUri;
var _url = new RequestUriBuilder();
_url.Reset(_baseUri);
_url.AppendPath(
"/api/pull-requests/tracked/{id}".Replace("{id}", Uri.EscapeDataString(Client.Serialize(id))),
false);

_url.AppendQuery("api-version", Client.Serialize(apiVersion));


using (var _req = Client.Pipeline.CreateRequest())
{
_req.Uri = _url;
_req.Method = RequestMethod.Delete;

using (var _res = await Client.SendAsync(_req, cancellationToken).ConfigureAwait(false))
{
if (_res.Status < 200 || _res.Status >= 300)
{
await OnUntrackPullRequestFailed(_req, _res).ConfigureAwait(false);
}


return;
}
}
}

internal async Task OnUntrackPullRequestFailed(Request req, Response res)
{
string content = null;
if (res.ContentStream != null)
{
using (var reader = new StreamReader(res.ContentStream))
{
content = await reader.ReadToEndAsync().ConfigureAwait(false);
}
}

var ex = new RestApiException<Models.ApiError>(
req,
res,
content,
Client.Deserialize<Models.ApiError>(content)
);
HandleFailedUntrackPullRequestRequest(ex);
HandleFailedRequest(ex);
Client.OnFailedRequest(ex);
throw ex;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,23 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using Newtonsoft.Json.Linq;
using System.Net;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using Azure.Core;
using Newtonsoft.Json.Linq;

#nullable enable
namespace Microsoft.DotNet.ProductConstructionService.Client
{
public partial interface IProductConstructionServiceApi
{
Task<bool> IsAdmin(CancellationToken cancellationToken = default);
}

public partial class ProductConstructionServiceApi
{
// Special error handler to consumes the generated MaestroApi code. If this method returns without throwing a specific exception
Expand Down Expand Up @@ -40,6 +48,33 @@ partial void HandleFailedRequest(RestApiException ex)
"Please make sure the PAT you're using is valid.");
}
}

public async Task<bool> IsAdmin(CancellationToken cancellationToken = default)
{
var url = new RequestUriBuilder();
url.Reset(Options.BaseUri);
url.AppendPath("/Account", false);

using (var request = Pipeline.CreateRequest())
{
request.Uri = url;
request.Method = RequestMethod.Get;

using (var response = await SendAsync(request, cancellationToken).ConfigureAwait(false))
{
if (response.Status < 200 || response.Status >= 300 || response.ContentStream == null)
{
throw new RestApiException(request, response, "Invalid response");
}

using (var _reader = new StreamReader(response.ContentStream))
{
var content = await _reader.ReadToEndAsync().ConfigureAwait(false);
return content.Trim() == "Admin";
}
}
}
}
}

internal partial class ProductConstructionServiceApiResponseClassifier
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
using Maestro.Data;
using Microsoft.AspNetCore.ApiVersioning;
using Microsoft.AspNetCore.ApiVersioning.Swashbuckle;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.DotNet.DarcLib.Helpers;
using Microsoft.EntityFrameworkCore;
using ProductConstructionService.Api.Configuration;
using ProductConstructionService.Common;
using ProductConstructionService.DependencyFlow;

Expand Down Expand Up @@ -57,10 +59,11 @@ public PullRequestController(
[ValidateModelState]
public async Task<IActionResult> GetTrackedPullRequests()
{
var cache = _cacheFactory.Create(nameof(InProgressPullRequest) + "_");
var keyPrefix = nameof(InProgressPullRequest) + "_";
var cache = _cacheFactory.Create(keyPrefix);

var prs = new List<TrackedPullRequest>();
await foreach (var key in cache.GetKeysAsync(nameof(InProgressPullRequest) + "_*"))
await foreach (var key in cache.GetKeysAsync(keyPrefix + "*"))
{
var pr = await _cacheFactory
.Create<InProgressPullRequest>(key, includeTypeInKey: false)
Expand Down Expand Up @@ -99,19 +102,36 @@ public async Task<IActionResult> GetTrackedPullRequests()
var updates = subscriptions
.Select(update => new PullRequestUpdate(
TurnApiUrlToWebsite(update.SourceRepository, org, repoName),
pr.ContainedSubscriptions.First(s => s.SubscriptionId == update.Id).SubscriptionId,
pr.ContainedSubscriptions.First(s => s.SubscriptionId == update.Id).BuildId))
.ToList();

prs.Add(new TrackedPullRequest(
key.Replace(keyPrefix, null, StringComparison.InvariantCultureIgnoreCase),
TurnApiUrlToWebsite(pr.Url, org, repoName),
sampleSub?.Channel?.Name,
sampleSub?.TargetBranch,
sampleSub?.SourceEnabled ?? false,
pr.LastUpdate,
pr.LastCheck,
pr.NextCheck,
updates));
}

return Ok(prs.AsQueryable());
}

[HttpDelete("tracked/{id}")]
[Authorize(Policy = AuthenticationConfiguration.AdminAuthorizationPolicyName)]
[SwaggerApiResponse(HttpStatusCode.OK, Type = typeof(void), Description = "The pull request was successfully untracked")]
[SwaggerApiResponse(HttpStatusCode.NotFound, Type = typeof(void), Description = "The pull request was not found in the list of tracked pull requests")]
[ValidateModelState]
public async Task<IActionResult> UntrackPullRequest(string id)
{
var cache = _cacheFactory.Create<InProgressPullRequest>($"{nameof(InProgressPullRequest)}_{id}", includeTypeInKey: false);
return await cache.TryDeleteAsync() == null ? NotFound() : Ok();
}

private static string TurnApiUrlToWebsite(string url, string? orgName, string? repoName)
{
var match = GitHubApiPrUrlRegex().Match(url);
Expand Down Expand Up @@ -139,12 +159,18 @@ private static string TurnApiUrlToWebsite(string url, string? orgName, string? r
}

private record TrackedPullRequest(
string Id,
string Url,
string? Channel,
string? TargetBranch,
bool SourceEnabled,
DateTime LastUpdate,
DateTime LastCheck,
DateTime? NextCheck,
List<PullRequestUpdate> Updates);

private record PullRequestUpdate(
string SourceRepository,
Guid SubscriptionId,
int BuildId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

namespace ProductConstructionService.Api.Configuration;

public static class AuthenticationConfiguration
internal static class AuthenticationConfiguration
{
public const string EntraAuthorizationPolicyName = "Entra";
public const string MsftAuthorizationPolicyName = "msft";
Expand Down Expand Up @@ -87,20 +87,18 @@ public static void ConfigureAuthServices(this IServiceCollection services, IConf
});

services
.AddAuthorization(options =>
{
options.AddPolicy(MsftAuthorizationPolicyName, policy =>
.AddAuthorizationBuilder()
.AddPolicy(MsftAuthorizationPolicyName, policy =>
{
policy.AddAuthenticationSchemes(AuthenticationSchemes);
policy.RequireAuthenticatedUser();
policy.RequireRole(userRole);
});
options.AddPolicy(AdminAuthorizationPolicyName, policy =>
})
.AddPolicy(AdminAuthorizationPolicyName, policy =>
{
policy.AddAuthenticationSchemes(AuthenticationSchemes);
policy.RequireAuthenticatedUser();
policy.RequireRole("Admin");
policy.RequireRole(adminRole);
});
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,15 @@ public IActionResult SignIn(string? returnUrl = null)
new AuthenticationProperties() { RedirectUri = returnUrl },
OpenIdConnectDefaults.AuthenticationScheme);
}

[HttpGet("/Account")]
[Authorize]
public IActionResult Account()
{
#if DEBUG
return Ok("Admin");
#else
return Ok(HttpContext.User.IsInRole("Admin") ? "Admin" : "User");
#endif
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@
</NotFound>
</Router>

<FluentMenuProvider />
<FluentTooltipProvider />
<FluentToastProvider />

@code {
private IDisposable? _navigationHandlerRegistration = null;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.DotNet.ProductConstructionService.Client;

namespace ProductConstructionService.BarViz.Code.Services;

public class UserRoleManager(IProductConstructionServiceApi pcsApi)
{
private readonly Lazy<Task<bool>> _isAdmin = new(
() => pcsApi.IsAdmin(),
LazyThreadSafetyMode.ExecutionAndPublication);

public Task<bool> IsAdmin => _isAdmin.Value;
}
Loading

0 comments on commit 53f49d3

Please sign in to comment.