Skip to content
This repository was archived by the owner on Jun 5, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 26 additions & 34 deletions src/AzureExtension/DataManager/AzureDataManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -627,6 +627,12 @@ await ProcessPullRequests(
return;
}

public IEnumerable<PullRequests> GetPullRequestsForLoggedInDeveloperIds()
{
ValidateDataStore();
return PullRequests.GetAllForDeveloper(DataStore);
}

public async Task UpdatePullRequestsForLoggedInDeveloperIdsAsync(RequestOptions? options = null, Guid? requestor = null)
{
ValidateDataStore();
Expand All @@ -653,50 +659,35 @@ public async Task UpdatePullRequestsForLoggedInDeveloperIdsAsync(RequestOptions?
return;
}

SendPullRequestUpdateEvent(_log, this, parameters.Requestor, context);
SendDeveloperUpdateEvent(_log, this, parameters.Requestor, context);
}

private async Task UpdateDataForDeveloperPullRequestsAsync(DataStoreOperationParameters parameters)
{
_log.Debug($"Inside UpdateDataForDeveloperPullRequestsAsync with Parameters: {parameters}");

dynamic context = new ExpandoObject();
var contextDict = (IDictionary<string, object>)context;
contextDict.Add("Requestor", parameters.Requestor);

try
// This is a loop over a subset of repositories with a specific developer ID and pull request view specified.
var repositoryReferences = RepositoryReference.GetAll(DataStore);
foreach (var repositoryRef in repositoryReferences)
{
// This is a loop over a subset of repositories with a specific developer ID and pull request view specified.
var repositoryReferences = RepositoryReference.GetAll(DataStore);
foreach (var repositoryRef in repositoryReferences)
var uri = new AzureUri(repositoryRef.Repository.CloneUrl);
var uris = new List<AzureUri>
{
var uri = new AzureUri(repositoryRef.Repository.CloneUrl);
var uris = new List<AzureUri>
{
new(repositoryRef.Repository.CloneUrl),
};
new(repositoryRef.Repository.CloneUrl),
};

var suboperationParameters = new DataStoreOperationParameters
{
Uris = uris,
DeveloperId = repositoryRef.Developer.DeveloperId,
RequestOptions = parameters.RequestOptions,
OperationName = nameof(UpdateDataForDeveloperPullRequestsAsync),
PullRequestView = PullRequestView.Mine,
Requestor = parameters.Requestor,
};

await UpdateDataForPullRequestsAsync(suboperationParameters);
}
}
catch (Exception ex)
{
contextDict.Add("ErrorMessage", ex.Message);
SendErrorUpdateEvent(_log, this, parameters.Requestor, context, ex);
return;
}
var suboperationParameters = new DataStoreOperationParameters
{
Uris = uris,
DeveloperId = repositoryRef.Developer.DeveloperId,
RequestOptions = parameters.RequestOptions,
OperationName = nameof(UpdateDataForDeveloperPullRequestsAsync),
PullRequestView = PullRequestView.Mine,
Requestor = parameters.Requestor,
};

SendDeveloperUpdateEvent(_log, this, parameters.Requestor, context);
await UpdateDataForPullRequestsAsync(suboperationParameters);
}
}

