From fb1f84b85be92b9672c0814f2353ce4cf57fd699 Mon Sep 17 00:00:00 2001
From: Jonathan Idland Olsnes <73334350+Jonathanio123@users.noreply.github.com>
Date: Tue, 12 Nov 2024 11:02:04 +0100
Subject: [PATCH 01/10] feat: Az sender for sending task owner report
---
.../ApiClients/ApiModels/Mails.cs | 39 ++++
.../ApiClients/IMailApiClient.cs | 10 +
.../ApiClients/ISummaryApiClient.cs | 4 +
.../ApiClients/MailApiClient.cs | 48 ++++
.../ApiClients/SummaryApiClient.cs | 19 ++
.../IServiceCollectionExtensions.cs | 3 +
.../Fusion.Resources.Functions.Common.csproj | 2 +-
.../Http/Handlers/MailHttpHandler.cs | 25 +++
.../Http/HttpClientFactoryBuilder.cs | 16 +-
.../Integration/Http/HttpClientNames.cs | 1 +
.../ServiceDiscovery/ServiceEndpoint.cs | 1 +
.../WeeklyTaskOwnerReportSender.cs | 206 ++++++++++++++++++
.../Fusion.Summary.Functions.csproj | 1 +
.../local.settings.template.json | 1 +
14 files changed, 374 insertions(+), 2 deletions(-)
create mode 100644 src/Fusion.Resources.Functions.Common/ApiClients/ApiModels/Mails.cs
create mode 100644 src/Fusion.Resources.Functions.Common/ApiClients/IMailApiClient.cs
create mode 100644 src/Fusion.Resources.Functions.Common/ApiClients/MailApiClient.cs
create mode 100644 src/Fusion.Resources.Functions.Common/Integration/Http/Handlers/MailHttpHandler.cs
create mode 100644 src/Fusion.Summary.Functions/Functions/TaskOwnerReports/WeeklyTaskOwnerReportSender.cs
diff --git a/src/Fusion.Resources.Functions.Common/ApiClients/ApiModels/Mails.cs b/src/Fusion.Resources.Functions.Common/ApiClients/ApiModels/Mails.cs
new file mode 100644
index 000000000..2712669a1
--- /dev/null
+++ b/src/Fusion.Resources.Functions.Common/ApiClients/ApiModels/Mails.cs
@@ -0,0 +1,39 @@
+namespace Fusion.Resources.Functions.Common.ApiClients.ApiModels;
+
+public class SendEmailRequest
+{
+ public required string[] Recipients { get; set; }
+ public required string Subject { get; set; }
+ public required string Body { get; set; }
+ public string? FromDisplayName { get; set; }
+}
+
+public class SendEmailWithTemplateRequest
+{
+ public required string Subject { get; set; }
+
+ public required string[] Recipients { get; set; }
+
+ ///
+ /// Specify the content that is to be displayed in the mail
+ ///
+ public required MailBody MailBody { get; set; }
+}
+
+public class MailBody
+{
+ ///
+ /// The main content in the mail placed between the header and footer
+ ///
+ public required string HtmlContent { get; set; }
+
+ ///
+ /// Optional. If not specified, the footer template will be used
+ ///
+ public string? HtmlFooter { get; set; }
+
+ ///
+ /// Optional. A text that is displayed inside the header. Will default to 'Mail from Fusion'
+ ///
+ public string? HeaderTitle { get; set; }
+}
\ No newline at end of file
diff --git a/src/Fusion.Resources.Functions.Common/ApiClients/IMailApiClient.cs b/src/Fusion.Resources.Functions.Common/ApiClients/IMailApiClient.cs
new file mode 100644
index 000000000..bce8e5229
--- /dev/null
+++ b/src/Fusion.Resources.Functions.Common/ApiClients/IMailApiClient.cs
@@ -0,0 +1,10 @@
+using Fusion.Resources.Functions.Common.ApiClients.ApiModels;
+
+namespace Fusion.Resources.Functions.Common.ApiClients;
+
+public interface IMailApiClient
+{
+ public Task SendEmailAsync(SendEmailRequest request, CancellationToken cancellationToken = default);
+
+ public Task SendEmailWithTemplate(SendEmailWithTemplateRequest request, string? templateName = "default", CancellationToken cancellationToken = default);
+}
\ No newline at end of file
diff --git a/src/Fusion.Resources.Functions.Common/ApiClients/ISummaryApiClient.cs b/src/Fusion.Resources.Functions.Common/ApiClients/ISummaryApiClient.cs
index e2c5f7373..b7c675305 100644
--- a/src/Fusion.Resources.Functions.Common/ApiClients/ISummaryApiClient.cs
+++ b/src/Fusion.Resources.Functions.Common/ApiClients/ISummaryApiClient.cs
@@ -20,6 +20,10 @@ public Task PutDepartmentAsync(ApiResourceOwnerDepartment departments,
public Task GetLatestWeeklyReportAsync(string departmentSapId,
CancellationToken cancellationToken = default);
+ ///
+ public Task GetLatestWeeklyTaskOwnerReportAsync(Guid projectId,
+ CancellationToken cancellationToken = default);
+
///
public Task PutWeeklySummaryReportAsync(string departmentSapId, ApiWeeklySummaryReport report,
CancellationToken cancellationToken = default);
diff --git a/src/Fusion.Resources.Functions.Common/ApiClients/MailApiClient.cs b/src/Fusion.Resources.Functions.Common/ApiClients/MailApiClient.cs
new file mode 100644
index 000000000..41644a932
--- /dev/null
+++ b/src/Fusion.Resources.Functions.Common/ApiClients/MailApiClient.cs
@@ -0,0 +1,48 @@
+using System.Text;
+using Fusion.Resources.Functions.Common.ApiClients.ApiModels;
+using Fusion.Resources.Functions.Common.Extensions;
+using Fusion.Resources.Functions.Common.Integration.Errors;
+using Fusion.Resources.Functions.Common.Integration.Http;
+using Newtonsoft.Json;
+
+namespace Fusion.Resources.Functions.Common.ApiClients;
+
+public class MailApiClient : IMailApiClient
+{
+ private readonly HttpClient mailClient;
+
+ public MailApiClient(IHttpClientFactory httpClientFactory)
+ {
+ mailClient = httpClientFactory.CreateClient(HttpClientNames.Application.Mail);
+ mailClient.Timeout = TimeSpan.FromMinutes(2);
+ }
+
+ public async Task SendEmailAsync(SendEmailRequest request, CancellationToken cancellationToken = default)
+ {
+ var json = JsonConvert.SerializeObject(request);
+ var content = new StringContent(json, Encoding.UTF8, "application/json");
+
+ using var response = await mailClient.PostAsync("/mails", content, cancellationToken);
+
+ await ThrowIfNotSuccess(response);
+ }
+
+ public async Task SendEmailWithTemplate(SendEmailWithTemplateRequest request, string? templateName = "default", CancellationToken cancellationToken = default)
+ {
+ var json = JsonConvert.SerializeObject(request);
+ var content = new StringContent(json, Encoding.UTF8, "application/json");
+
+ using var response = await mailClient.PostAsync($"templates/{templateName}/mails", content, cancellationToken);
+
+ await ThrowIfNotSuccess(response);
+ }
+
+ private async Task ThrowIfNotSuccess(HttpResponseMessage response)
+ {
+ if (!response.IsSuccessStatusCode)
+ {
+ var body = await response.Content.ReadAsStringAsync();
+ throw new ApiError(response.RequestMessage!.RequestUri!.ToString(), response.StatusCode, body, "Response from API call indicates error");
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Fusion.Resources.Functions.Common/ApiClients/SummaryApiClient.cs b/src/Fusion.Resources.Functions.Common/ApiClients/SummaryApiClient.cs
index 15ac0d1d1..b5a8a73d5 100644
--- a/src/Fusion.Resources.Functions.Common/ApiClients/SummaryApiClient.cs
+++ b/src/Fusion.Resources.Functions.Common/ApiClients/SummaryApiClient.cs
@@ -80,6 +80,25 @@ public async Task PutProjectAsync(ApiProject project, CancellationTo
cancellationToken: cancellationToken))?.Items?.FirstOrDefault();
}
+ public async Task GetLatestWeeklyTaskOwnerReportAsync(Guid projectId, CancellationToken cancellationToken = default)
+ {
+ var lastMonday = DateTime.UtcNow.GetPreviousWeeksMondayDate();
+
+ var queryString = $"/projects/{projectId}/task-owners-summary-reports/weekly?$filter=PeriodStart eq '{lastMonday.Date:O}'&$top=1";
+
+
+ using var response = await summaryClient.GetAsync(queryString, cancellationToken);
+
+ await ThrowIfUnsuccessfulAsync(response);
+
+
+ await using var contentStream = await response.Content.ReadAsStreamAsync(cancellationToken);
+
+ return (await JsonSerializer.DeserializeAsync>(contentStream,
+ jsonSerializerOptions,
+ cancellationToken: cancellationToken))?.Items?.FirstOrDefault();
+ }
+
public async Task PutWeeklySummaryReportAsync(string departmentSapId, ApiWeeklySummaryReport report,
CancellationToken cancellationToken = default)
{
diff --git a/src/Fusion.Resources.Functions.Common/Configuration/IServiceCollectionExtensions.cs b/src/Fusion.Resources.Functions.Common/Configuration/IServiceCollectionExtensions.cs
index 58adca253..2a84a5365 100644
--- a/src/Fusion.Resources.Functions.Common/Configuration/IServiceCollectionExtensions.cs
+++ b/src/Fusion.Resources.Functions.Common/Configuration/IServiceCollectionExtensions.cs
@@ -61,6 +61,9 @@ public static IServiceCollection AddHttpClients(this IServiceCollection services
builder.AddRolesClient();
services.AddScoped();
+ builder.AddMailClient();
+ services.AddScoped();
+
return services;
}
}
diff --git a/src/Fusion.Resources.Functions.Common/Fusion.Resources.Functions.Common.csproj b/src/Fusion.Resources.Functions.Common/Fusion.Resources.Functions.Common.csproj
index e92bc89ac..fa4ebffa7 100644
--- a/src/Fusion.Resources.Functions.Common/Fusion.Resources.Functions.Common.csproj
+++ b/src/Fusion.Resources.Functions.Common/Fusion.Resources.Functions.Common.csproj
@@ -8,7 +8,7 @@
-
+
diff --git a/src/Fusion.Resources.Functions.Common/Integration/Http/Handlers/MailHttpHandler.cs b/src/Fusion.Resources.Functions.Common/Integration/Http/Handlers/MailHttpHandler.cs
new file mode 100644
index 000000000..e68b58d46
--- /dev/null
+++ b/src/Fusion.Resources.Functions.Common/Integration/Http/Handlers/MailHttpHandler.cs
@@ -0,0 +1,25 @@
+using Fusion.Resources.Functions.Common.Integration.Authentication;
+using Fusion.Resources.Functions.Common.Integration.ServiceDiscovery;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+namespace Fusion.Resources.Functions.Common.Integration.Http.Handlers;
+
+public class MailHttpHandler : FunctionHttpMessageHandler
+{
+ private readonly IOptions options;
+
+ public MailHttpHandler(ILoggerFactory loggerFactory, ITokenProvider tokenProvider, IServiceDiscovery serviceDiscovery, IOptions options)
+ : base(loggerFactory.CreateLogger(), tokenProvider, serviceDiscovery)
+ {
+ this.options = options;
+ }
+
+ protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ await SetEndpointUriForRequestAsync(request, ServiceEndpoint.Mail);
+ await AddAuthHeaderForRequestAsync(request, options.Value.Fusion);
+
+ return await base.SendAsync(request, cancellationToken);
+ }
+}
\ No newline at end of file
diff --git a/src/Fusion.Resources.Functions.Common/Integration/Http/HttpClientFactoryBuilder.cs b/src/Fusion.Resources.Functions.Common/Integration/Http/HttpClientFactoryBuilder.cs
index 7c54e7ee7..b7c240bbf 100644
--- a/src/Fusion.Resources.Functions.Common/Integration/Http/HttpClientFactoryBuilder.cs
+++ b/src/Fusion.Resources.Functions.Common/Integration/Http/HttpClientFactoryBuilder.cs
@@ -113,7 +113,7 @@ public HttpClientFactoryBuilder AddRolesClient()
services.AddTransient();
services.AddHttpClient(HttpClientNames.Application.Roles, client =>
{
- client.BaseAddress = new Uri("https://fusion-notifications");
+ client.BaseAddress = new Uri("https://fusion-roles");
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
})
.AddHttpMessageHandler()
@@ -122,6 +122,20 @@ public HttpClientFactoryBuilder AddRolesClient()
return this;
}
+ public HttpClientFactoryBuilder AddMailClient()
+ {
+ services.AddTransient();
+ services.AddHttpClient(HttpClientNames.Application.Mail, client =>
+ {
+ client.BaseAddress = new Uri("https://fusion-mail");
+ client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
+ })
+ .AddHttpMessageHandler()
+ .AddTransientHttpErrorPolicy(DefaultRetryPolicy());
+
+ return this;
+ }
+
private readonly TimeSpan[] DefaultSleepDurations = new[] { TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(10) };
private Func, IAsyncPolicy> DefaultRetryPolicy(TimeSpan[] sleepDurations = null) =>
diff --git a/src/Fusion.Resources.Functions.Common/Integration/Http/HttpClientNames.cs b/src/Fusion.Resources.Functions.Common/Integration/Http/HttpClientNames.cs
index 2217901c9..90c49183e 100644
--- a/src/Fusion.Resources.Functions.Common/Integration/Http/HttpClientNames.cs
+++ b/src/Fusion.Resources.Functions.Common/Integration/Http/HttpClientNames.cs
@@ -12,6 +12,7 @@ public static class Application
public const string Context = "App.Context";
public const string LineOrg = "App.LineOrg";
public const string Roles = "App.Roles";
+ public const string Mail = "App.Mail";
}
}
diff --git a/src/Fusion.Resources.Functions.Common/Integration/ServiceDiscovery/ServiceEndpoint.cs b/src/Fusion.Resources.Functions.Common/Integration/ServiceDiscovery/ServiceEndpoint.cs
index a2893251d..525f2e093 100644
--- a/src/Fusion.Resources.Functions.Common/Integration/ServiceDiscovery/ServiceEndpoint.cs
+++ b/src/Fusion.Resources.Functions.Common/Integration/ServiceDiscovery/ServiceEndpoint.cs
@@ -12,5 +12,6 @@ public sealed class ServiceEndpoint
public static ServiceEndpoint Context = new ServiceEndpoint { Key = "context" };
public static ServiceEndpoint LineOrg = new ServiceEndpoint { Key = "lineorg" };
public static ServiceEndpoint Roles = new ServiceEndpoint { Key = "roles" };
+ public static ServiceEndpoint Mail = new ServiceEndpoint { Key = "mail" };
}
}
diff --git a/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/WeeklyTaskOwnerReportSender.cs b/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/WeeklyTaskOwnerReportSender.cs
new file mode 100644
index 000000000..f18e97902
--- /dev/null
+++ b/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/WeeklyTaskOwnerReportSender.cs
@@ -0,0 +1,206 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using AdaptiveCards;
+using AdaptiveCards.Rendering.Html;
+using Fusion.Integration.Profile;
+using Fusion.Resources.Functions.Common.ApiClients;
+using Fusion.Resources.Functions.Common.ApiClients.ApiModels;
+using Fusion.Resources.Functions.Common.Extensions;
+using Fusion.Resources.Functions.Common.Integration.Errors;
+using Fusion.Summary.Functions.CardBuilder;
+using Microsoft.Azure.WebJobs;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging;
+
+namespace Fusion.Summary.Functions.Functions.TaskOwnerReports;
+
+public class WeeklyTaskOwnerReportSender
+{
+ private readonly ILogger logger;
+ private readonly ISummaryApiClient summaryApiClient;
+ private readonly IMailApiClient mailApiClient;
+ private readonly IPeopleApiClient peopleApiClient;
+ private AdaptiveCardRenderer cardHtmlRenderer;
+ private readonly bool sendingNotificationEnabled = true; // Default to true so that we don't accidentally disable sending notifications
+
+ public WeeklyTaskOwnerReportSender(ILogger logger, IConfiguration configuration, ISummaryApiClient summaryApiClient, IMailApiClient mailApiClient, IPeopleApiClient peopleApiClient)
+ {
+ this.logger = logger;
+ this.summaryApiClient = summaryApiClient;
+ this.mailApiClient = mailApiClient;
+ this.peopleApiClient = peopleApiClient;
+ cardHtmlRenderer = new AdaptiveCardRenderer();
+
+ // Need to explicitly add the configuration key to the app settings to disable sending of notifications
+ if (int.TryParse(configuration["isSendingNotificationEnabled"], out var enabled))
+ sendingNotificationEnabled = enabled == 1;
+ else if (bool.TryParse(configuration["isSendingNotificationEnabled"], out var enabledBool))
+ sendingNotificationEnabled = enabledBool;
+ }
+
+ private const string FunctionName = "weekly-task-owner-report-sender";
+
+ [FunctionName(FunctionName)]
+ public async Task RunAsync([TimerTrigger("0 0 5 * * MON", RunOnStartup = false)] TimerInfo timerInfo, CancellationToken cancellationToken = default)
+ {
+ logger.LogInformation("{FunctionName} started", FunctionName);
+
+ if (!sendingNotificationEnabled)
+ logger.LogInformation("Sending of notifications is disabled");
+
+ var projects = await GetProjectsAsync(cancellationToken);
+
+ var taskOwnerReports = await GetTaskOwnerReportsAsync(projects, cancellationToken);
+
+ var mailRequests = await CreateMailRequestsAsync(projects, taskOwnerReports, cancellationToken);
+
+ await SendTaskOwnerReportsAsync(mailRequests);
+ }
+
+ public async Task> GetProjectsAsync(CancellationToken cancellationToken = default)
+ => await summaryApiClient.GetProjectsAsync(cancellationToken);
+
+ public async Task GetTaskOwnerReportsAsync(IEnumerable projects, CancellationToken cancellationToken = default)
+ {
+ var taskOwnerReports = new List();
+ foreach (var project in projects)
+ {
+ try
+ {
+ var report = await summaryApiClient.GetLatestWeeklyTaskOwnerReportAsync(project.Id, cancellationToken);
+
+ if (report is null)
+ continue;
+
+ taskOwnerReports.Add(report);
+ }
+ catch (SummaryApiError e)
+ {
+ logger.LogError(e, "Failed to get task owner report for project {Project}", project.ToJson());
+ }
+ }
+
+ return taskOwnerReports.ToArray();
+ }
+
+ public async Task> CreateMailRequestsAsync(IEnumerable projects, ICollection taskOwnerReports, CancellationToken cancellationToken)
+ {
+ var requests = new List();
+
+ // TODO: Recipients should be stored on the report itself, alternatively retried specifically from the summary api
+ // For now we just extract the recipients from the api project model and resolve email addresses during the creation of the mail request
+
+ foreach (var project in projects)
+ {
+ var report = taskOwnerReports.FirstOrDefault(r => r.ProjectId == project.Id);
+ if (report is null)
+ continue;
+
+ var recipients = project.AssignedAdminsAzureUniqueId;
+ if (project.DirectorAzureUniqueId.HasValue)
+ recipients = recipients.Append(project.DirectorAzureUniqueId.Value).ToArray();
+
+ if (recipients.Length == 0)
+ {
+ logger.LogWarning("No recipients found for project {Project}", project.ToJson());
+ continue;
+ }
+
+ string[] recipientEmails;
+ try
+ {
+ // TODO: Email resolution should be done before the az func sender runs, and the resolved emails should be stored on the report/project
+ recipientEmails = await ResolveEmailsAsync(recipients, cancellationToken);
+ }
+ catch (Exception e)
+ {
+ logger.LogError(e, "Failed to resolve emails for project {Project} | Report {Report}", project.ToJson(), report.ToJson());
+ continue;
+ }
+
+
+ try
+ {
+ requests.Add(CreateReportMail(recipientEmails, project, report));
+ }
+ catch (Exception e)
+ {
+ logger.LogError(e, "Failed to create mail request for project {Project} | Report {Report}", project.ToJson(), report.ToJson());
+ }
+ }
+
+ return requests;
+ }
+
+
+ private async Task ResolveEmailsAsync(IEnumerable azureUniqueId, CancellationToken cancellationToken)
+ {
+ var personIdentifiers = azureUniqueId.Select(id => new PersonIdentifier(id));
+
+ var resolvedPersons = await peopleApiClient.ResolvePersonsAsync(personIdentifiers, cancellationToken);
+
+ resolvedPersons.Where(p => !p.Success).ToList().ForEach(p => logger.LogWarning("Failed to resolve person {PersonId}", p.Identifier));
+
+ return resolvedPersons
+ .Where(p => p.Success)
+ .Select(p => p.Person!.PreferredContactMail ?? p.Person.Mail).ToArray();
+ }
+
+ private SendEmailWithTemplateRequest CreateReportMail(string[] recipients, ApiProject project, ApiWeeklyTaskOwnerReport report)
+ {
+ var subject = $"Weekly summary - {project.Name}";
+
+ var card = new AdaptiveCardBuilder()
+ .AddHeading($"**Weekly summary - {project.Name}**")
+ .AddTextRow(report.ActionsAwaitingTaskOwnerAction.ToString(), "Actions awaiting task owners")
+ .AddListContainer("Admin access expire less than 3 months", report.AdminAccessExpiringInLessThanThreeMonths
+ .Select(a => new List()
+ {
+ new()
+ {
+ Value = a.FullName,
+ Alignment = AdaptiveHorizontalAlignment.Left
+ },
+ new()
+ {
+ Value = a.Expires.ToString(),
+ Alignment = AdaptiveHorizontalAlignment.Right
+ }
+ }).ToList())
+ .AddTextRow(report.PositionAllocationsEndingInNextThreeMonths.Length.ToString(), "Position allocations expiring next 3 months")
+ .AddTextRow(report.TBNPositionsStartingInLessThanThreeMonths.Length.ToString(), "TBN positions with start date in less than 3 months")
+ .Build();
+
+
+ return new SendEmailWithTemplateRequest()
+ {
+ Recipients = recipients,
+ Subject = subject,
+ MailBody = new()
+ {
+ HtmlContent = cardHtmlRenderer.RenderCard(card).Html.ToString()
+ }
+ };
+ }
+
+ public async Task SendTaskOwnerReportsAsync(IEnumerable emailReportRequests)
+ {
+ foreach (var request in emailReportRequests)
+ {
+ try
+ {
+ if (sendingNotificationEnabled)
+ await mailApiClient.SendEmailWithTemplate(request);
+ else
+ logger.LogInformation("Sending of notifications is disabled. Skipping sending mail to {Recipients}", string.Join(',', request.Recipients));
+ }
+ catch (ApiError e)
+ {
+ logger.LogError(e, "Failed to send task owner report mail. Request: {Request}", request.ToJson());
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Fusion.Summary.Functions/Fusion.Summary.Functions.csproj b/src/Fusion.Summary.Functions/Fusion.Summary.Functions.csproj
index 779362bd8..7cb12f8c9 100644
--- a/src/Fusion.Summary.Functions/Fusion.Summary.Functions.csproj
+++ b/src/Fusion.Summary.Functions/Fusion.Summary.Functions.csproj
@@ -5,6 +5,7 @@
enable
+
diff --git a/src/Fusion.Summary.Functions/local.settings.template.json b/src/Fusion.Summary.Functions/local.settings.template.json
index 2e1516aaf..618340361 100644
--- a/src/Fusion.Summary.Functions/local.settings.template.json
+++ b/src/Fusion.Summary.Functions/local.settings.template.json
@@ -20,6 +20,7 @@
"Endpoints_context": "https://fusion-s-context-ci.azurewebsites.net",
"Endpoints_notifications": "https://fusion-s-notification-ci.azurewebsites.net",
"Endpoints_roles": "https://roles.ci.api.fusion-dev.net",
+ "Endpoints_mail": "https://mail.ci.api.fusion-dev.net",
"Endpoints_Resources_Fusion": "5a842df8-3238-415d-b168-9f16a6a6031b"
}
}
\ No newline at end of file
From 5223395639a850d8cffd9f9f8a847208167e73da Mon Sep 17 00:00:00 2001
From: Jonathan Idland Olsnes <73334350+Jonathanio123@users.noreply.github.com>
Date: Tue, 12 Nov 2024 15:55:17 +0100
Subject: [PATCH 02/10] feat: Updated mail design
---
.../ApiClients/IMailApiClient.cs | 2 +-
.../ApiClients/MailApiClient.cs | 2 +-
.../CardBuilder/AdaptiveCardBuilder.cs | 104 +++++++++++++
.../WeeklyTaskOwnerReportSender.cs | 140 +++++++++++++-----
4 files changed, 209 insertions(+), 39 deletions(-)
diff --git a/src/Fusion.Resources.Functions.Common/ApiClients/IMailApiClient.cs b/src/Fusion.Resources.Functions.Common/ApiClients/IMailApiClient.cs
index bce8e5229..567fb4c6b 100644
--- a/src/Fusion.Resources.Functions.Common/ApiClients/IMailApiClient.cs
+++ b/src/Fusion.Resources.Functions.Common/ApiClients/IMailApiClient.cs
@@ -6,5 +6,5 @@ public interface IMailApiClient
{
public Task SendEmailAsync(SendEmailRequest request, CancellationToken cancellationToken = default);
- public Task SendEmailWithTemplate(SendEmailWithTemplateRequest request, string? templateName = "default", CancellationToken cancellationToken = default);
+ public Task SendEmailWithTemplateAsync(SendEmailWithTemplateRequest request, string? templateName = "default", CancellationToken cancellationToken = default);
}
\ No newline at end of file
diff --git a/src/Fusion.Resources.Functions.Common/ApiClients/MailApiClient.cs b/src/Fusion.Resources.Functions.Common/ApiClients/MailApiClient.cs
index 41644a932..14b952b68 100644
--- a/src/Fusion.Resources.Functions.Common/ApiClients/MailApiClient.cs
+++ b/src/Fusion.Resources.Functions.Common/ApiClients/MailApiClient.cs
@@ -27,7 +27,7 @@ public async Task SendEmailAsync(SendEmailRequest request, CancellationToken can
await ThrowIfNotSuccess(response);
}
- public async Task SendEmailWithTemplate(SendEmailWithTemplateRequest request, string? templateName = "default", CancellationToken cancellationToken = default)
+ public async Task SendEmailWithTemplateAsync(SendEmailWithTemplateRequest request, string? templateName = "default", CancellationToken cancellationToken = default)
{
var json = JsonConvert.SerializeObject(request);
var content = new StringContent(json, Encoding.UTF8, "application/json");
diff --git a/src/Fusion.Summary.Functions/CardBuilder/AdaptiveCardBuilder.cs b/src/Fusion.Summary.Functions/CardBuilder/AdaptiveCardBuilder.cs
index 464e2df62..d96dad447 100644
--- a/src/Fusion.Summary.Functions/CardBuilder/AdaptiveCardBuilder.cs
+++ b/src/Fusion.Summary.Functions/CardBuilder/AdaptiveCardBuilder.cs
@@ -51,6 +51,85 @@ public AdaptiveCardBuilder AddTextRow(string valueText, string headerText, strin
return this;
}
+
+ public AdaptiveCardBuilder AddGrid(string headerText, IEnumerable columns, GoToAction? goToAction = null)
+ {
+ var listContainer = new AdaptiveContainer
+ {
+ Separator = true
+ };
+
+ var header = new AdaptiveTextBlock
+ {
+ Weight = AdaptiveTextWeight.Bolder,
+ Text = headerText,
+ Wrap = true,
+ Size = AdaptiveTextSize.Large,
+ HorizontalAlignment = AdaptiveHorizontalAlignment.Center
+ };
+
+
+ var grid = new AdaptiveColumnSet();
+
+ foreach (var column in columns)
+ {
+ var rows = new List();
+
+ foreach (var gridCell in column.Cells)
+ {
+ var cell = new AdaptiveTextBlock
+ {
+ Text = gridCell.Value,
+ Wrap = true,
+ HorizontalAlignment = gridCell.Alignment,
+ IsSubtle = gridCell.IsHeader
+ };
+
+ rows.Add(cell);
+ }
+
+ var gridColumn = new AdaptiveColumn
+ {
+ Width = column.Width,
+ Items = rows
+ };
+
+ grid.Columns.Add(gridColumn);
+ }
+
+ listContainer.Items.Add(header);
+ listContainer.Items.Add(grid);
+
+ // If no data is present, add a "None" text
+ if (columns.SelectMany(c => c.Cells).All(c => c.IsHeader))
+ {
+ listContainer.Items.Add(new AdaptiveTextBlock
+ {
+ Text = "None",
+ Wrap = true,
+ HorizontalAlignment = AdaptiveHorizontalAlignment.Center
+ });
+ }
+
+ if (goToAction != null)
+ {
+ var actionSet = new AdaptiveActionSet();
+ var action = new AdaptiveOpenUrlAction()
+ {
+ Title = goToAction.Title,
+ Url = new Uri(goToAction.Url)
+ };
+
+ actionSet.Actions.Add(action);
+
+ listContainer.Items.Add(actionSet);
+ }
+
+ _adaptiveCard.Body.Add(listContainer);
+ return this;
+ }
+
+
public AdaptiveCardBuilder AddListContainer(string headerText,
List> objectLists)
{
@@ -136,4 +215,29 @@ public class ListObject
public string Value { get; set; }
public AdaptiveHorizontalAlignment Alignment { get; set; }
}
+}
+
+public class GridColumn
+{
+ public ICollection Cells { get; set; }
+ public string Width { get; set; } = AdaptiveColumnWidth.Auto;
+}
+
+public class GridCell
+{
+ public GridCell(bool isHeader, string value)
+ {
+ IsHeader = isHeader;
+ Value = value;
+ }
+
+ public bool IsHeader { get; set; }
+ public string Value { get; set; }
+ public AdaptiveHorizontalAlignment Alignment { get; set; }
+}
+
+public class GoToAction
+{
+ public string Title { get; set; }
+ public string Url { get; set; }
}
\ No newline at end of file
diff --git a/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/WeeklyTaskOwnerReportSender.cs b/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/WeeklyTaskOwnerReportSender.cs
index f18e97902..4653dc93a 100644
--- a/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/WeeklyTaskOwnerReportSender.cs
+++ b/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/WeeklyTaskOwnerReportSender.cs
@@ -23,8 +23,9 @@ public class WeeklyTaskOwnerReportSender
private readonly ISummaryApiClient summaryApiClient;
private readonly IMailApiClient mailApiClient;
private readonly IPeopleApiClient peopleApiClient;
- private AdaptiveCardRenderer cardHtmlRenderer;
+ private readonly AdaptiveCardRenderer cardHtmlRenderer;
private readonly bool sendingNotificationEnabled = true; // Default to true so that we don't accidentally disable sending notifications
+ private readonly string fusionUri;
public WeeklyTaskOwnerReportSender(ILogger logger, IConfiguration configuration, ISummaryApiClient summaryApiClient, IMailApiClient mailApiClient, IPeopleApiClient peopleApiClient)
{
@@ -33,6 +34,7 @@ public WeeklyTaskOwnerReportSender(ILogger logger,
this.mailApiClient = mailApiClient;
this.peopleApiClient = peopleApiClient;
cardHtmlRenderer = new AdaptiveCardRenderer();
+ fusionUri = (configuration["Endpoints_portal"] ?? "https://fusion.equinor.com/").TrimEnd('/');
// Need to explicitly add the configuration key to the app settings to disable sending of notifications
if (int.TryParse(configuration["isSendingNotificationEnabled"], out var enabled))
@@ -113,7 +115,7 @@ public async Task> CreateMailRequestsAsync(IE
try
{
// TODO: Email resolution should be done before the az func sender runs, and the resolved emails should be stored on the report/project
- recipientEmails = await ResolveEmailsAsync(recipients, cancellationToken);
+ recipientEmails = await ResolveEmailsAsync(recipients.Distinct(), cancellationToken);
}
catch (Exception e)
{
@@ -135,6 +137,24 @@ public async Task> CreateMailRequestsAsync(IE
return requests;
}
+ public async Task SendTaskOwnerReportsAsync(IEnumerable emailReportRequests)
+ {
+ foreach (var request in emailReportRequests)
+ {
+ try
+ {
+ if (sendingNotificationEnabled)
+ await mailApiClient.SendEmailWithTemplateAsync(request);
+ else
+ logger.LogInformation("Sending of notifications is disabled. Skipping sending mail to {Recipients}", string.Join(',', request.Recipients));
+ }
+ catch (ApiError e)
+ {
+ logger.LogError(e, "Failed to send task owner report mail. Request: {Request}", request.ToJson());
+ }
+ }
+ }
+
private async Task ResolveEmailsAsync(IEnumerable azureUniqueId, CancellationToken cancellationToken)
{
@@ -151,29 +171,93 @@ private async Task ResolveEmailsAsync(IEnumerable azureUniqueId,
private SendEmailWithTemplateRequest CreateReportMail(string[] recipients, ApiProject project, ApiWeeklyTaskOwnerReport report)
{
- var subject = $"Weekly summary - {project.Name}";
-
var card = new AdaptiveCardBuilder()
.AddHeading($"**Weekly summary - {project.Name}**")
.AddTextRow(report.ActionsAwaitingTaskOwnerAction.ToString(), "Actions awaiting task owners")
- .AddListContainer("Admin access expire less than 3 months", report.AdminAccessExpiringInLessThanThreeMonths
- .Select(a => new List()
+ .AddGrid("Admin access expiring in less than 3 months", new List()
+ {
+ new()
+ {
+ Width = AdaptiveColumnWidth.Stretch,
+ Cells =
+ [
+ new GridCell(isHeader: true, value: "Name"),
+ ..report.AdminAccessExpiringInLessThanThreeMonths.Select(a
+ => new GridCell(isHeader: false, value: a.FullName))
+ ]
+ },
+ new()
+ {
+ Width = AdaptiveColumnWidth.Auto,
+ Cells =
+ [
+ new GridCell(isHeader: true, value: "Expires"),
+ ..report.AdminAccessExpiringInLessThanThreeMonths.Select(a
+ => new GridCell(isHeader: false, value: a.Expires.ToString("dd/MM/yyyy")))
+ ]
+ }
+ }, new GoToAction()
+ {
+ Title = "Go to Access Management",
+ Url = $"{fusionUri}/apps/org-admin/{project.OrgProjectExternalId}/access-control"
+ })
+ .AddGrid("Position allocations expiring next 3 months", new List()
+ {
+ new()
{
- new()
- {
- Value = a.FullName,
- Alignment = AdaptiveHorizontalAlignment.Left
- },
- new()
- {
- Value = a.Expires.ToString(),
- Alignment = AdaptiveHorizontalAlignment.Right
- }
- }).ToList())
- .AddTextRow(report.PositionAllocationsEndingInNextThreeMonths.Length.ToString(), "Position allocations expiring next 3 months")
- .AddTextRow(report.TBNPositionsStartingInLessThanThreeMonths.Length.ToString(), "TBN positions with start date in less than 3 months")
+ Width = AdaptiveColumnWidth.Stretch,
+ Cells =
+ [
+ new GridCell(isHeader: true, value: "Position"),
+ ..report.PositionAllocationsEndingInNextThreeMonths.Select(p
+ => new GridCell(isHeader: false, value: $"{p.PositionExternalId} {p.PositionNameDetailed}"))
+ ]
+ },
+ new()
+ {
+ Width = AdaptiveColumnWidth.Auto,
+ Cells =
+ [
+ new GridCell(isHeader: true, value: "End date"),
+ ..report.PositionAllocationsEndingInNextThreeMonths.Select(p
+ => new GridCell(isHeader: false, value: p.PositionAppliesTo.ToString("dd/MM/yyyy")))
+ ]
+ }
+ }, new GoToAction()
+ {
+ Title = "Go to Position Overview",
+ Url = $"{fusionUri}/apps/org-admin/{project.OrgProjectExternalId}/edit-positions/listing-view"
+ })
+ .AddGrid("TBN positions with start date in less than 3 months", new List()
+ {
+ new()
+ {
+ Width = AdaptiveColumnWidth.Stretch,
+ Cells =
+ [
+ new GridCell(isHeader: true, value: "Position"),
+ ..report.TBNPositionsStartingInLessThanThreeMonths.Select(p
+ => new GridCell(isHeader: false, value: $"{p.PositionExternalId} {p.PositionNameDetailed}"))
+ ]
+ },
+ new()
+ {
+ Width = AdaptiveColumnWidth.Auto,
+ Cells =
+ [
+ new GridCell(isHeader: true, value: "Start date"),
+ ..report.TBNPositionsStartingInLessThanThreeMonths.Select(p
+ => new GridCell(isHeader: false, value: p.PositionAppliesFrom.ToString("dd/MM/yyyy")))
+ ]
+ }
+ }, new GoToAction()
+ {
+ Title = "Go to Position Overview",
+ Url = $"{fusionUri}/apps/org-admin/{project.OrgProjectExternalId}/edit-positions/listing-view"
+ })
.Build();
+ var subject = $"Weekly summary - {project.Name}";
return new SendEmailWithTemplateRequest()
{
@@ -185,22 +269,4 @@ private SendEmailWithTemplateRequest CreateReportMail(string[] recipients, ApiPr
}
};
}
-
- public async Task SendTaskOwnerReportsAsync(IEnumerable emailReportRequests)
- {
- foreach (var request in emailReportRequests)
- {
- try
- {
- if (sendingNotificationEnabled)
- await mailApiClient.SendEmailWithTemplate(request);
- else
- logger.LogInformation("Sending of notifications is disabled. Skipping sending mail to {Recipients}", string.Join(',', request.Recipients));
- }
- catch (ApiError e)
- {
- logger.LogError(e, "Failed to send task owner report mail. Request: {Request}", request.ToJson());
- }
- }
- }
}
\ No newline at end of file
From 1a84f19ea9cff40a39bac8bc2b5b30740ab081a6 Mon Sep 17 00:00:00 2001
From: Jonathan Idland Olsnes <73334350+Jonathanio123@users.noreply.github.com>
Date: Fri, 15 Nov 2024 14:11:08 +0100
Subject: [PATCH 03/10] Get context id from context api
---
.../ApiClients/ApiModels/Contexts.cs | 27 ++++
.../ApiClients/ContextApiClient.cs | 20 +++
.../ApiClients/IContextApiClient.cs | 8 ++
.../IServiceCollectionExtensions.cs | 3 +
.../Http/Handlers/ContextHttpHandler.cs | 25 ++++
.../Http/HttpClientFactoryBuilder.cs | 14 ++
.../CardBuilder/AdaptiveCardBuilder.cs | 1 -
.../WeeklyTaskOwnerReportSender.cs | 124 ++++++++++++++----
8 files changed, 196 insertions(+), 26 deletions(-)
create mode 100644 src/Fusion.Resources.Functions.Common/ApiClients/ApiModels/Contexts.cs
create mode 100644 src/Fusion.Resources.Functions.Common/ApiClients/ContextApiClient.cs
create mode 100644 src/Fusion.Resources.Functions.Common/ApiClients/IContextApiClient.cs
create mode 100644 src/Fusion.Resources.Functions.Common/Integration/Http/Handlers/ContextHttpHandler.cs
diff --git a/src/Fusion.Resources.Functions.Common/ApiClients/ApiModels/Contexts.cs b/src/Fusion.Resources.Functions.Common/ApiClients/ApiModels/Contexts.cs
new file mode 100644
index 000000000..55f8e8c7c
--- /dev/null
+++ b/src/Fusion.Resources.Functions.Common/ApiClients/ApiModels/Contexts.cs
@@ -0,0 +1,27 @@
+namespace Fusion.Resources.Functions.Common.ApiClients.ApiModels;
+
+public class ApiContext
+{
+ public Guid Id { get; set; }
+
+ public string? ExternalId { get; set; }
+
+ public ApiContextType Type { get; set; } = null!;
+
+ public Dictionary Value { get; set; } = null!;
+
+ public string Title { get; set; } = null!;
+
+ public string? Source { get; set; }
+
+ public bool IsActive { get; set; }
+}
+
+public class ApiContextType
+{
+ public string Id { get; set; } = null!;
+
+ public bool IsChildType { get; set; }
+
+ public string[]? ParentTypeIds { get; set; }
+}
\ No newline at end of file
diff --git a/src/Fusion.Resources.Functions.Common/ApiClients/ContextApiClient.cs b/src/Fusion.Resources.Functions.Common/ApiClients/ContextApiClient.cs
new file mode 100644
index 000000000..b4db08055
--- /dev/null
+++ b/src/Fusion.Resources.Functions.Common/ApiClients/ContextApiClient.cs
@@ -0,0 +1,20 @@
+using Fusion.Resources.Functions.Common.ApiClients.ApiModels;
+using Fusion.Resources.Functions.Common.Integration.Http;
+
+namespace Fusion.Resources.Functions.Common.ApiClients;
+
+public class ContextApiClient : IContextApiClient
+{
+ private readonly HttpClient client;
+
+ public ContextApiClient(IHttpClientFactory httpClientFactory)
+ {
+ client = httpClientFactory.CreateClient(HttpClientNames.Application.Context);
+ }
+
+ public async Task> GetContextsAsync(string? contextType = null, CancellationToken cancellationToken = default)
+ {
+ var url = contextType is null ? "/contexts" : $"/contexts?$filter=type eq '{contextType}'";
+ return await client.GetAsJsonAsync>(url, cancellationToken);
+ }
+}
\ No newline at end of file
diff --git a/src/Fusion.Resources.Functions.Common/ApiClients/IContextApiClient.cs b/src/Fusion.Resources.Functions.Common/ApiClients/IContextApiClient.cs
new file mode 100644
index 000000000..d1825ff2c
--- /dev/null
+++ b/src/Fusion.Resources.Functions.Common/ApiClients/IContextApiClient.cs
@@ -0,0 +1,8 @@
+using Fusion.Resources.Functions.Common.ApiClients.ApiModels;
+
+namespace Fusion.Resources.Functions.Common.ApiClients;
+
+public interface IContextApiClient
+{
+ public Task> GetContextsAsync(string? contextType = null, CancellationToken cancellationToken = default);
+}
\ No newline at end of file
diff --git a/src/Fusion.Resources.Functions.Common/Configuration/IServiceCollectionExtensions.cs b/src/Fusion.Resources.Functions.Common/Configuration/IServiceCollectionExtensions.cs
index 2a84a5365..e93a99f01 100644
--- a/src/Fusion.Resources.Functions.Common/Configuration/IServiceCollectionExtensions.cs
+++ b/src/Fusion.Resources.Functions.Common/Configuration/IServiceCollectionExtensions.cs
@@ -64,6 +64,9 @@ public static IServiceCollection AddHttpClients(this IServiceCollection services
builder.AddMailClient();
services.AddScoped();
+ builder.AddContextClient();
+ services.AddScoped();
+
return services;
}
}
diff --git a/src/Fusion.Resources.Functions.Common/Integration/Http/Handlers/ContextHttpHandler.cs b/src/Fusion.Resources.Functions.Common/Integration/Http/Handlers/ContextHttpHandler.cs
new file mode 100644
index 000000000..9a52ba8ed
--- /dev/null
+++ b/src/Fusion.Resources.Functions.Common/Integration/Http/Handlers/ContextHttpHandler.cs
@@ -0,0 +1,25 @@
+using Fusion.Resources.Functions.Common.Integration.Authentication;
+using Fusion.Resources.Functions.Common.Integration.ServiceDiscovery;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+namespace Fusion.Resources.Functions.Common.Integration.Http.Handlers;
+
+public class ContextHttpHandler : FunctionHttpMessageHandler
+{
+ private readonly IOptions options;
+
+ public ContextHttpHandler(ILoggerFactory logger, ITokenProvider tokenProvider, IServiceDiscovery serviceDiscovery, IOptions options)
+ : base(logger.CreateLogger(), tokenProvider, serviceDiscovery)
+ {
+ this.options = options;
+ }
+
+ protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ await SetEndpointUriForRequestAsync(request, ServiceEndpoint.Context);
+ await AddAuthHeaderForRequestAsync(request, options.Value.Fusion);
+
+ return await base.SendAsync(request, cancellationToken);
+ }
+}
\ No newline at end of file
diff --git a/src/Fusion.Resources.Functions.Common/Integration/Http/HttpClientFactoryBuilder.cs b/src/Fusion.Resources.Functions.Common/Integration/Http/HttpClientFactoryBuilder.cs
index b7c240bbf..17a3907ca 100644
--- a/src/Fusion.Resources.Functions.Common/Integration/Http/HttpClientFactoryBuilder.cs
+++ b/src/Fusion.Resources.Functions.Common/Integration/Http/HttpClientFactoryBuilder.cs
@@ -136,6 +136,20 @@ public HttpClientFactoryBuilder AddMailClient()
return this;
}
+ public HttpClientFactoryBuilder AddContextClient()
+ {
+ services.AddTransient();
+ services.AddHttpClient(HttpClientNames.Application.Context, client =>
+ {
+ client.BaseAddress = new Uri("https://fusion-context");
+ client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
+ })
+ .AddHttpMessageHandler()
+ .AddTransientHttpErrorPolicy(DefaultRetryPolicy());
+
+ return this;
+ }
+
private readonly TimeSpan[] DefaultSleepDurations = new[] { TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(10) };
private Func, IAsyncPolicy> DefaultRetryPolicy(TimeSpan[] sleepDurations = null) =>
diff --git a/src/Fusion.Summary.Functions/CardBuilder/AdaptiveCardBuilder.cs b/src/Fusion.Summary.Functions/CardBuilder/AdaptiveCardBuilder.cs
index d96dad447..9b310dba1 100644
--- a/src/Fusion.Summary.Functions/CardBuilder/AdaptiveCardBuilder.cs
+++ b/src/Fusion.Summary.Functions/CardBuilder/AdaptiveCardBuilder.cs
@@ -121,7 +121,6 @@ public AdaptiveCardBuilder AddGrid(string headerText, IEnumerable co
};
actionSet.Actions.Add(action);
-
listContainer.Items.Add(actionSet);
}
diff --git a/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/WeeklyTaskOwnerReportSender.cs b/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/WeeklyTaskOwnerReportSender.cs
index 4653dc93a..f5e32a248 100644
--- a/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/WeeklyTaskOwnerReportSender.cs
+++ b/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/WeeklyTaskOwnerReportSender.cs
@@ -23,16 +23,18 @@ public class WeeklyTaskOwnerReportSender
private readonly ISummaryApiClient summaryApiClient;
private readonly IMailApiClient mailApiClient;
private readonly IPeopleApiClient peopleApiClient;
+ private readonly IContextApiClient contextApiClient;
private readonly AdaptiveCardRenderer cardHtmlRenderer;
private readonly bool sendingNotificationEnabled = true; // Default to true so that we don't accidentally disable sending notifications
private readonly string fusionUri;
- public WeeklyTaskOwnerReportSender(ILogger logger, IConfiguration configuration, ISummaryApiClient summaryApiClient, IMailApiClient mailApiClient, IPeopleApiClient peopleApiClient)
+ public WeeklyTaskOwnerReportSender(ILogger logger, IConfiguration configuration, ISummaryApiClient summaryApiClient, IMailApiClient mailApiClient, IPeopleApiClient peopleApiClient, IContextApiClient contextApiClient)
{
this.logger = logger;
this.summaryApiClient = summaryApiClient;
this.mailApiClient = mailApiClient;
this.peopleApiClient = peopleApiClient;
+ this.contextApiClient = contextApiClient;
cardHtmlRenderer = new AdaptiveCardRenderer();
fusionUri = (configuration["Endpoints_portal"] ?? "https://fusion.equinor.com/").TrimEnd('/');
@@ -53,7 +55,7 @@ public async Task RunAsync([TimerTrigger("0 0 5 * * MON", RunOnStartup = false)]
if (!sendingNotificationEnabled)
logger.LogInformation("Sending of notifications is disabled");
- var projects = await GetProjectsAsync(cancellationToken);
+ var projects = await GetProjectsInformationAsync(cancellationToken);
var taskOwnerReports = await GetTaskOwnerReportsAsync(projects, cancellationToken);
@@ -62,10 +64,76 @@ public async Task RunAsync([TimerTrigger("0 0 5 * * MON", RunOnStartup = false)]
await SendTaskOwnerReportsAsync(mailRequests);
}
- public async Task> GetProjectsAsync(CancellationToken cancellationToken = default)
- => await summaryApiClient.GetProjectsAsync(cancellationToken);
+ private async Task> GetProjectsInformationAsync(CancellationToken cancellationToken = default)
+ {
+ var apiProjects = await summaryApiClient.GetProjectsAsync(cancellationToken);
+
+ ICollection apiOrgContexts;
+ try
+ {
+ apiOrgContexts = await contextApiClient.GetContextsAsync("OrgChart", cancellationToken: cancellationToken);
+ }
+ catch (Exception e)
+ {
+ // Log and continue
+ // Everything else works but the "Go To" links in the mail will not work
+ logger.LogError(e, "Failed to get org contexts");
+ apiOrgContexts = [];
+ }
+
+ var mergedProjects = new List();
+
+ foreach (var project in apiProjects)
+ {
+ // TODO: Recipients should be stored on the report itself, alternatively retried specifically from the summary api
+ // For now we just extract the recipients from the api project model and resolve email addresses during the creation of the mail request
+
+ var recipients = project.AssignedAdminsAzureUniqueId;
+ if (project.DirectorAzureUniqueId.HasValue)
+ recipients = recipients.Append(project.DirectorAzureUniqueId.Value).ToArray();
+
+ if (recipients.Length == 0)
+ {
+ logger.LogWarning("No recipients found for project {Project}", project.ToJson());
+ continue;
+ }
+
+ recipients = recipients.Distinct().ToArray();
+
+ // For contexts of type OrgChart
+ // The ExternalId is the same as the internal id used for a project in the Org API
+ // The value property bag also contains this externalId. value.orgChartId
+
+ // So we try and find the common external id between the project and the org context
+
+ var orgProjectContext = apiOrgContexts.FirstOrDefault(c => Guid.TryParse(c.ExternalId, out var contextExternalId)
+ && contextExternalId == project.OrgProjectExternalId);
+
+
+ if (orgProjectContext is null) // Try and check the PropertyBag
+ orgProjectContext = apiOrgContexts.FirstOrDefault(c => c.Value.TryGetValue("orgChartId", out var orgChartId)
+ && Guid.TryParse(orgChartId as string, out var contextExternalId)
+ && contextExternalId == project.OrgProjectExternalId);
+
+
+ if (orgProjectContext is null)
+ logger.LogError("No org context found for project {Project}", project.ToJson());
- public async Task GetTaskOwnerReportsAsync(IEnumerable projects, CancellationToken cancellationToken = default)
+
+ mergedProjects.Add(new Project()
+ {
+ Id = project.Id,
+ OrgProjectExternalId = project.OrgProjectExternalId,
+ ContextProjectId = orgProjectContext?.Id,
+ Name = project.Name,
+ Recipients = recipients
+ });
+ }
+
+ return mergedProjects;
+ }
+
+ private async Task GetTaskOwnerReportsAsync(IEnumerable projects, CancellationToken cancellationToken = default)
{
var taskOwnerReports = new List();
foreach (var project in projects)
@@ -88,34 +156,21 @@ public async Task GetTaskOwnerReportsAsync(IEnumerab
return taskOwnerReports.ToArray();
}
- public async Task> CreateMailRequestsAsync(IEnumerable projects, ICollection taskOwnerReports, CancellationToken cancellationToken)
+ private async Task> CreateMailRequestsAsync(IEnumerable projects, ICollection taskOwnerReports, CancellationToken cancellationToken)
{
var requests = new List();
- // TODO: Recipients should be stored on the report itself, alternatively retried specifically from the summary api
- // For now we just extract the recipients from the api project model and resolve email addresses during the creation of the mail request
-
foreach (var project in projects)
{
var report = taskOwnerReports.FirstOrDefault(r => r.ProjectId == project.Id);
if (report is null)
continue;
- var recipients = project.AssignedAdminsAzureUniqueId;
- if (project.DirectorAzureUniqueId.HasValue)
- recipients = recipients.Append(project.DirectorAzureUniqueId.Value).ToArray();
-
- if (recipients.Length == 0)
- {
- logger.LogWarning("No recipients found for project {Project}", project.ToJson());
- continue;
- }
-
string[] recipientEmails;
try
{
// TODO: Email resolution should be done before the az func sender runs, and the resolved emails should be stored on the report/project
- recipientEmails = await ResolveEmailsAsync(recipients.Distinct(), cancellationToken);
+ recipientEmails = await ResolveEmailsAsync(project.Recipients, cancellationToken);
}
catch (Exception e)
{
@@ -137,7 +192,7 @@ public async Task> CreateMailRequestsAsync(IE
return requests;
}
- public async Task SendTaskOwnerReportsAsync(IEnumerable emailReportRequests)
+ private async Task SendTaskOwnerReportsAsync(IEnumerable emailReportRequests)
{
foreach (var request in emailReportRequests)
{
@@ -169,8 +224,10 @@ private async Task ResolveEmailsAsync(IEnumerable azureUniqueId,
.Select(p => p.Person!.PreferredContactMail ?? p.Person.Mail).ToArray();
}
- private SendEmailWithTemplateRequest CreateReportMail(string[] recipients, ApiProject project, ApiWeeklyTaskOwnerReport report)
+ private SendEmailWithTemplateRequest CreateReportMail(string[] recipients, Project project, ApiWeeklyTaskOwnerReport report)
{
+ var contextId = project.ContextProjectId;
+
var card = new AdaptiveCardBuilder()
.AddHeading($"**Weekly summary - {project.Name}**")
.AddTextRow(report.ActionsAwaitingTaskOwnerAction.ToString(), "Actions awaiting task owners")
@@ -199,7 +256,7 @@ private SendEmailWithTemplateRequest CreateReportMail(string[] recipients, ApiPr
}, new GoToAction()
{
Title = "Go to Access Management",
- Url = $"{fusionUri}/apps/org-admin/{project.OrgProjectExternalId}/access-control"
+ Url = $"{fusionUri}/apps/org-admin/{contextId}/access-control"
})
.AddGrid("Position allocations expiring next 3 months", new List()
{
@@ -226,7 +283,7 @@ private SendEmailWithTemplateRequest CreateReportMail(string[] recipients, ApiPr
}, new GoToAction()
{
Title = "Go to Position Overview",
- Url = $"{fusionUri}/apps/org-admin/{project.OrgProjectExternalId}/edit-positions/listing-view"
+ Url = $"{fusionUri}/apps/org-admin/{contextId}/edit-positions/listing-view"
})
.AddGrid("TBN positions with start date in less than 3 months", new List()
{
@@ -253,7 +310,7 @@ private SendEmailWithTemplateRequest CreateReportMail(string[] recipients, ApiPr
}, new GoToAction()
{
Title = "Go to Position Overview",
- Url = $"{fusionUri}/apps/org-admin/{project.OrgProjectExternalId}/edit-positions/listing-view"
+ Url = $"{fusionUri}/apps/org-admin/{contextId}/edit-positions/listing-view"
})
.Build();
@@ -269,4 +326,21 @@ private SendEmailWithTemplateRequest CreateReportMail(string[] recipients, ApiPr
}
};
}
+
+
+ private class Project
+ {
+ /// Internal id used in summary api
+ public Guid Id { get; set; }
+
+ // Internal id used in org Api
+ public Guid OrgProjectExternalId { get; set; }
+
+ /// Internal id of the connected org context. Used to create the url to the project in Fusion
+ public Guid? ContextProjectId { get; set; }
+
+ public string Name { get; set; } = string.Empty;
+
+ public Guid[] Recipients { get; set; } = [];
+ }
}
\ No newline at end of file
From fdc0c7e3305d2095731bc1216449897d8960a236 Mon Sep 17 00:00:00 2001
From: Jonathan Idland Olsnes <73334350+Jonathanio123@users.noreply.github.com>
Date: Fri, 15 Nov 2024 15:12:45 +0100
Subject: [PATCH 04/10] Fixed buttons not working
---
.../WeeklyTaskOwnerReportSender.cs | 28 ++++++++++++++++---
1 file changed, 24 insertions(+), 4 deletions(-)
diff --git a/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/WeeklyTaskOwnerReportSender.cs b/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/WeeklyTaskOwnerReportSender.cs
index f5e32a248..56de0456d 100644
--- a/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/WeeklyTaskOwnerReportSender.cs
+++ b/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/WeeklyTaskOwnerReportSender.cs
@@ -255,7 +255,7 @@ private SendEmailWithTemplateRequest CreateReportMail(string[] recipients, Proje
}
}, new GoToAction()
{
- Title = "Go to Access Management",
+ Title = "Go to access management",
Url = $"{fusionUri}/apps/org-admin/{contextId}/access-control"
})
.AddGrid("Position allocations expiring next 3 months", new List()
@@ -282,7 +282,7 @@ private SendEmailWithTemplateRequest CreateReportMail(string[] recipients, Proje
}
}, new GoToAction()
{
- Title = "Go to Position Overview",
+ Title = "Go to position overview",
Url = $"{fusionUri}/apps/org-admin/{contextId}/edit-positions/listing-view"
})
.AddGrid("TBN positions with start date in less than 3 months", new List()
@@ -309,24 +309,44 @@ private SendEmailWithTemplateRequest CreateReportMail(string[] recipients, Proje
}
}, new GoToAction()
{
- Title = "Go to Position Overview",
+ Title = "Go to position overview",
Url = $"{fusionUri}/apps/org-admin/{contextId}/edit-positions/listing-view"
})
.Build();
var subject = $"Weekly summary - {project.Name}";
+ var html = cardHtmlRenderer.RenderCard(card).Html;
+
+ TransformActionButtonsToLinks(html);
+
return new SendEmailWithTemplateRequest()
{
Recipients = recipients,
Subject = subject,
MailBody = new()
{
- HtmlContent = cardHtmlRenderer.RenderCard(card).Html.ToString()
+ HtmlContent = html.ToString()
}
};
}
+ private static void TransformActionButtonsToLinks(HtmlTag htmlTag)
+ {
+ if (htmlTag.Classes.Contains("ac-action-openUrl") && htmlTag.Attributes.Any(a => a.Key == "data-ac-url"))
+ {
+ var url = htmlTag.Attributes.First(a => a.Key == "data-ac-url").Value;
+ htmlTag.Element = "a";
+ htmlTag.Attributes.Add("href", url);
+ return;
+ }
+
+ foreach (var child in htmlTag.Children)
+ {
+ TransformActionButtonsToLinks(child);
+ }
+ }
+
private class Project
{
From 51ee75f39871973c594cc696db5e8c3205d11311 Mon Sep 17 00:00:00 2001
From: Jonathan Idland Olsnes <73334350+Jonathanio123@users.noreply.github.com>
Date: Wed, 20 Nov 2024 16:04:35 +0100
Subject: [PATCH 05/10] feat: Invalidate links if context id could not be
resolved
---
.../Functions/TaskOwnerReports/WeeklyTaskOwnerReportSender.cs | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/WeeklyTaskOwnerReportSender.cs b/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/WeeklyTaskOwnerReportSender.cs
index 56de0456d..927931370 100644
--- a/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/WeeklyTaskOwnerReportSender.cs
+++ b/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/WeeklyTaskOwnerReportSender.cs
@@ -318,7 +318,9 @@ private SendEmailWithTemplateRequest CreateReportMail(string[] recipients, Proje
var html = cardHtmlRenderer.RenderCard(card).Html;
- TransformActionButtonsToLinks(html);
+ // If for some reason the context id is not found, we will not be able to create the links
+ if (project.ContextProjectId != null)
+ TransformActionButtonsToLinks(html);
return new SendEmailWithTemplateRequest()
{
From c154956a2bc6f910db1f4466a95bfef8f6c06b8b Mon Sep 17 00:00:00 2001
From: Jonathan Idland Olsnes <73334350+Jonathanio123@users.noreply.github.com>
Date: Thu, 21 Nov 2024 13:08:13 +0100
Subject: [PATCH 06/10] feat: Correct email format for classic outlook
---
.../CardBuilder/AdaptiveCardBuilder.cs | 28 +++-
.../WeeklyTaskOwnerReportSender.cs | 135 ++++++++++++++++--
2 files changed, 151 insertions(+), 12 deletions(-)
diff --git a/src/Fusion.Summary.Functions/CardBuilder/AdaptiveCardBuilder.cs b/src/Fusion.Summary.Functions/CardBuilder/AdaptiveCardBuilder.cs
index 9b310dba1..4519d58b9 100644
--- a/src/Fusion.Summary.Functions/CardBuilder/AdaptiveCardBuilder.cs
+++ b/src/Fusion.Summary.Functions/CardBuilder/AdaptiveCardBuilder.cs
@@ -24,7 +24,7 @@ public AdaptiveCardBuilder AddHeading(string text)
return this;
}
- public AdaptiveCardBuilder AddTextRow(string valueText, string headerText, string customText = "")
+ public AdaptiveCardBuilder AddTextRow(string valueText, string headerText, string customText = "", GoToAction? goToAction = null)
{
var container = new AdaptiveContainer()
{
@@ -47,12 +47,25 @@ public AdaptiveCardBuilder AddTextRow(string valueText, string headerText, strin
}
};
+ if (goToAction != null)
+ {
+ var actionSet = new AdaptiveActionSet();
+ var action = new AdaptiveOpenUrlAction()
+ {
+ Title = goToAction.Title,
+ Url = new Uri(goToAction.Url)
+ };
+
+ actionSet.Actions.Add(action);
+ container.Items.Add(actionSet);
+ }
+
_adaptiveCard.Body.Add(container);
return this;
}
- public AdaptiveCardBuilder AddGrid(string headerText, IEnumerable columns, GoToAction? goToAction = null)
+ public AdaptiveCardBuilder AddGrid(string headerText, string subtitleText, IEnumerable columns, GoToAction? goToAction = null)
{
var listContainer = new AdaptiveContainer
{
@@ -68,6 +81,13 @@ public AdaptiveCardBuilder AddGrid(string headerText, IEnumerable co
HorizontalAlignment = AdaptiveHorizontalAlignment.Center
};
+ var subtitle = new AdaptiveTextBlock
+ {
+ Text = subtitleText,
+ Wrap = true,
+ HorizontalAlignment = AdaptiveHorizontalAlignment.Center
+ };
+
var grid = new AdaptiveColumnSet();
@@ -82,7 +102,8 @@ public AdaptiveCardBuilder AddGrid(string headerText, IEnumerable co
Text = gridCell.Value,
Wrap = true,
HorizontalAlignment = gridCell.Alignment,
- IsSubtle = gridCell.IsHeader
+ IsSubtle = gridCell.IsHeader,
+ Id = gridCell.IsHeader ? "isHeader" : "isCell"
};
rows.Add(cell);
@@ -98,6 +119,7 @@ public AdaptiveCardBuilder AddGrid(string headerText, IEnumerable co
}
listContainer.Items.Add(header);
+ listContainer.Items.Add(subtitle);
listContainer.Items.Add(grid);
// If no data is present, add a "None" text
diff --git a/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/WeeklyTaskOwnerReportSender.cs b/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/WeeklyTaskOwnerReportSender.cs
index 927931370..331259b52 100644
--- a/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/WeeklyTaskOwnerReportSender.cs
+++ b/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/WeeklyTaskOwnerReportSender.cs
@@ -147,7 +147,7 @@ private async Task GetTaskOwnerReportsAsync(IEnumera
taskOwnerReports.Add(report);
}
- catch (SummaryApiError e)
+ catch (Exception e)
{
logger.LogError(e, "Failed to get task owner report for project {Project}", project.ToJson());
}
@@ -230,8 +230,13 @@ private SendEmailWithTemplateRequest CreateReportMail(string[] recipients, Proje
var card = new AdaptiveCardBuilder()
.AddHeading($"**Weekly summary - {project.Name}**")
- .AddTextRow(report.ActionsAwaitingTaskOwnerAction.ToString(), "Actions awaiting task owners")
- .AddGrid("Admin access expiring in less than 3 months", new List()
+ .AddTextRow(report.ActionsAwaitingTaskOwnerAction.ToString(), "Actions awaiting task owners",
+ goToAction: new GoToAction()
+ {
+ Title = "Go to open requests",
+ Url = $"{fusionUri}/apps/org-admin/{contextId}/open-requests?filter=awaiting-task-owner"
+ })
+ .AddGrid("Admin access expiring in less than 3 months", "(Consider extending the access in Access control management)", new List()
{
new()
{
@@ -255,10 +260,10 @@ private SendEmailWithTemplateRequest CreateReportMail(string[] recipients, Proje
}
}, new GoToAction()
{
- Title = "Go to access management",
+ Title = "Go to access control management",
Url = $"{fusionUri}/apps/org-admin/{contextId}/access-control"
})
- .AddGrid("Position allocations expiring next 3 months", new List()
+ .AddGrid("Allocations expiring next 3 months", "(Contact the resource owner if there is a need to extend the allocation)", new List()
{
new()
{
@@ -285,7 +290,7 @@ private SendEmailWithTemplateRequest CreateReportMail(string[] recipients, Proje
Title = "Go to position overview",
Url = $"{fusionUri}/apps/org-admin/{contextId}/edit-positions/listing-view"
})
- .AddGrid("TBN positions with start date in less than 3 months", new List()
+ .AddGrid("TBN positions with start date next 3 months", "(Please create a resource request or update the position start-date)", new List()
{
new()
{
@@ -310,7 +315,7 @@ private SendEmailWithTemplateRequest CreateReportMail(string[] recipients, Proje
}, new GoToAction()
{
Title = "Go to position overview",
- Url = $"{fusionUri}/apps/org-admin/{contextId}/edit-positions/listing-view"
+ Url = $"{fusionUri}/apps/org-admin/{contextId}/edit-positions/listing-view?filter=tbn-pos-3m"
})
.Build();
@@ -322,6 +327,8 @@ private SendEmailWithTemplateRequest CreateReportMail(string[] recipients, Proje
if (project.ContextProjectId != null)
TransformActionButtonsToLinks(html);
+ ReplaceColumnSetsWithTables(html);
+
return new SendEmailWithTemplateRequest()
{
Recipients = recipients,
@@ -333,19 +340,129 @@ private SendEmailWithTemplateRequest CreateReportMail(string[] recipients, Proje
};
}
- private static void TransformActionButtonsToLinks(HtmlTag htmlTag)
+ private static void TransformActionButtonsToLinks(HtmlTag htmlTag, HtmlTag? parent = null)
{
if (htmlTag.Classes.Contains("ac-action-openUrl") && htmlTag.Attributes.Any(a => a.Key == "data-ac-url"))
{
var url = htmlTag.Attributes.First(a => a.Key == "data-ac-url").Value;
htmlTag.Element = "a";
htmlTag.Attributes.Add("href", url);
+ htmlTag.Styles.Add("text-align", "center");
+
+ var childDivText = htmlTag.Children.FirstOrDefault(c => c.Element == "div");
+
+ if (childDivText != null)
+ {
+ htmlTag.Text = childDivText.Text;
+ htmlTag.Children.Remove(childDivText);
+ }
+
+ // Needed because of classic outlook rendering...
+ if (parent != null)
+ {
+ parent.Styles.Add("text-align", "center");
+ }
+
+
return;
}
foreach (var child in htmlTag.Children)
{
- TransformActionButtonsToLinks(child);
+ TransformActionButtonsToLinks(child, htmlTag);
+ }
+ }
+
+ ///
+ /// Default column html generated from the adaptive card renderer is too advanced for classic outlook rendering.
+ /// This replaces the advanced table with a simple html table element. This is hardcoded to the specific structure of
+ /// the adaptive card and two columns.
+ ///
+ private static void ReplaceColumnSetsWithTables(HtmlTag reportHtml)
+ {
+ var columnsSets = RecursiveGetChildren(reportHtml).Where(c => c.Classes.Contains("ac-columnset")).ToArray();
+
+
+ foreach (var columnsSet in columnsSets)
+ {
+ var headers = RecursiveGetChildren(columnsSet).Where(c => c.Attributes.Any(a => a.Key == "name" && a.Value == "isHeader"))
+ .Select(c => c.Children.First().Children.First().Text)
+ .ToList();
+
+ var cells = RecursiveGetChildren(columnsSet).Where(c => c.Attributes.Any(a => a.Key == "name" && a.Value == "isCell"));
+ var cellValues = cells.Select(c => c.Children.First().Children.First().Text).ToList();
+
+
+ var namesList = cellValues.Slice(0, cellValues.Count / 2);
+ var dateList = cellValues.Slice(cellValues.Count / 2, cellValues.Count / 2);
+
+
+ var table = new HtmlTag("table")
+ {
+ Styles = new Dictionary
+ {
+ { "width", "800px" },
+ { "text-align", "left" },
+ { "margin", "auto" }
+ }
+ };
+
+ var headerRow = new HtmlTag("tr");
+ foreach (var header in headers)
+ {
+ headerRow.Children.Add(new HtmlTag("th")
+ {
+ Text = header,
+ Attributes = new Dictionary()
+ {
+ { "align", "left" }
+ },
+ Styles = new Dictionary()
+ {
+ { "text-align", "left" }
+ }
+ });
+ }
+
+ table.Children.Add(headerRow);
+
+ for (var i = 0; i < cellValues.Count / 2; i++)
+ {
+ var row = new HtmlTag("tr");
+
+ row.Children.Add(new HtmlTag("td")
+ {
+ Text = namesList[i],
+ Styles =
+ {
+ { "width", "80%" }
+ }
+ });
+ row.Children.Add(new HtmlTag("td") { Text = dateList[i] });
+
+ table.Children.Add(row);
+ }
+
+ var parent = RecursiveGetChildren(reportHtml).First(c => c.Children.Contains(columnsSet));
+ var columnIndex = parent.Children.IndexOf(columnsSet);
+
+ parent.Children.RemoveAt(columnIndex);
+ var tableWrapper = new HtmlTag("div");
+ tableWrapper.Children.Add(table);
+ parent.Children.Insert(columnIndex, tableWrapper);
+ }
+ }
+
+ private static IEnumerable RecursiveGetChildren(HtmlTag htmlTag)
+ {
+ foreach (var child in htmlTag.Children)
+ {
+ yield return child;
+
+ foreach (var grandChild in RecursiveGetChildren(child))
+ {
+ yield return grandChild;
+ }
}
}
From 3eed0cbf3fd21ff0648809c488b21aa1ae86bada Mon Sep 17 00:00:00 2001
From: Jonathan Idland Olsnes <73334350+Jonathanio123@users.noreply.github.com>
Date: Thu, 21 Nov 2024 15:06:53 +0100
Subject: [PATCH 07/10] chore: Disabled weekly-task-owner-report-sender
function
---
.../Deployment/disabled-functions.json | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/src/Fusion.Summary.Functions/Deployment/disabled-functions.json b/src/Fusion.Summary.Functions/Deployment/disabled-functions.json
index fea774561..b508aa218 100644
--- a/src/Fusion.Summary.Functions/Deployment/disabled-functions.json
+++ b/src/Fusion.Summary.Functions/Deployment/disabled-functions.json
@@ -11,13 +11,15 @@
{
"environment": "fqa",
"disabledFunctions": [
- "weekly-project-recipients-sync"
+ "weekly-project-recipients-sync",
+ "weekly-task-owner-report-sender"
]
},
{
"environment": "fprd",
"disabledFunctions": [
- "weekly-project-recipients-sync"
+ "weekly-project-recipients-sync",
+ "weekly-task-owner-report-sender"
]
}
]
From 37b99df5cc7476a333d7829e411406b0d101f56f Mon Sep 17 00:00:00 2001
From: Jonathan Idland Olsnes <73334350+Jonathanio123@users.noreply.github.com>
Date: Fri, 29 Nov 2024 11:35:53 +0100
Subject: [PATCH 08/10] chore: Added mail endpoint configuration
---
pipelines/templates/deploy-summary-function-pr-template.yml | 1 +
pipelines/templates/deploy-summary-function-template.yml | 3 +++
src/Fusion.Summary.Functions/Deployment/function.template.json | 3 ++-
3 files changed, 6 insertions(+), 1 deletion(-)
diff --git a/pipelines/templates/deploy-summary-function-pr-template.yml b/pipelines/templates/deploy-summary-function-pr-template.yml
index a9dd7ebe7..f3b48cb38 100644
--- a/pipelines/templates/deploy-summary-function-pr-template.yml
+++ b/pipelines/templates/deploy-summary-function-pr-template.yml
@@ -59,6 +59,7 @@ steps:
resources = "https://fra-resources-$pullRequestNumber.pr.api.fusion-dev.net"
summary = "https://fra-summary-$pullRequestNumber.pr.api.fusion-dev.net"
roles = "https://roles.$fusionEnvironment.api.fusion-dev.net"
+ mail = "https://mail.$fusionEnvironment.api.fusion-dev.net"
}
resources = @{
fusion = "${{ parameters.fusionResource }}"
diff --git a/pipelines/templates/deploy-summary-function-template.yml b/pipelines/templates/deploy-summary-function-template.yml
index 09804e244..3f9db2d2d 100644
--- a/pipelines/templates/deploy-summary-function-template.yml
+++ b/pipelines/templates/deploy-summary-function-template.yml
@@ -48,6 +48,7 @@ steps:
$notifications = "https://notification.api.fusion.equinor.com"
$context = "https://context.api.fusion.equinor.com"
$roles = "https://roles.api.fusion.equinor.com"
+ $mail = "https://mail.api.fusion.equinor.com"
}
else {
$summary = "https://fra-summary.$environment.api.fusion-dev.net"
@@ -59,6 +60,7 @@ steps:
$context = "https://context.$fusionEnvironment.api.fusion-dev.net"
$portal = "https://fusion.$fusionEnvironment.fusion-dev.net"
$roles = "https://roles.$fusionEnvironment.api.fusion-dev.net"
+ $mail = "https://mail.$fusionEnvironment.api.fusion-dev.net"
}
$settings = @{
@@ -79,6 +81,7 @@ steps:
portal = $portal
summary = $summary
roles = $roles
+ mail = $mail
}
resources = @{
fusion = "${{ parameters.fusionResource }}"
diff --git a/src/Fusion.Summary.Functions/Deployment/function.template.json b/src/Fusion.Summary.Functions/Deployment/function.template.json
index 4dcf485e2..e74d92b5b 100644
--- a/src/Fusion.Summary.Functions/Deployment/function.template.json
+++ b/src/Fusion.Summary.Functions/Deployment/function.template.json
@@ -21,7 +21,8 @@
"resources": "[concat('https://fra-resources.', parameters('env-name'), '.api.fusion-dev.net')]",
"notifications": "https://notification.ci.api.fusion-dev.net",
"context": "https://context.ci.api.fusion-dev.net",
- "portal": "https://fusion.ci.fusion-dev.net"
+ "portal": "https://fusion.ci.fusion-dev.net",
+ "mail": "https://mail.ci.api.fusion-dev.net"
},
"resources": {
"fusion": "5a842df8-3238-415d-b168-9f16a6a6031b"
From af679a4736e06ac784c430abfea06547b20cf9ba Mon Sep 17 00:00:00 2001
From: Jonathan Idland Olsnes <73334350+Jonathanio123@users.noreply.github.com>
Date: Fri, 29 Nov 2024 13:59:14 +0100
Subject: [PATCH 09/10] fix: Added empty row to fix formating issues when there
was no data
---
.../CardBuilder/AdaptiveCardBuilder.cs | 33 +++++++++++++++++--
1 file changed, 31 insertions(+), 2 deletions(-)
diff --git a/src/Fusion.Summary.Functions/CardBuilder/AdaptiveCardBuilder.cs b/src/Fusion.Summary.Functions/CardBuilder/AdaptiveCardBuilder.cs
index 4519d58b9..bdc9b28f9 100644
--- a/src/Fusion.Summary.Functions/CardBuilder/AdaptiveCardBuilder.cs
+++ b/src/Fusion.Summary.Functions/CardBuilder/AdaptiveCardBuilder.cs
@@ -65,8 +65,9 @@ public AdaptiveCardBuilder AddTextRow(string valueText, string headerText, strin
}
- public AdaptiveCardBuilder AddGrid(string headerText, string subtitleText, IEnumerable columns, GoToAction? goToAction = null)
+ public AdaptiveCardBuilder AddGrid(string headerText, string subtitleText, IEnumerable columnsEnumerable, GoToAction? goToAction = null)
{
+ var columns = columnsEnumerable.ToList();
var listContainer = new AdaptiveContainer
{
Separator = true
@@ -118,12 +119,40 @@ public AdaptiveCardBuilder AddGrid(string headerText, string subtitleText, IEnum
grid.Columns.Add(gridColumn);
}
+ // Add empty row so that things are aligned correctly
+ if (columns.SelectMany(c => c.Cells).All(c => c.IsHeader))
+ {
+ var rows = new List
+ {
+ new AdaptiveTextBlock
+ {
+ Text = "-",
+ Wrap = true,
+ HorizontalAlignment = AdaptiveHorizontalAlignment.Left,
+ Id = "isCell"
+ },
+ new AdaptiveTextBlock
+ {
+ Text = "-",
+ Wrap = true,
+ HorizontalAlignment = AdaptiveHorizontalAlignment.Right,
+ Id = "isCell"
+ }
+ };
+
+ grid.Columns.Add(new AdaptiveColumn
+ {
+ Width = AdaptiveColumnWidth.Auto,
+ Items = rows
+ });
+ }
+
listContainer.Items.Add(header);
listContainer.Items.Add(subtitle);
listContainer.Items.Add(grid);
// If no data is present, add a "None" text
- if (columns.SelectMany(c => c.Cells).All(c => c.IsHeader))
+ if (columns.SelectMany(c => c.Cells).All(c => c.IsHeader || string.IsNullOrEmpty(c.Value)))
{
listContainer.Items.Add(new AdaptiveTextBlock
{
From bb1f38f5e5c8769fc21692cf7370cb83e8cedf46 Mon Sep 17 00:00:00 2001
From: Jonathan Idland Olsnes <73334350+Jonathanio123@users.noreply.github.com>
Date: Mon, 2 Dec 2024 10:18:01 +0100
Subject: [PATCH 10/10] fix: Added missing mail section in
function.template.json
---
.../Deployment/function.template.json | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/src/Fusion.Summary.Functions/Deployment/function.template.json b/src/Fusion.Summary.Functions/Deployment/function.template.json
index e74d92b5b..3cdebdf69 100644
--- a/src/Fusion.Summary.Functions/Deployment/function.template.json
+++ b/src/Fusion.Summary.Functions/Deployment/function.template.json
@@ -166,6 +166,10 @@
"name": "Endpoints_roles",
"value": "[parameters('settings').endpoints.roles]"
},
+ {
+ "name": "Endpoints_mail",
+ "value": "[parameters('settings').endpoints.mail]"
+ },
{
"name": "Endpoints_Resources_Fusion",
"value": "[parameters('settings').resources.fusion]"