diff --git a/DigitalLearningSolutions.Data/Extensions/ConfigurationExtensions.cs b/DigitalLearningSolutions.Data/Extensions/ConfigurationExtensions.cs index 7d2e2447a5..2dd6ed5cb4 100644 --- a/DigitalLearningSolutions.Data/Extensions/ConfigurationExtensions.cs +++ b/DigitalLearningSolutions.Data/Extensions/ConfigurationExtensions.cs @@ -49,7 +49,17 @@ public static class ConfigurationExtensions private const string LearningHubUserAPIUserAPIUrl = "LearningHubUserApi:UserApiUrl"; private const string UserResearchUrlName = "UserResearchUrl"; - + private const string TableauSectionKey = "TableauDashboards"; + private const string TableauClientId = "ClientId"; + private const string TableauClientSecretId = "ClientSecretId"; + private const string TableauClientSecret = "ClientSecret"; + private const string TableauUsername = "Username"; + private const string TableauClientName = "ClientName"; + private const string TableauSiteUrl = "SiteUrl"; + private const string TableauWorkbookName = "WorkBookName"; + private const string TableauViewName = "ViewName"; + private const string TableauSiteName = "SiteName"; + private const string TableauAuthApi = "AuthApiPath"; public static string GetAppRootPath(this IConfiguration config) { return config[AppRootPathName]!; @@ -180,7 +190,7 @@ public static int GetExportQueryRowLimit(this IConfiguration config) } public static int GetMaxBulkUploadRowsLimit(this IConfiguration config) { - int.TryParse(config[MaxBulkUploadRowsLimitKey],out int limitKey); + int.TryParse(config[MaxBulkUploadRowsLimitKey], out int limitKey); return limitKey; } @@ -201,7 +211,7 @@ public static string GetLearningHubAuthenticationClientSecret(this IConfiguratio public static long GetFreshdeskCreateTicketGroupId(this IConfiguration config) { - long.TryParse(config[FreshdeskCreateTicketGroupId], out long ticketGroupId); + long.TryParse(config[FreshdeskCreateTicketGroupId], out long ticketGroupId); return ticketGroupId; } public static long GetFreshdeskCreateTicketProductId(this IConfiguration config) @@ -218,5 +228,46 @@ public static string GetUserResearchUrl(this IConfiguration config) { return config[UserResearchUrlName]!; } + public static string GetTableauClientName(this IConfiguration config) + { + return config[$"{TableauSectionKey}:{TableauClientName}"]!; + } + public static string GetTableauClientId(this IConfiguration config) + { + return config[$"{TableauSectionKey}:{TableauClientId}"]!; + } + public static string GetTableauClientSecret(this IConfiguration config) + { + return config[$"{TableauSectionKey}:{TableauClientSecret}"]!; + } + public static string GetTableauClientSecretId(this IConfiguration config) + { + return config[$"{TableauSectionKey}:{TableauClientSecretId}"]!; + } + public static string GetTableauUser(this IConfiguration config) + { + return config[$"{TableauSectionKey}:{TableauUsername}"]!; + } + public static string GetTableauSiteUrl(this IConfiguration config) + { + return config[$"{TableauSectionKey}:{TableauSiteUrl}"]!; + } + public static string GetTableauAuthApi(this IConfiguration config) + { + return config[$"{TableauSectionKey}:{TableauAuthApi}"]!; + } + public static string GetTableauSiteName(this IConfiguration config) + { + return config[$"{TableauSectionKey}:{TableauSiteName}"]!; + } + public static string GetTableauWorkbookName(this IConfiguration config) + { + return config[$"{TableauSectionKey}:{TableauWorkbookName}"]!; + } + public static string GetTableauViewName(this IConfiguration config) + { + return config[$"{TableauSectionKey}:{TableauViewName}"]!; + } + } } diff --git a/DigitalLearningSolutions.Web/Controllers/TrackingSystem/Centre/SelfAssessmentReports/SelfAssessmentReportsController.cs b/DigitalLearningSolutions.Web/Controllers/TrackingSystem/Centre/SelfAssessmentReports/SelfAssessmentReportsController.cs index 8955b4b2a6..dccb1d0301 100644 --- a/DigitalLearningSolutions.Web/Controllers/TrackingSystem/Centre/SelfAssessmentReports/SelfAssessmentReportsController.cs +++ b/DigitalLearningSolutions.Web/Controllers/TrackingSystem/Centre/SelfAssessmentReports/SelfAssessmentReportsController.cs @@ -11,6 +11,10 @@ using System; using DigitalLearningSolutions.Data.Utilities; using DigitalLearningSolutions.Web.ViewModels.TrackingSystem.Centre.Reports; + using DigitalLearningSolutions.Web.Helpers.ExternalApis; + using System.Threading.Tasks; + using Microsoft.Extensions.Configuration; + using DigitalLearningSolutions.Data.Extensions; [FeatureGate(FeatureFlags.RefactoredTrackingSystem)] [Authorize(Policy = CustomPolicies.UserCentreAdmin)] @@ -20,15 +24,26 @@ public class SelfAssessmentReportsController : Controller { private readonly ISelfAssessmentReportService selfAssessmentReportService; + private readonly ITableauConnectionHelperService tableauConnectionHelper; private readonly IClockUtility clockUtility; - + private readonly string tableauUrl; + private readonly string tableauSiteName; + private readonly string workbookName; + private readonly string viewName; public SelfAssessmentReportsController( ISelfAssessmentReportService selfAssessmentReportService, - IClockUtility clockUtility + ITableauConnectionHelperService tableauConnectionHelper, + IClockUtility clockUtility, + IConfiguration config ) { this.selfAssessmentReportService = selfAssessmentReportService; + this.tableauConnectionHelper = tableauConnectionHelper; this.clockUtility = clockUtility; + tableauUrl = config.GetTableauSiteUrl(); + tableauSiteName = config.GetTableauSiteName(); + workbookName = config.GetTableauWorkbookName(); + viewName = config.GetTableauViewName(); } public IActionResult Index() { @@ -63,5 +78,19 @@ public IActionResult DownloadSelfAssessmentReport(int selfAssessmentId) fileName ); } + [HttpGet] + [Route("TableauCompetencyDashboard")] + public IActionResult TableauCompetencyDashboard() + { + var userEmail = User.GetUserPrimaryEmail(); + var jwt = tableauConnectionHelper.GetTableauJwt(userEmail); + ViewBag.SiteName = tableauSiteName; + ViewBag.TableauServerUrl = tableauUrl; + ViewBag.WorkbookName = workbookName; + ViewBag.ViewName = viewName; + ViewBag.JwtToken = jwt; + + return View(); + } } } diff --git a/DigitalLearningSolutions.Web/Helpers/ExternalApis/FilteredApiHelper.cs b/DigitalLearningSolutions.Web/Helpers/ExternalApis/FilteredApiHelper.cs index d0f4ae16f0..7c39bbe027 100644 --- a/DigitalLearningSolutions.Web/Helpers/ExternalApis/FilteredApiHelper.cs +++ b/DigitalLearningSolutions.Web/Helpers/ExternalApis/FilteredApiHelper.cs @@ -6,12 +6,10 @@ using System.Net.Http; using System.Security.Claims; using System.Text; - using System.Text.Json; using System.Threading.Tasks; using Newtonsoft.Json; using System.Net.Http.Headers; using DigitalLearningSolutions.Data.Models.External.Filtered; - using System.Collections; using System.Collections.Generic; using System.Linq; using DigitalLearningSolutions.Data.Utilities; diff --git a/DigitalLearningSolutions.Web/Helpers/ExternalApis/TableauConnectionHelper.cs b/DigitalLearningSolutions.Web/Helpers/ExternalApis/TableauConnectionHelper.cs new file mode 100644 index 0000000000..ffe5240748 --- /dev/null +++ b/DigitalLearningSolutions.Web/Helpers/ExternalApis/TableauConnectionHelper.cs @@ -0,0 +1,53 @@ +namespace DigitalLearningSolutions.Web.Helpers.ExternalApis +{ + using Microsoft.IdentityModel.Tokens; + using System.IdentityModel.Tokens.Jwt; + using System.Text; + using System; + using Microsoft.Extensions.Configuration; + using DigitalLearningSolutions.Data.Extensions; + + public interface ITableauConnectionHelperService + { + string GetTableauJwt(string email); + } + public class TableauConnectionHelper : ITableauConnectionHelperService + { + private readonly string connectedAppSecretKey; + private readonly string connectedAppSecretId; + private readonly string connectedAppClientId; + private readonly string user; + public TableauConnectionHelper(IConfiguration config) + { + connectedAppClientId = config.GetTableauClientId(); + connectedAppSecretId = config.GetTableauClientSecretId(); + connectedAppSecretKey = config.GetTableauClientSecret(); + user = config.GetTableauUser(); + } + public string GetTableauJwt(string email) + { + var key = Encoding.UTF8.GetBytes(connectedAppSecretKey); + var signingCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256); + var header = new JwtHeader(signingCredentials) + { + { "kid", connectedAppSecretId }, + { "iss", connectedAppClientId } + }; + + var payload = new JwtPayload + { + { "jti", Guid.NewGuid().ToString()}, + { "iss", connectedAppClientId }, + { "aud", "tableau" }, + { "exp", new DateTimeOffset(DateTime.UtcNow.AddMinutes(5)).ToUnixTimeSeconds() }, + { "sub", user }, + { "scp", new[] { "tableau:views:embed" } }, + { "ExernalUserEmail", new [] { email } } + }; + var token = new JwtSecurityToken(header, payload); + var tokenHandler = new JwtSecurityTokenHandler(); + var tokenString = tokenHandler.WriteToken(token); + return tokenString; + } + } +} diff --git a/DigitalLearningSolutions.Web/Helpers/FeatureFlags.cs b/DigitalLearningSolutions.Web/Helpers/FeatureFlags.cs index 5390708ea2..e88d41d79e 100644 --- a/DigitalLearningSolutions.Web/Helpers/FeatureFlags.cs +++ b/DigitalLearningSolutions.Web/Helpers/FeatureFlags.cs @@ -14,5 +14,6 @@ public static class FeatureFlags public const string UserFeedbackBar = "UserFeedbackBar"; public const string ShowSelfAssessmentProgressButtons = "ShowSelfAssessmentProgressButtons"; public const string LoginWithLearningHub = "LoginWithLearningHub"; + public const string TableauSelfAssessmentDashboards = "TableauSelfAssessmentDashboards"; } } diff --git a/DigitalLearningSolutions.Web/Startup.cs b/DigitalLearningSolutions.Web/Startup.cs index 0433f1637f..193bdd1d2f 100644 --- a/DigitalLearningSolutions.Web/Startup.cs +++ b/DigitalLearningSolutions.Web/Startup.cs @@ -46,21 +46,13 @@ namespace DigitalLearningSolutions.Web using Microsoft.Extensions.Hosting; using Microsoft.FeatureManagement; using Microsoft.IdentityModel.Protocols.OpenIdConnect; - using Microsoft.IdentityModel.Tokens; - using Microsoft.AspNetCore.Http; - using System.Linq; using Microsoft.AspNetCore.Identity; - using AspNetCoreRateLimit; using static DigitalLearningSolutions.Data.DataServices.ICentreApplicationsDataService; using static DigitalLearningSolutions.Web.Services.ICentreApplicationsService; using static DigitalLearningSolutions.Web.Services.ICentreSelfAssessmentsService; using System; using IsolationLevel = System.Transactions.IsolationLevel; - using System.Collections.Concurrent; using Serilog; - using static DigitalLearningSolutions.Data.DataServices.ICentreApplicationsDataService; - using static DigitalLearningSolutions.Web.Services.ICentreApplicationsService; - using static DigitalLearningSolutions.Web.Services.ICentreSelfAssessmentsService; public class Startup { @@ -560,6 +552,7 @@ private static void RegisterHttpClients(IServiceCollection services) services.AddHttpClient(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); } private static void RegisterWebServiceFilters(IServiceCollection services) @@ -592,12 +585,13 @@ private static void RegisterWebServiceFilters(IServiceCollection services) public void Configure(IApplicationBuilder app, IMigrationRunner migrationRunner, IFeatureManager featureManager) { + var tableauServerUrl = config.GetTableauSiteUrl(); app.UseMiddleware(); app.Use(async (context, next) => { context.Response.Headers.Add("content-security-policy", "default-src 'self'; " + - "script-src 'self' 'unsafe-hashes' 'sha256-oywvD6W6okwID679n4cvPJtWLowSS70Pz87v1ryS0DU=' 'sha256-kbHtQyYDQKz4SWMQ8OHVol3EC0t3tHEJFPCSwNG9NxQ' 'sha256-YoDy5WvNzQHMq2kYTFhDYiGnEgPrvAY5Il6eUu/P4xY=' 'sha256-/n13APBYdqlQW71ZpWflMB/QoXNSUKDxZk1rgZc+Jz8=' https://script.hotjar.com https://www.google-analytics.com https://static.hotjar.com https://www.googletagmanager.com https://cdnjs.cloudflare.com 'sha256-+6WnXIl4mbFTCARd8N3COQmT3bJJmo32N8q8ZSQAIcU=' 'sha256-VQKp2qxuvQmMpqE/U/ASQ0ZQ0pIDvC3dgQPPCqDlvBo=';" + + $"script-src 'self' 'nonce-random772362' https://script.hotjar.com https://www.google-analytics.com https://static.hotjar.com https://www.googletagmanager.com https://cdnjs.cloudflare.com {tableauServerUrl} 'unsafe-hashes' 'sha256-oywvD6W6okwID679n4cvPJtWLowSS70Pz87v1ryS0DU=' 'sha256-kbHtQyYDQKz4SWMQ8OHVol3EC0t3tHEJFPCSwNG9NxQ' 'sha256-YoDy5WvNzQHMq2kYTFhDYiGnEgPrvAY5Il6eUu/P4xY=' 'sha256-/n13APBYdqlQW71ZpWflMB/QoXNSUKDxZk1rgZc+Jz8=' 'sha256-+6WnXIl4mbFTCARd8N3COQmT3bJJmo32N8q8ZSQAIcU=' 'sha256-VQKp2qxuvQmMpqE/U/ASQ0ZQ0pIDvC3dgQPPCqDlvBo=';" + "style-src 'self' 'unsafe-inline' https://use.fontawesome.com; " + "font-src https://script.hotjar.com https://assets.nhs.uk/; " + "connect-src 'self' http: ws:; " + diff --git a/DigitalLearningSolutions.Web/Views/TrackingSystem/Centre/SelfAssessmentReports/Index.cshtml b/DigitalLearningSolutions.Web/Views/TrackingSystem/Centre/SelfAssessmentReports/Index.cshtml index 58f32cd637..d61717e107 100644 --- a/DigitalLearningSolutions.Web/Views/TrackingSystem/Centre/SelfAssessmentReports/Index.cshtml +++ b/DigitalLearningSolutions.Web/Views/TrackingSystem/Centre/SelfAssessmentReports/Index.cshtml @@ -1,5 +1,6 @@ @using DigitalLearningSolutions.Web.Models.Enums @using DigitalLearningSolutions.Web.ViewModels.TrackingSystem.Centre.Reports +@using DigitalLearningSolutions.Web.Helpers @model SelfAssessmentReportsViewModel @{ ViewData["Title"] = "Self assessment reports"; @@ -18,7 +19,14 @@ - @ViewData["Title"] -

Use this page to download Excel learner activity reports for competency self assessments at your centre.

+ + @if (Model.SelfAssessmentSelects.Any()) + { + + } + +

Excel learner activity reports

+

Download Excel competency self assessments activity reports for your centre.

+ + diff --git a/DigitalLearningSolutions.Web/Views/TrackingSystem/Centre/SelfAssessmentReports/TableauCompetencyDashboard.cshtml b/DigitalLearningSolutions.Web/Views/TrackingSystem/Centre/SelfAssessmentReports/TableauCompetencyDashboard.cshtml new file mode 100644 index 0000000000..163d6d7b46 --- /dev/null +++ b/DigitalLearningSolutions.Web/Views/TrackingSystem/Centre/SelfAssessmentReports/TableauCompetencyDashboard.cshtml @@ -0,0 +1,33 @@ +@using DigitalLearningSolutions.Web.Helpers +@{ + var tableauServerUrl = ViewBag.TableauServerUrl; + var workbookName = ViewBag.WorkbookName; + var viewName = ViewBag.ViewName; + var jwtToken = ViewBag.JwtToken; + var siteName = ViewBag.SiteName; + var srcUrl = $"{tableauServerUrl}/t/{siteName}/views/{workbookName}/{viewName}"; + ViewData["Title"] = "Supervised self assessments dashboard"; +} + +

@ViewData["Title"]

+ + + If the dashboard doesn't appear after a few seconds, reload the page + + @section scripts { + @* We are not using Yarn/npm for the Tableau JS becaue of errors during installation relating to a missing dependency *@ + + } + + +

Oops! We are still working on this area of the site

+

This feature is under development and should be available soon.

+
diff --git a/DigitalLearningSolutions.Web/appsettings.Production.json b/DigitalLearningSolutions.Web/appsettings.Production.json index a8675c0954..02a9d9fd44 100644 --- a/DigitalLearningSolutions.Web/appsettings.Production.json +++ b/DigitalLearningSolutions.Web/appsettings.Production.json @@ -22,7 +22,8 @@ "UserFeedbackBar": true, "ExportQueryRowLimit": 250, "MaxBulkUploadRows": 200, - "LoginWithLearningHub": true + "LoginWithLearningHub": true, + "TableauSelfAssessmentDashboards": false }, "LearningHubOpenAPIBaseUrl": "https://learninghubnhsuk-openapi-prod.azurewebsites.net", "LearningHubReportAPIConfig": { diff --git a/DigitalLearningSolutions.Web/appsettings.json b/DigitalLearningSolutions.Web/appsettings.json index 30ee426241..4d7000b3e3 100644 --- a/DigitalLearningSolutions.Web/appsettings.json +++ b/DigitalLearningSolutions.Web/appsettings.json @@ -17,7 +17,8 @@ "UseSignposting": true, "PricingPage": true, "ShowSelfAssessmentProgressButtons": false, - "LoginWithLearningHub": true + "LoginWithLearningHub": true, + "TableauSelfAssessmentDashboards": true }, "LearningHubOpenAPIBaseUrl": "https://uks-learninghubnhsuk-openapi-test.azurewebsites.net", "LearningHubOpenAPIKey": "", @@ -78,5 +79,18 @@ "LearningHubUserApi": { "UserApiUrl": "https://userapi.learninghub.nhs.uk/api/" }, + "TableauDashboards": { + "SiteUrl": "https://tabuat.data.england.nhs.uk", + "SiteName": "monitor", + "AuthApiPath": "/api/3.21/auth/signin", + "WorkBookName": "DLSIdentifiableDataNHSEUAT", + "ViewName": "Cover", + "CompetencyDashboardUrl": "https://tabuat.data.england.nhs.uk/#/site/monitor/views/DLSIdentifiableDataNHSEUAT/Cover", + "Username": "svc-tel-dls", + "ClientName": "tel_dls", + "ClientId": "a7906ce3-e0c9-403e-a169-8eb78d858f8a", + "ClientSecretId": "38b99058-a806-4ee2-bf7a-ad1933a81ae5", + "ClientSecret": "" + }, "UserResearchUrl": "https://forms.office.com/e/nKcK8AdHRX" }