private async Task ProcessPullRequests(
Expand Down Expand Up @@ -769,6 +760,7 @@ private async Task ProcessPullRequests(

pullRequestObjFields.Add("Id", pullRequest.PullRequestId);
pullRequestObjFields.Add("Title", pullRequest.Title);
pullRequestObjFields.Add("RepositoryId", repository.Id);
pullRequestObjFields.Add("Status", pullRequest.Status);
pullRequestObjFields.Add("PolicyStatus", status.ToString());
pullRequestObjFields.Add("PolicyStatusReason", statusReason);
Expand Down
19 changes: 19 additions & 0 deletions src/AzureExtension/DataManager/AzureDataManagerCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ public partial class AzureDataManager

private static readonly int _pullRequestRepositoryLimit = 25;

private static readonly TimeSpan _orgUpdateDelayTime = TimeSpan.FromSeconds(5);

public async Task UpdateDataForAccountsAsync(RequestOptions? options = null, Guid? requestor = null)
{
// A parameterless call will always update the data, effectively a 'force update'.
Expand Down Expand Up @@ -104,11 +106,23 @@ public async Task UpdateDataForAccountsAsync(TimeSpan olderThan, RequestOptions?
continue;
}

var orgStartTime = DateTime.UtcNow;

UpdateOrganization(account, developerId, connection, cancellationToken);

tx.Commit();
_log.Information($"Updated organization: {account.AccountName}");

// Send an update event once the transaction is completed so anyone waiting on the DB to be
// available has an opportunity to use it.
var orgUpdateContext = CreateUpdateEventContext(0, 1, 0, DateTime.UtcNow - orgStartTime);
SendAccountUpdateEvent(_log, this, requestorGuid, orgUpdateContext, firstException);
++accountsUpdated;

// Delay to allow widgets and other code to respond to the event and use the database.
// This is to prevent DOS'ing widgets using the datastore during large cache updates of
// many organizations.
await Task.Delay(_orgUpdateDelayTime, cancellationToken);
}
catch (Exception ex) when (IsCancelException(ex))
{
Expand Down Expand Up @@ -313,6 +327,11 @@ private static void SendCacheUpdateEvent(ILogger logger, object? source, Guid re
SendUpdateEvent(logger, source, DataManagerUpdateKind.Cache, requestor, context, ex);
}

private static void SendAccountUpdateEvent(ILogger logger, object? source, Guid requestor, dynamic context, Exception? ex)
{
SendUpdateEvent(logger, source, DataManagerUpdateKind.Account, requestor, context, ex);
}

private static bool IsCancelException(Exception ex)
{
return (ex is OperationCanceledException) || (ex is TaskCanceledException);
Expand Down
13 changes: 12 additions & 1 deletion src/AzureExtension/DataManager/CacheManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public class CacheManager : IDisposable
private static readonly string _cacheManagerLastUpdatedMetaDataKey = "CacheManagerLastUpdated";

// Frequency the CacheManager checks for an update.
private static readonly TimeSpan _updateInterval = TimeSpan.FromHours(4);
private static readonly TimeSpan _updateInterval = TimeSpan.FromMinutes(15);

private static readonly TimeSpan _defaultAccountUpdateFrequency = TimeSpan.FromDays(3);

Expand All @@ -32,6 +32,8 @@ public class CacheManager : IDisposable

public bool UpdateInProgress { get; private set; }

public bool NeverUpdated => LastUpdated == DateTime.MinValue;

public DateTime LastUpdated
{
get => GetLastUpdated();
Expand Down Expand Up @@ -193,6 +195,15 @@ private void HandleDataManagerUpdate(object? source, DataManagerUpdateEventArgs
Log.Debug("DataManager update received");
switch (e.Kind)
{
case DataManagerUpdateKind.Account:
// Account is sent after each organization has been updated, but the entire cache
// is not necessarily updated. We will treat this as an update is still in progress, and
// notify others who may be waiting for an opportunity to query the database.
// Receiving this event means a transaction was just completed and the datastore is
// briefly unlocked for queries.
SendUpdateEvent(this, CacheManagerUpdateKind.Account);
break;

case DataManagerUpdateKind.Cache:
lock (_stateLock)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public enum CacheManagerUpdateKind
Cleared,
Error,
Cancel,
Account,
}

public class CacheManagerUpdateEventArgs : EventArgs
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ public enum DataManagerUpdateKind
PullRequest,
Error,
Cache,
Account,
Cancel,
Developer,
}
Expand Down
2 changes: 2 additions & 0 deletions src/AzureExtension/DataManager/IAzureDataManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ public interface IAzureDataManager : IDisposable

IEnumerable<Repository> GetDeveloperRepositories();

IEnumerable<PullRequests> GetPullRequestsForLoggedInDeveloperIds();

// Repository name may not be unique across projects, and projects may not be unique across
// organizations, so we need all three to identify the repository.
PullRequests? GetPullRequests(string organization, string project, string repositoryName, string developerId, PullRequestView view);
Expand Down
17 changes: 17 additions & 0 deletions src/AzureExtension/DataModel/DataObjects/PullRequests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,23 @@ public static PullRequests Get(DataStore dataStore, long id)
return Get(dataStore, project.Id, repository.Id, developerLogin, view);
}

public static IEnumerable<PullRequests> GetAllForDeveloper(DataStore dataStore)
{
var sql = @"SELECT * FROM PullRequests WHERE ViewId = @ViewId;";
var param = new
{
ViewId = (long)PullRequestView.Mine,
};

var pullRequestsSet = dataStore.Connection!.Query<PullRequests>(sql, param, null) ?? [];
foreach (var pullRequestsEntry in pullRequestsSet)
{
pullRequestsEntry.DataStore = dataStore;
}

return pullRequestsSet;
}

public static PullRequests GetOrCreate(DataStore dataStore, long repositoryId, long projectId, string developerId, PullRequestView view, string pullRequests)
{
var newDeveloperPullRequests = Create(repositoryId, projectId, developerId, view, pullRequests);
Expand Down
4 changes: 4 additions & 0 deletions src/AzureExtension/Strings/en-US/Resources.resw
Original file line number Diff line number Diff line change
Expand Up @@ -726,4 +726,8 @@
<value>Rejected</value>
<comment>Shown in Toast Notification, title line</comment>
</data>
<data name="Widget_Template.LoadingFindingPullRequests" xml:space="preserve">
<value>Finding your pull requests. This may take several minutes.</value>
<comment>Shown in Widget</comment>
</data>
</root>
126 changes: 126 additions & 0 deletions src/AzureExtension/Widgets/AzurePullRequestsBaseWidget.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Text.Json.Nodes;
using DevHomeAzureExtension.Client;
using DevHomeAzureExtension.DataManager;
using DevHomeAzureExtension.DataModel;
using DevHomeAzureExtension.DeveloperId;
using DevHomeAzureExtension.Helpers;
using Microsoft.Windows.Widgets.Providers;
using Newtonsoft.Json;

namespace DevHomeAzureExtension.Widgets;

internal abstract class AzurePullRequestsBaseWidget : AzureWidget
{
private readonly string _sampleIconData = IconLoader.GetIconAsBase64("screenshot.png");

// Widget Data
protected string WidgetTitle { get; set; } = string.Empty;

protected string? Message { get; set; }

// Creation and destruction methods
public AzurePullRequestsBaseWidget()
: base()
{
}

public override void CreateWidget(WidgetContext widgetContext, string state)
{
if (state.Length != 0)
{
ResetDataFromState(state);
}

base.CreateWidget(widgetContext, state);
}

public override void DeleteWidget(string widgetId, string customState)
{
base.DeleteWidget(widgetId, customState);
}

protected string GetIconForPullRequestStatus(string? prStatus)
{
prStatus ??= string.Empty;
if (Enum.TryParse<PolicyStatus>(prStatus, false, out var policyStatus))
{
return policyStatus switch
{
PolicyStatus.Approved => IconLoader.GetIconAsBase64("PullRequestApproved.png"),
PolicyStatus.Running => IconLoader.GetIconAsBase64("PullRequestWaiting.png"),
PolicyStatus.Queued => IconLoader.GetIconAsBase64("PullRequestWaiting.png"),
PolicyStatus.Rejected => IconLoader.GetIconAsBase64("PullRequestRejected.png"),
PolicyStatus.Broken => IconLoader.GetIconAsBase64("PullRequestRejected.png"),
_ => IconLoader.GetIconAsBase64("PullRequestReviewNotStarted.png"),
};
}

return string.Empty;
}

protected override bool ValidateConfiguration(WidgetActionInvokedArgs args)
{
return true;
}

// Increase precision of SetDefaultDeveloperLoginId by matching the selectedRepositoryUrl's org
// with the first matching DeveloperId that contains that org.
protected override void SetDefaultDeveloperLoginId()
{
base.SetDefaultDeveloperLoginId();
}

// Data loading methods
public override void HandleDataManagerUpdate(object? source, DataManagerUpdateEventArgs e)
{
return;
}

public override void RequestContentData()
{
return;
}

protected override void ResetDataFromState(string data)
{
return;
}

public override string GetConfiguration(string data)
{
return EmptyJson;
}

public override void LoadContentData()
{
return;
}

// Overriding methods from Widget base
public override string GetTemplatePath(WidgetPageState page)
{
return page switch
{
WidgetPageState.SignIn => @"Widgets\Templates\AzureSignInTemplate.json",
WidgetPageState.Configure => @"Widgets\Templates\AzurePullRequestsConfigurationTemplate.json",
WidgetPageState.Content => @"Widgets\Templates\AzurePullRequestsTemplate.json",
WidgetPageState.Loading => @"Widgets\Templates\AzureLoadingTemplate.json",
_ => throw new NotImplementedException(Page.GetType().Name),
};
}

public override string GetData(WidgetPageState page)
{
return page switch
{
WidgetPageState.SignIn => GetSignIn(),
WidgetPageState.Configure => GetConfiguration(string.Empty),
WidgetPageState.Content => ContentData,
WidgetPageState.Loading => GetLoadingMessage(),
_ => throw new NotImplementedException(Page.GetType().Name),
};
}
}
Loading