diff --git a/.gitignore b/.gitignore index 8cc7d8bb..3b61a883 100644 --- a/.gitignore +++ b/.gitignore @@ -56,3 +56,8 @@ obj /AdminUI/LearningHub.Nhs.AdminUI/web.config /LearningHub.Nhs.WebUI/web.config /WebAPI/LearningHub.Nhs.API/web.config +/LearningHub.Nhs.WebUI/nuget.config +/LearningHub.Nhs.WebUI.BlazorClient/Properties/launchSettings.json +/LearningHub.Nhs.WebUI.BlazorClient/wwwroot/appsettings.json +/LearningHub.Nhs.WebUI.BlazorClient/wwwroot/appsettings.Development.json +/LearningHub.Nhs.WebUI.BlazorClient/nuget.config diff --git a/LearningHub.Nhs.Shared/Configuration/FindwiseSettingsPublic.cs b/LearningHub.Nhs.Shared/Configuration/ExposableFindwiseSettings.cs similarity index 92% rename from LearningHub.Nhs.Shared/Configuration/FindwiseSettingsPublic.cs rename to LearningHub.Nhs.Shared/Configuration/ExposableFindwiseSettings.cs index 830351ed..0b915bdd 100644 --- a/LearningHub.Nhs.Shared/Configuration/FindwiseSettingsPublic.cs +++ b/LearningHub.Nhs.Shared/Configuration/ExposableFindwiseSettings.cs @@ -9,7 +9,7 @@ /// Contains only non-sensitive data such as page sizes for various search types. /// /// - public class FindwiseSettingsPublic : IFindwiseSettingsPublic + public class ExposableFindwiseSettings : IExposableFindwiseSettings { /// /// Gets or sets the ResourceSearchPageSize. diff --git a/LearningHub.Nhs.Shared/Configuration/ExposableSettings.cs b/LearningHub.Nhs.Shared/Configuration/ExposableSettings.cs new file mode 100644 index 00000000..25a82505 --- /dev/null +++ b/LearningHub.Nhs.Shared/Configuration/ExposableSettings.cs @@ -0,0 +1,36 @@ +namespace LearningHub.Nhs.Shared.Configuration +{ + using LearningHub.Nhs.Shared.Interfaces.Configuration; + /// + /// Represents configuration values that are safe to expose to clientside frontend applications + /// (such as Blazor WebAssembly) or public-facing APIs. + /// + /// + /// Implements and contains only non-sensitive, non-secret + /// values such as public API endpoints and pagination settings. This separation ensures + /// that secure or private configuration data is not inadvertently exposed to clients. + /// + /// + public class ExposableSettings : IExposableSettings + { + /// + public string LearningHubApiUrl { get; set; } + + /// + /// Gets or sets the UserApiUrl. + /// + public string UserApiUrl { get; set; } + + /// + /// Gets or sets the OpenApiUrl. + /// + public string OpenApiUrl { get; set; } + /// + /// Backend for Frontend (BFF) URL for the Learning Hub API accessed by samesite cookie and uses httpclients with bearers to access external apis. + /// + public string LearningHubApiBFFUrl { get; set; } + /// + public IExposableFindwiseSettings FindwiseSettings { get; set; } + + } +} diff --git a/LearningHub.Nhs.Shared/Configuration/PublicSettings.cs b/LearningHub.Nhs.Shared/Configuration/PublicSettings.cs deleted file mode 100644 index c8607094..00000000 --- a/LearningHub.Nhs.Shared/Configuration/PublicSettings.cs +++ /dev/null @@ -1,31 +0,0 @@ -namespace LearningHub.Nhs.Shared.Configuration -{ - using LearningHub.Nhs.Shared.Interfaces.Configuration; - /// - /// Represents configuration values that are safe to expose to clientside frontend applications - /// (such as Blazor WebAssembly) or public-facing APIs. - /// - /// - /// Implements and contains only non-sensitive, non-secret - /// values such as public API endpoints and pagination settings. This separation ensures - /// that secure or private configuration data is not inadvertently exposed to clients. - /// - /// - public class PublicSettings : IPublicSettings - { - /// - public string LearningHubApiUrl { get; set; } - - /// - public int ResourceSearchPageSize { get; set; } - - /// - public int CatalogueSearchPageSize { get; set; } - - /// - public int AllCatalogueSearchPageSize { get; set; } - - /// - public IFindwiseSettingsPublic FindwiseSettings { get; set; } - } -} diff --git a/LearningHub.Nhs.Shared/Helpers/FormattingHelper.cs b/LearningHub.Nhs.Shared/Helpers/FormattingHelper.cs new file mode 100644 index 00000000..1afb7b46 --- /dev/null +++ b/LearningHub.Nhs.Shared/Helpers/FormattingHelper.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LearningHub.Nhs.Shared.Helpers +{ + public static class FormattingHelper + { + + /// + /// Returns a number of milliseconds converted into a duration string, such as "10 min 15 sec". Includes rounding to match the behaviour of the Azure Media Player. + /// + /// The number of milliseconds. + /// The duration string. + public static string GetDurationText(int durationInMilliseconds) + { + if (durationInMilliseconds > 0) + { + // Azure media player rounds duration to nearest second. e.g. 8:59.88 becomes 9:00. LH needs to match. + int nearestSecond = (int)Math.Round(((double)durationInMilliseconds) / 1000); + var duration = new TimeSpan(0, 0, nearestSecond); + string returnValue = string.Empty; + + // If duration greater than an hour, don't return the seconds part. + if (duration.Hours > 0) + { + returnValue = $"{duration.Hours} hr {duration.Minutes} min "; + + // Exclude "0 min" from the return value. + if (returnValue.EndsWith(" 0 min ")) + { + returnValue = returnValue.Replace("0 min ", string.Empty); + } + } + else + { + returnValue = $"{duration.Minutes} min {duration.Seconds} sec "; + + // Exclude "0 min" and "0 sec" from the return value. + if (returnValue.StartsWith("0 min ")) + { + returnValue = returnValue.Replace("0 min ", string.Empty); + } + + if (returnValue.EndsWith(" 0 sec ")) + { + returnValue = returnValue.Replace("0 sec ", string.Empty); + } + } + + return returnValue; + } + else + { + return string.Empty; + } + } + } +} diff --git a/LearningHub.Nhs.Shared/Helpers/MoodleHelper.cs b/LearningHub.Nhs.Shared/Helpers/MoodleHelper.cs new file mode 100644 index 00000000..b8d5b3b5 --- /dev/null +++ b/LearningHub.Nhs.Shared/Helpers/MoodleHelper.cs @@ -0,0 +1,115 @@ +using LearningHub.Nhs.Models.Enums; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LearningHub.Nhs.Shared.Helpers +{ + public static class MoodleHelper + { + /// TODO: Remove this method after adding to Moodle resource types to models project. + /// + /// Returns a prettified resource type name, suitable for display in the UI. Includes video/audio duration string. + /// + /// The resource type. + /// The media duration in milliseconds. + /// The resource type name, and duration if applicable. + public static string GetPrettifiedResourceTypeNameMoodle(ResourceTypeEnum resourceType, int? durationInMilliseconds = 0) + { + switch (resourceType) + { + case ResourceTypeEnum.Assessment: + return "Assessment"; + case ResourceTypeEnum.Article: + return "Article"; + case ResourceTypeEnum.Audio: + string durationText = FormattingHelper.GetDurationText(durationInMilliseconds ?? 0); + durationText = string.IsNullOrEmpty(durationText) ? string.Empty : " - " + durationText; + return "Audio" + durationText; + case ResourceTypeEnum.Equipment: + return "Equipment"; + case ResourceTypeEnum.Image: + return "Image"; + case ResourceTypeEnum.Scorm: + return "elearning"; + case ResourceTypeEnum.Video: + durationText = FormattingHelper.GetDurationText(durationInMilliseconds ?? 0); + durationText = string.IsNullOrEmpty(durationText) ? string.Empty : " - " + durationText; + return "Video" + durationText; + case ResourceTypeEnum.WebLink: + return "Web link"; + case ResourceTypeEnum.GenericFile: + return "File"; + case ResourceTypeEnum.Embedded: + return "Embedded"; + case ResourceTypeEnum.Case: + return "Case"; + case ResourceTypeEnum.Html: + return "HTML"; + case ResourceTypeEnum.Moodle: + return "Course"; + default: + return "File"; + } + } + + /// TODO: Remove this method after adding to Moodle resource types to models project. + /// + /// Findwise Moodle resource type dictionary. + /// + public static readonly Dictionary FindwiseResourceMoodleTypeDict = new Dictionary() + { + { "video", ResourceTypeEnum.Video }, + { "article", ResourceTypeEnum.Article }, + { "case", ResourceTypeEnum.Case }, + { "weblink", ResourceTypeEnum.WebLink }, + { "audio", ResourceTypeEnum.Audio }, + { "scorm", ResourceTypeEnum.Scorm }, + { "assessment", ResourceTypeEnum.Assessment }, + { "genericfile", ResourceTypeEnum.GenericFile }, + { "image", ResourceTypeEnum.Image }, + { "html", ResourceTypeEnum.Html }, + { "moodle", ResourceTypeEnum.Moodle }, + }; + + /// + /// Returns a prettified resource type name, suitable for display in the UI. Excludes video/audio duration string. + /// + /// The resource type. + /// The resource type name, and duration if applicable. + public static string GetPrettifiedResourceTypeName(ResourceTypeEnum resourceType) + { + switch (resourceType) + { + case ResourceTypeEnum.Assessment: + return "Assessment"; + case ResourceTypeEnum.Article: + return "Article"; + case ResourceTypeEnum.Audio: + return "Audio"; + case ResourceTypeEnum.Equipment: + return "Equipment"; + case ResourceTypeEnum.Image: + return "Image"; + case ResourceTypeEnum.Scorm: + return "elearning"; + case ResourceTypeEnum.Video: + return "Video"; + case ResourceTypeEnum.WebLink: + return "Web link"; + case ResourceTypeEnum.GenericFile: + return "File"; + case ResourceTypeEnum.Embedded: + return "Embedded"; + case ResourceTypeEnum.Case: + return "Case"; + case ResourceTypeEnum.Html: + return "HTML"; + default: + return "File"; + } + } + } +} diff --git a/LearningHub.Nhs.Shared/Interfaces/Configuration/IFindwiseSettingsPublic.cs b/LearningHub.Nhs.Shared/Interfaces/Configuration/IExposableFindwiseSettings.cs similarity index 95% rename from LearningHub.Nhs.Shared/Interfaces/Configuration/IFindwiseSettingsPublic.cs rename to LearningHub.Nhs.Shared/Interfaces/Configuration/IExposableFindwiseSettings.cs index e63a9803..3c3a9691 100644 --- a/LearningHub.Nhs.Shared/Interfaces/Configuration/IFindwiseSettingsPublic.cs +++ b/LearningHub.Nhs.Shared/Interfaces/Configuration/IExposableFindwiseSettings.cs @@ -9,7 +9,7 @@ /// It does not contain any secure credentials or internal service configuration. /// /// - public interface IFindwiseSettingsPublic + public interface IExposableFindwiseSettings { /// /// Gets or sets the page size for resource search results. diff --git a/LearningHub.Nhs.Shared/Interfaces/Configuration/IPublicSettings.cs b/LearningHub.Nhs.Shared/Interfaces/Configuration/IExposableSettings.cs similarity index 63% rename from LearningHub.Nhs.Shared/Interfaces/Configuration/IPublicSettings.cs rename to LearningHub.Nhs.Shared/Interfaces/Configuration/IExposableSettings.cs index f81ef73a..753858fb 100644 --- a/LearningHub.Nhs.Shared/Interfaces/Configuration/IPublicSettings.cs +++ b/LearningHub.Nhs.Shared/Interfaces/Configuration/IExposableSettings.cs @@ -16,13 +16,27 @@ /// without risking exposure of sensitive information. /// /// - public interface IPublicSettings + public interface IExposableSettings { /// /// Gets or sets the LearningHubApiUrl. /// public string LearningHubApiUrl { get; set; } - public IFindwiseSettingsPublic FindwiseSettings { get; set; } + /// + /// Gets or sets the UserApiUrl. + /// + public string UserApiUrl { get; set; } + + /// + /// Gets or sets the OpenApiUrl. + /// + public string OpenApiUrl { get; set; } + /// + /// Gets or sets the LearningHubApiBFFUrl used to proxy via same domain cookie to the BFF LearningHubAPI calls. + /// + public string LearningHubApiBFFUrl { get; set; } + + public IExposableFindwiseSettings FindwiseSettings { get; set; } } } diff --git a/LearningHub.Nhs.Shared/LearningHub.Nhs.Shared.csproj b/LearningHub.Nhs.Shared/LearningHub.Nhs.Shared.csproj index 48ade5c7..11a4de7f 100644 --- a/LearningHub.Nhs.Shared/LearningHub.Nhs.Shared.csproj +++ b/LearningHub.Nhs.Shared/LearningHub.Nhs.Shared.csproj @@ -7,13 +7,15 @@ - - + + - - + + - + + + diff --git a/LearningHub.Nhs.Shared/Models/Search/SearchRequestViewModel.cs b/LearningHub.Nhs.Shared/Models/Search/SearchRequestViewModel.cs index a59b832f..f8b56cfd 100644 --- a/LearningHub.Nhs.Shared/Models/Search/SearchRequestViewModel.cs +++ b/LearningHub.Nhs.Shared/Models/Search/SearchRequestViewModel.cs @@ -2,7 +2,6 @@ { using System.Collections.Generic; using System.ComponentModel.DataAnnotations; - using Microsoft.AspNetCore.Mvc;//qqqq /// /// Defines the . @@ -13,67 +12,56 @@ public class SearchRequestViewModel /// Gets or sets the search string. /// [Required(ErrorMessage = "Search text is required")] - [FromQuery] public string Term { get; set; } /// /// Gets or sets the filters. /// - [FromQuery] public IEnumerable Filters { get; set; } /// /// Gets or sets the sort item index. /// - [FromQuery] public int? Sortby { get; set; } /// /// Gets or sets the catalogue current page index. /// - [FromQuery] public int? CataloguePageIndex { get; set; } /// /// Gets or sets the resource current page index. /// - [FromQuery] public int? ResourcePageIndex { get; set; } /// /// Gets or sets the group id. /// - [FromQuery] public string GroupId { get; set; } /// /// Gets or sets a value indicating whether gets or sets the feedback submitted. /// - [FromQuery] public bool? FeedbackSubmitted { get; set; } /// /// Gets or sets the search id. /// - [FromQuery] public int? SearchId { get; set; } /// /// Gets or sets the catalogue id, when searching within a particular catalogue. /// - [FromQuery] public int? CatalogueId { get; set; } /// /// Gets or sets the resource access level id. /// - [FromQuery] public int? ResourceAccessLevelId { get; set; } /// /// Gets or sets the provider ids. /// - [FromQuery] public IEnumerable ProviderFilters { get; set; } } } \ No newline at end of file diff --git a/LearningHub.Nhs.Shared/Services/SearchService.cs b/LearningHub.Nhs.Shared/Services/SearchService.cs index 8d82ee59..ab6dc87f 100644 --- a/LearningHub.Nhs.Shared/Services/SearchService.cs +++ b/LearningHub.Nhs.Shared/Services/SearchService.cs @@ -11,7 +11,6 @@ using LearningHub.Nhs.Shared.Interfaces.Http; using LearningHub.Nhs.Shared.Interfaces.Services; using LearningHub.Nhs.Shared.Models.Search; - using LearningHub.Nhs.WebUI.Helpers; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Newtonsoft.Json; @@ -30,7 +29,7 @@ /// public class SearchService : BaseService, ISearchService { - private readonly IPublicSettings publicSettings; + private readonly IExposableSettings ExposableSettings; private IProviderService providerService; /// @@ -39,15 +38,15 @@ public class SearchService : BaseService, ISearchService /// The Open Api Http Client. /// Provider service. /// Logger. - /// Settings. + /// Settings. public SearchService( IOpenApiHttpClient openApiHttpClient, IProviderService providerService, ILogger logger, - IOptions publicSettings) + IOptions ExposableSettings) : base(openApiHttpClient, logger) { - this.publicSettings = publicSettings.Value; + this.ExposableSettings = ExposableSettings.Value; this.providerService = providerService; } @@ -74,8 +73,8 @@ public async Task PerformSearch(IPrincipal user, SearchRe var suggestedCatalogue = string.Empty; var suggestedResource = string.Empty; - var resourceSearchPageSize = this.publicSettings.FindwiseSettings.ResourceSearchPageSize; - var catalogueSearchPageSize = this.publicSettings.FindwiseSettings.CatalogueSearchPageSize; + var resourceSearchPageSize = this.ExposableSettings.FindwiseSettings.ResourceSearchPageSize; + var catalogueSearchPageSize = this.ExposableSettings.FindwiseSettings.CatalogueSearchPageSize; var resourceSearchRequestModel = new SearchRequestModel { @@ -170,10 +169,10 @@ public async Task PerformSearch(IPrincipal user, SearchRe { var filter = filters.Where(x => x.DisplayName == filteritem).FirstOrDefault(); - if (filter != null && UtilityHelper.FindwiseResourceMoodleTypeDict.ContainsKey(filter.DisplayName)) + if (filter != null && MoodleHelper.FindwiseResourceMoodleTypeDict.ContainsKey(filter.DisplayName)) { - var resourceTypeEnum = UtilityHelper.FindwiseResourceMoodleTypeDict[filter.DisplayName]; - var searchfilter = new SearchFilterModel() { DisplayName = UtilityHelper.GetPrettifiedResourceTypeNameMoodle(resourceTypeEnum), Count = filter.Count, Value = filteritem, Selected = searchRequest.Filters?.Contains(filter.DisplayName) ?? false }; + var resourceTypeEnum = MoodleHelper.FindwiseResourceMoodleTypeDict[filter.DisplayName]; + var searchfilter = new SearchFilterModel() { DisplayName = MoodleHelper.GetPrettifiedResourceTypeNameMoodle(resourceTypeEnum), Count = filter.Count, Value = filteritem, Selected = searchRequest.Filters?.Contains(filter.DisplayName) ?? false }; searchfilters.Add(searchfilter); } } @@ -260,8 +259,8 @@ public async Task PerformSearch(IPrincipal user, SearchRe public async Task RegisterSearchEventsAsync(SearchRequestViewModel search, SearchFormActionTypeEnum action, int resourceCount = 0, int catalogueCount = 0) { var eventId = 0; - var resourceSearchPageSize = this.publicSettings.FindwiseSettings.ResourceSearchPageSize; - var catalogueSearchPageSize = this.publicSettings.FindwiseSettings.CatalogueSearchPageSize; + var resourceSearchPageSize = this.ExposableSettings.FindwiseSettings.ResourceSearchPageSize; + var catalogueSearchPageSize = this.ExposableSettings.FindwiseSettings.CatalogueSearchPageSize; var sortBy = search.Sortby.HasValue ? (SearchSortTypeEnum)search.Sortby : SearchSortTypeEnum.Relevance; @@ -518,7 +517,7 @@ public async Task SubmitFeedbackAsync(SearchFeedBackModel model) int createId = 0; var client = await this.OpenApiHttpClient.GetClientAsync(); - var request = this.publicSettings.LearningHubApiUrl + "Search/SubmitFeedback"; + var request = this.ExposableSettings.LearningHubApiUrl + "Search/SubmitFeedback"; var content = new StringContent(JsonConvert.SerializeObject(model), Encoding.UTF8, "application/json"); var response = await client.PostAsync(request, content).ConfigureAwait(false); diff --git a/LearningHub.Nhs.WebUI.BlazorClient/DI/DI.cs b/LearningHub.Nhs.WebUI.BlazorClient/DI/DI.cs new file mode 100644 index 00000000..3be1a1cb --- /dev/null +++ b/LearningHub.Nhs.WebUI.BlazorClient/DI/DI.cs @@ -0,0 +1,30 @@ +using Microsoft.Extensions.Options; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Collections.Generic; +using LearningHub.Nhs.Shared.Configuration; + + +namespace LearningHub.Nhs.WebUI.BlazorClient.DI +{ + public static class DI + { + public static IHttpClientBuilder AddBffHttpClient(this IServiceCollection services, Func getApiUrl) + where TInterface : class + where TImplementation : class, TInterface + { + return services.AddHttpClient((serviceProvider, client) => + { + var ExposableSettings = serviceProvider.GetRequiredService>().Value; + var apiUrl = getApiUrl(ExposableSettings); + var apiUri = new Uri(apiUrl); + var apiHost = apiUri.Host; + string forwardSlash = "/"; + // Using the Uri class for robust path joining + client.BaseAddress = new Uri($"{ExposableSettings.LearningHubApiBFFUrl}{apiHost}{forwardSlash}"); + }); + } + } +} diff --git a/LearningHub.Nhs.WebUI.BlazorClient/LearningHub.Nhs.WebUI.BlazorClient.csproj b/LearningHub.Nhs.WebUI.BlazorClient/LearningHub.Nhs.WebUI.BlazorClient.csproj new file mode 100644 index 00000000..2082f00b --- /dev/null +++ b/LearningHub.Nhs.WebUI.BlazorClient/LearningHub.Nhs.WebUI.BlazorClient.csproj @@ -0,0 +1,56 @@ + + + + net8.0 + enable + enable + + + + true + full + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/LearningHub.Nhs.WebUI.BlazorClient/LearningHub.Nhs.WebUI.BlazorClient.csproj.user b/LearningHub.Nhs.WebUI.BlazorClient/LearningHub.Nhs.WebUI.BlazorClient.csproj.user new file mode 100644 index 00000000..877a5e48 --- /dev/null +++ b/LearningHub.Nhs.WebUI.BlazorClient/LearningHub.Nhs.WebUI.BlazorClient.csproj.user @@ -0,0 +1,9 @@ + + + + IIS Local + + + ProjectDebugger + + \ No newline at end of file diff --git a/LearningHub.Nhs.WebUI.BlazorClient/Program.cs b/LearningHub.Nhs.WebUI.BlazorClient/Program.cs new file mode 100644 index 00000000..d9393629 --- /dev/null +++ b/LearningHub.Nhs.WebUI.BlazorClient/Program.cs @@ -0,0 +1,111 @@ +//using LearningHub.Nhs.Models.Entities; +// Still required server side even if not used so components dont fail +using Blazored.LocalStorage; +using LearningHub.Nhs.WebUI.BlazorClient.DI; + +using LearningHub.Nhs.Caching; +using LearningHub.Nhs.Shared.Configuration; +using LearningHub.Nhs.Shared.Interfaces; +using LearningHub.Nhs.Shared.Interfaces.Configuration; +using LearningHub.Nhs.Shared.Interfaces.Http; +using LearningHub.Nhs.Shared.Interfaces.Services; +using LearningHub.Nhs.Shared.Services; +using LearningHub.Nhs.WebUI.BlazorClient.Services; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Web; +using Microsoft.AspNetCore.Components.WebAssembly.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Http; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +// Serilog core (used via appsettings, do not delete even if vs marks not in use) +using Serilog; +using Serilog.Configuration; +using Serilog.Core; +using Serilog.Events; +// Serilog extensions and sinks (used via appsettings, do not delete even if vs marks not in use) +using Serilog.Extensions.Logging; +using Serilog.Formatting.Compact; +using Serilog.Settings.Configuration; +using Serilog.Sinks.BrowserConsole; +using System; +using TELBlazor.Components.Core.Configuration; +using TELBlazor.Components.Core.Services.HelperServices; +using TELBlazor.Components.OptionalImplementations.Core.Services.HelperServices; + + +var builder = WebAssemblyHostBuilder.CreateDefault(args); + +var http = new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }; +var env = builder.HostEnvironment.Environment; +using var envSettings = await http.GetStreamAsync($"appsettings.{env}.json"); + +builder.Configuration.AddJsonStream(envSettings); + +builder.Services.Configure(builder.Configuration.GetSection("Settings")); +builder.Logging.ClearProviders(); + +// Read default logging level from configuration +var logLevelString = builder.Configuration["Serilog:MinimumLevel:Default"]; +// Convert string to LogEventLevel (with fallback) +if (!Enum.TryParse(logLevelString, true, out LogEventLevel defaultLogLevel)) +{ + defaultLogLevel = LogEventLevel.Information; // Default if parsing fails +} + +// Create a LoggingLevelSwitch that can be updated dynamically +LoggingLevelSwitch levelSwitch = new LoggingLevelSwitch(defaultLogLevel); // Default: Information added this so in production can change the logging +Log.Logger = new LoggerConfiguration() + .ReadFrom.Configuration(builder.Configuration) + .MinimumLevel.ControlledBy(levelSwitch) + .CreateLogger(); + +// Add Serilog to logging providers +builder.Logging.AddSerilog(Log.Logger, dispose: true);//qqqq may not need dispose for client + +//for really bad fails +try +{ + // Candidates for DI collection + builder.Services.AddSingleton(sp => + { + return new TELBlazorBaseComponentConfiguration + { + JSEnabled = true, //if we are inject the client then it is true + HostType = $"{builder.Configuration["Properties:Environment"]} {builder.Configuration["Properties:Application"]}" + }; + }); + + builder.Services.AddBlazoredLocalStorage(); + + + // Register your BFF using httpclient ILearningHubHttpClient + builder.Services.AddBffHttpClient(settings => settings.LearningHubApiUrl); + + // Register your BFF using httpclient IUserApiHttpClient + builder.Services.AddBffHttpClient(settings => settings.UserApiUrl); + + // Register your BFF using httpclient IOpenApiHttpClient + builder.Services.AddBffHttpClient(settings => settings.OpenApiUrl); + + + builder.Services.AddScoped(sp => levelSwitch); + builder.Services.AddScoped(); + + builder.Services.AddScoped(); + + // qqqq will go in a shared DI service collection extension + builder.Services.AddScoped(); + builder.Services.AddScoped(); + + await builder.Build().RunAsync(); +} +catch (Exception ex) +{ + //If in production as requires sending to api we may never receive it + Log.Fatal(ex, "Application terminated unexpectedly"); +} +finally +{ + Log.CloseAndFlush(); // Ensure logs are flushed before exit +} \ No newline at end of file diff --git a/LearningHub.Nhs.WebUI.BlazorClient/Services/GenericAPIHttpClient.cs b/LearningHub.Nhs.WebUI.BlazorClient/Services/GenericAPIHttpClient.cs new file mode 100644 index 00000000..f08a7c09 --- /dev/null +++ b/LearningHub.Nhs.WebUI.BlazorClient/Services/GenericAPIHttpClient.cs @@ -0,0 +1,30 @@ +using LearningHub.Nhs.Shared.Interfaces.Http; + +namespace LearningHub.Nhs.WebUI.BlazorClient.Services +{ + public class GenericAPIHttpClient : IAPIHttpClient, ILearningHubHttpClient, IUserApiHttpClient, IOpenApiHttpClient + { + private readonly HttpClient _httpClient; // Private field to hold the injected HttpClient + + /// + /// Initializes a new instance of the class. + /// + /// The HttpClient instance provided by dependency injection. + public GenericAPIHttpClient(HttpClient httpClient) // Inject HttpClient + { + _httpClient = httpClient; + } + + public string ApiUrl => _httpClient.BaseAddress.AbsoluteUri; + + /// + /// Retrieves the configured HttpClient instance. + /// + /// A Task that resolves to the HttpClient instance. + public Task GetClientAsync() + { + // Return the injected HttpClient instance wrapped in a completed Task + return Task.FromResult(_httpClient); + } + } +} diff --git a/LearningHub.Nhs.WebUI.BlazorClient/Services/WasmCacheServiceStub.cs b/LearningHub.Nhs.WebUI.BlazorClient/Services/WasmCacheServiceStub.cs new file mode 100644 index 00000000..3640c206 --- /dev/null +++ b/LearningHub.Nhs.WebUI.BlazorClient/Services/WasmCacheServiceStub.cs @@ -0,0 +1,62 @@ +using LearningHub.Nhs.Caching; + +namespace LearningHub.Nhs.WebUI.BlazorClient.Services +{ + /// + /// We may use storage, we may just stub it and throw an error, we cant directly use redis we may access it via an api + /// The cachestub currently just returns there is nothing available so the caller then will revert to calling the api. + /// + public class WasmCacheServiceStub : ICacheService + { + public Task GetAsync(string key) + { + return Task.FromResult(default(T)); + } + + public Task<(bool Success, T Value)> TryGetAsync(string key) + { + return Task.FromResult((false, default(T))); + } + + public Task SetAsync(string key, T value) + { + return Task.FromResult(value); + } + + public Task RemoveAsync(string key) + { + return Task.CompletedTask; + } + + public Task SetAsync(string key, T value, int? expiryInMinutes, bool slidingExpiration = true) + { + return Task.FromResult(value); + } + + public Task GetOrCreateAsync(string key, Func getValue) + { + return Task.FromResult(getValue()); + } + + public Task GetOrCreateAsync(string key, Func getValue, int? expiryInMinutes, bool slidingExpiration = true) + { + return Task.FromResult(getValue()); + } + + public Task GetOrFetchAsync(string key, Func> getValue) + { + return getValue(); + } + + public Task GetOrFetchAsync(string key, Func> getValue, int? expiryInMinutes, bool slidingExpiration = true) + { + return getValue(); + } + + public Task FlushAll() + { + return Task.CompletedTask; + } + + } +} diff --git a/LearningHub.Nhs.WebUI.BlazorClient/TestDeleteMe/APITestDeleteME.razor b/LearningHub.Nhs.WebUI.BlazorClient/TestDeleteMe/APITestDeleteME.razor new file mode 100644 index 00000000..39590e7e --- /dev/null +++ b/LearningHub.Nhs.WebUI.BlazorClient/TestDeleteMe/APITestDeleteME.razor @@ -0,0 +1,66 @@ +@using LearningHub.Nhs.Shared.Interfaces.Http +@inject ILearningHubHttpClient LearningHubBFFHttpClient +@inherits TELBlazor.Components.Core.TELComponentBase +
+

⚠️ Works when required service packages moved to BlazorClient currently will fail on call, happens due to the runtime hack AllowUsingAspNetCoreInBlazorWasm ⚠️

+ + +
+
+ Response from custom endpoint: +
@APIResponse
+
+ +@code { + string ApiRoute = "Catalogue/GetLatestCatalogueAccessRequest/43"; // Default route 500 + string? APIResponse = "No response yet"; + + + protected override void OnInitialized() + { + base.OnInitialized(); // Call the base method + Logger.LogInformation("Serilogging"); + + + Logger.LogInformation( + "API Test Component initialized with default route: {Route}", + ApiRoute + ); + + Logger.LogInformation("!!!!!!!!!!!!!!!!!!!!!!!!! Base Component"); + } + + private async Task CallApi() + { + try + { + Logger.LogInformation("Base Component"); + var httpClient = await LearningHubBFFHttpClient.GetClientAsync(); + + Logger.LogInformation("📡 BaseAddress should be https://lh-web.dev.local/bff/lh-api.dev.local/ . HttpClient BaseAddress: {BaseAddress}", httpClient.BaseAddress?.ToString()); + Logger.LogInformation($"Logger: Calling via the bff this api route: {ApiRoute}"); + Logger.LogInformation("📡 Target: {TargetUrl}", "https://lh-web.dev.local/bff/lh-api.dev.local/Catalogue/GetLatestCatalogueAccessRequest/500"); + // Assuming httpClient.BaseAddress is set + string fullUrl = new Uri(httpClient.BaseAddress, ApiRoute).ToString(); + Logger.LogInformation("Making request to: {FullUrl}", fullUrl); + + // Now use standard HttpClient methods + var response = await httpClient.GetAsync(ApiRoute); + string responseContent = await response.Content.ReadAsStringAsync(); + + Logger.LogInformation("API Response - Status: {Status}, Content: {Content}", + response.StatusCode, responseContent); + + var content = await response.Content.ReadAsStringAsync(); + APIResponse = $"**Status:** {response.StatusCode}\n\n{content}"; + + /* qqqq + * // -> think need // https://lh-web.dev.local/bff/lh-api.dev.local/Catalogue/GetLatestCatalogueAccessRequest/500 + */ + } + catch (Exception ex) + { + Logger.LogError(ex, "An error occurred during API call to route: {Route}", ApiRoute); + } + } +} \ No newline at end of file diff --git a/LearningHub.Nhs.WebUI.BlazorClient/_Imports.razor b/LearningHub.Nhs.WebUI.BlazorClient/_Imports.razor new file mode 100644 index 00000000..1289a8af --- /dev/null +++ b/LearningHub.Nhs.WebUI.BlazorClient/_Imports.razor @@ -0,0 +1,4 @@ +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using static Microsoft.AspNetCore.Components.Web.RenderMode +@using LearningHub.Nhs.WebUI.BlazorClient \ No newline at end of file diff --git a/LearningHub.Nhs.WebUI.BlazorClient/nuget.config.template b/LearningHub.Nhs.WebUI.BlazorClient/nuget.config.template new file mode 100644 index 00000000..dac49600 --- /dev/null +++ b/LearningHub.Nhs.WebUI.BlazorClient/nuget.config.template @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/LearningHub.Nhs.WebUI.Shared/LearningHub.Nhs.WebUI.Shared.csproj b/LearningHub.Nhs.WebUI.Shared/LearningHub.Nhs.WebUI.Shared.csproj new file mode 100644 index 00000000..fa71b7ae --- /dev/null +++ b/LearningHub.Nhs.WebUI.Shared/LearningHub.Nhs.WebUI.Shared.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git a/LearningHub.Nhs.WebUI.sln b/LearningHub.Nhs.WebUI.sln index c8809438..24242191 100644 --- a/LearningHub.Nhs.WebUI.sln +++ b/LearningHub.Nhs.WebUI.sln @@ -93,6 +93,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LearningHub.Nhs.WebUI.Autom EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LearningHub.Nhs.Shared", "LearningHub.Nhs.Shared\LearningHub.Nhs.Shared.csproj", "{9F1B0470-E809-49FE-A6E8-152C7EBD012E}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LearningHub.Nhs.WebUI.BlazorClient", "LearningHub.Nhs.WebUI.BlazorClient\LearningHub.Nhs.WebUI.BlazorClient.csproj", "{A7DA82FE-A46C-47E9-8BD6-7FD7A8376CBA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LearningHub.Nhs.WebUI.Shared", "LearningHub.Nhs.WebUI.Shared\LearningHub.Nhs.WebUI.Shared.csproj", "{22596C8C-EF3A-4046-BB85-012D851D8D16}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -365,6 +369,22 @@ Global {9F1B0470-E809-49FE-A6E8-152C7EBD012E}.Release|Any CPU.Build.0 = Release|Any CPU {9F1B0470-E809-49FE-A6E8-152C7EBD012E}.Release|x64.ActiveCfg = Release|Any CPU {9F1B0470-E809-49FE-A6E8-152C7EBD012E}.Release|x64.Build.0 = Release|Any CPU + {A7DA82FE-A46C-47E9-8BD6-7FD7A8376CBA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A7DA82FE-A46C-47E9-8BD6-7FD7A8376CBA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A7DA82FE-A46C-47E9-8BD6-7FD7A8376CBA}.Debug|x64.ActiveCfg = Debug|Any CPU + {A7DA82FE-A46C-47E9-8BD6-7FD7A8376CBA}.Debug|x64.Build.0 = Debug|Any CPU + {A7DA82FE-A46C-47E9-8BD6-7FD7A8376CBA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A7DA82FE-A46C-47E9-8BD6-7FD7A8376CBA}.Release|Any CPU.Build.0 = Release|Any CPU + {A7DA82FE-A46C-47E9-8BD6-7FD7A8376CBA}.Release|x64.ActiveCfg = Release|Any CPU + {A7DA82FE-A46C-47E9-8BD6-7FD7A8376CBA}.Release|x64.Build.0 = Release|Any CPU + {22596C8C-EF3A-4046-BB85-012D851D8D16}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {22596C8C-EF3A-4046-BB85-012D851D8D16}.Debug|Any CPU.Build.0 = Debug|Any CPU + {22596C8C-EF3A-4046-BB85-012D851D8D16}.Debug|x64.ActiveCfg = Debug|Any CPU + {22596C8C-EF3A-4046-BB85-012D851D8D16}.Debug|x64.Build.0 = Debug|Any CPU + {22596C8C-EF3A-4046-BB85-012D851D8D16}.Release|Any CPU.ActiveCfg = Release|Any CPU + {22596C8C-EF3A-4046-BB85-012D851D8D16}.Release|Any CPU.Build.0 = Release|Any CPU + {22596C8C-EF3A-4046-BB85-012D851D8D16}.Release|x64.ActiveCfg = Release|Any CPU + {22596C8C-EF3A-4046-BB85-012D851D8D16}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/LearningHub.Nhs.WebUI/BlazorPageHosting/App.razor b/LearningHub.Nhs.WebUI/BlazorPageHosting/App.razor new file mode 100644 index 00000000..884020c9 --- /dev/null +++ b/LearningHub.Nhs.WebUI/BlazorPageHosting/App.razor @@ -0,0 +1 @@ +@* No-op App component *@ diff --git a/LearningHub.Nhs.WebUI/Configuration/Settings.cs b/LearningHub.Nhs.WebUI/Configuration/Settings.cs index 30f427cb..b8a26f73 100644 --- a/LearningHub.Nhs.WebUI/Configuration/Settings.cs +++ b/LearningHub.Nhs.WebUI/Configuration/Settings.cs @@ -2,12 +2,13 @@ namespace LearningHub.Nhs.WebUI.Configuration { using System; using LearningHub.Nhs.Shared.Configuration; + using LearningHub.Nhs.Shared.Interfaces.Configuration; using LearningHub.Nhs.WebUI.Models.Contribute; /// /// Defines the . /// - public class Settings + public class Settings : IExposableSettings { /// /// Initializes a new instance of the class. @@ -32,6 +33,9 @@ public Settings() /// public string LearningHubApiUrl { get; set; } + /// + public string LearningHubApiBFFUrl { get; set; } + /// /// Gets or sets the OpenApiUrl. /// @@ -255,7 +259,7 @@ public Settings() /// /// Gets or sets the FindwiseSettings. /// - public FindwiseSettingsPublic FindwiseSettings { get; set; } = new FindwiseSettingsPublic(); + public IExposableFindwiseSettings FindwiseSettings { get; set; } = new ExposableFindwiseSettings(); /// /// Gets or sets the MediaKindSettings. diff --git a/LearningHub.Nhs.WebUI/Controllers/Api/BFFController.cs b/LearningHub.Nhs.WebUI/Controllers/Api/BFFController.cs index c5f082d7..ef3cf02d 100644 --- a/LearningHub.Nhs.WebUI/Controllers/Api/BFFController.cs +++ b/LearningHub.Nhs.WebUI/Controllers/Api/BFFController.cs @@ -78,6 +78,7 @@ public async Task ProxyRequest(string apiName, string path) string sanitizedPath = path?.Trim('/').ToLowerInvariant() ?? string.Empty; string sanitizedApiName = apiName?.Trim('/').ToLowerInvariant() ?? string.Empty; + // qqqq https://lh-web.dev.local/bff/lh-api.dev.local/Catalogue/GetLatestCatalogueAccessRequest/500 IAPIHttpClient apiClient; try { @@ -102,6 +103,7 @@ public async Task ProxyRequest(string apiName, string path) if (!this.IsPathAllowed(sanitizedPath)) { + // qqqq "catalogue/getlatestcatalogueaccessrequest/500" return this.Forbid("This path is not allowed via BFF proxy."); } diff --git a/LearningHub.Nhs.WebUI/Controllers/SearchController.cs b/LearningHub.Nhs.WebUI/Controllers/SearchController.cs index cada3e01..1438a7ba 100644 --- a/LearningHub.Nhs.WebUI/Controllers/SearchController.cs +++ b/LearningHub.Nhs.WebUI/Controllers/SearchController.cs @@ -62,7 +62,7 @@ public SearchController( /// filter applied. /// The actionResult. [HttpGet("results")] - public async Task Index(SearchRequestViewModel search, bool noSortFilterError = false, bool emptyFeedbackError = false, bool filterApplied = false) + public async Task Index([FromQuery] SearchRequestViewModel search, bool noSortFilterError = false, bool emptyFeedbackError = false, bool filterApplied = false) { search.SearchId ??= 0; search.GroupId = !string.IsNullOrWhiteSpace(search.GroupId) && Guid.TryParse(search.GroupId, out Guid groupId) ? groupId.ToString() : Guid.NewGuid().ToString(); diff --git a/LearningHub.Nhs.Shared/Helpers/UtilityHelper.cs b/LearningHub.Nhs.WebUI/Helpers/UtilityHelper.cs similarity index 68% rename from LearningHub.Nhs.Shared/Helpers/UtilityHelper.cs rename to LearningHub.Nhs.WebUI/Helpers/UtilityHelper.cs index bdb41972..515786d0 100644 --- a/LearningHub.Nhs.Shared/Helpers/UtilityHelper.cs +++ b/LearningHub.Nhs.WebUI/Helpers/UtilityHelper.cs @@ -33,25 +33,6 @@ public static class UtilityHelper { "html", ResourceTypeEnum.Html }, }; - /// TODO: Remove this method after adding to Moodle resource types to models project. - /// - /// Findwise Moodle resource type dictionary. - /// - public static readonly Dictionary FindwiseResourceMoodleTypeDict = new Dictionary() - { - { "video", ResourceTypeEnum.Video }, - { "article", ResourceTypeEnum.Article }, - { "case", ResourceTypeEnum.Case }, - { "weblink", ResourceTypeEnum.WebLink }, - { "audio", ResourceTypeEnum.Audio }, - { "scorm", ResourceTypeEnum.Scorm }, - { "assessment", ResourceTypeEnum.Assessment }, - { "genericfile", ResourceTypeEnum.GenericFile }, - { "image", ResourceTypeEnum.Image }, - { "html", ResourceTypeEnum.Html }, - { "moodle", ResourceTypeEnum.Moodle }, - }; - /// /// The FormatTwitterDate. /// @@ -139,51 +120,7 @@ public static string GetPrettifiedResourceTypeName(ResourceTypeEnum resourceType case ResourceTypeEnum.Article: return "Article"; case ResourceTypeEnum.Audio: - string durationText = GetDurationText(durationInMilliseconds ?? 0); - durationText = string.IsNullOrEmpty(durationText) ? string.Empty : " - " + durationText; - return "Audio" + durationText; - case ResourceTypeEnum.Equipment: - return "Equipment"; - case ResourceTypeEnum.Image: - return "Image"; - case ResourceTypeEnum.Scorm: - return "elearning"; - case ResourceTypeEnum.Video: - durationText = GetDurationText(durationInMilliseconds ?? 0); - durationText = string.IsNullOrEmpty(durationText) ? string.Empty : " - " + durationText; - return "Video" + durationText; - case ResourceTypeEnum.WebLink: - return "Web link"; - case ResourceTypeEnum.GenericFile: - return "File"; - case ResourceTypeEnum.Embedded: - return "Embedded"; - case ResourceTypeEnum.Case: - return "Case"; - case ResourceTypeEnum.Html: - return "HTML"; - default: - return "File"; - } - } - - /// TODO: Remove this method after adding to Moodle resource types to models project. - /// - /// Returns a prettified resource type name, suitable for display in the UI. Includes video/audio duration string. - /// - /// The resource type. - /// The media duration in milliseconds. - /// The resource type name, and duration if applicable. - public static string GetPrettifiedResourceTypeNameMoodle(ResourceTypeEnum resourceType, int? durationInMilliseconds = 0) - { - switch (resourceType) - { - case ResourceTypeEnum.Assessment: - return "Assessment"; - case ResourceTypeEnum.Article: - return "Article"; - case ResourceTypeEnum.Audio: - string durationText = GetDurationText(durationInMilliseconds ?? 0); + string durationText = FormattingHelper.GetDurationText(durationInMilliseconds ?? 0); durationText = string.IsNullOrEmpty(durationText) ? string.Empty : " - " + durationText; return "Audio" + durationText; case ResourceTypeEnum.Equipment: @@ -193,7 +130,7 @@ public static string GetPrettifiedResourceTypeNameMoodle(ResourceTypeEnum resour case ResourceTypeEnum.Scorm: return "elearning"; case ResourceTypeEnum.Video: - durationText = GetDurationText(durationInMilliseconds ?? 0); + durationText = FormattingHelper.GetDurationText(durationInMilliseconds ?? 0); durationText = string.IsNullOrEmpty(durationText) ? string.Empty : " - " + durationText; return "Video" + durationText; case ResourceTypeEnum.WebLink: @@ -206,100 +143,11 @@ public static string GetPrettifiedResourceTypeNameMoodle(ResourceTypeEnum resour return "Case"; case ResourceTypeEnum.Html: return "HTML"; - case ResourceTypeEnum.Moodle: - return "Course"; default: return "File"; } } - /// - /// Returns a prettified resource type name, suitable for display in the UI. Excludes video/audio duration string. - /// - /// The resource type. - /// The resource type name, and duration if applicable. - public static string GetPrettifiedResourceTypeName(ResourceTypeEnum resourceType) - { - switch (resourceType) - { - case ResourceTypeEnum.Assessment: - return "Assessment"; - case ResourceTypeEnum.Article: - return "Article"; - case ResourceTypeEnum.Audio: - return "Audio"; - case ResourceTypeEnum.Equipment: - return "Equipment"; - case ResourceTypeEnum.Image: - return "Image"; - case ResourceTypeEnum.Scorm: - return "elearning"; - case ResourceTypeEnum.Video: - return "Video"; - case ResourceTypeEnum.WebLink: - return "Web link"; - case ResourceTypeEnum.GenericFile: - return "File"; - case ResourceTypeEnum.Embedded: - return "Embedded"; - case ResourceTypeEnum.Case: - return "Case"; - case ResourceTypeEnum.Html: - return "HTML"; - default: - return "File"; - } - } - - /// - /// Returns a number of milliseconds converted into a duration string, such as "10 min 15 sec". Includes rounding to match the behaviour of the Azure Media Player. - /// - /// The number of milliseconds. - /// The duration string. - public static string GetDurationText(int durationInMilliseconds) - { - if (durationInMilliseconds > 0) - { - // Azure media player rounds duration to nearest second. e.g. 8:59.88 becomes 9:00. LH needs to match. - int nearestSecond = (int)Math.Round(((double)durationInMilliseconds) / 1000); - var duration = new TimeSpan(0, 0, nearestSecond); - string returnValue = string.Empty; - - // If duration greater than an hour, don't return the seconds part. - if (duration.Hours > 0) - { - returnValue = $"{duration.Hours} hr {duration.Minutes} min "; - - // Exclude "0 min" from the return value. - if (returnValue.EndsWith(" 0 min ")) - { - returnValue = returnValue.Replace("0 min ", string.Empty); - } - } - else - { - returnValue = $"{duration.Minutes} min {duration.Seconds} sec "; - - // Exclude "0 min" and "0 sec" from the return value. - if (returnValue.StartsWith("0 min ")) - { - returnValue = returnValue.Replace("0 min ", string.Empty); - } - - if (returnValue.EndsWith(" 0 sec ")) - { - returnValue = returnValue.Replace("0 sec ", string.Empty); - } - } - - return returnValue; - } - else - { - return string.Empty; - } - } - /// /// Returns a string containing either the authoredBy or organisation string, or a combination of both if present. /// diff --git a/LearningHub.Nhs.WebUI/LearningHub.Nhs.WebUI.csproj b/LearningHub.Nhs.WebUI/LearningHub.Nhs.WebUI.csproj index caaf79c0..ed93048b 100644 --- a/LearningHub.Nhs.WebUI/LearningHub.Nhs.WebUI.csproj +++ b/LearningHub.Nhs.WebUI/LearningHub.Nhs.WebUI.csproj @@ -97,6 +97,9 @@ + + + @@ -104,6 +107,10 @@ + + + + @@ -112,7 +119,7 @@ - + @@ -169,6 +176,8 @@ + + diff --git a/LearningHub.Nhs.WebUI/Program.cs b/LearningHub.Nhs.WebUI/Program.cs index c24d9057..34013d03 100644 --- a/LearningHub.Nhs.WebUI/Program.cs +++ b/LearningHub.Nhs.WebUI/Program.cs @@ -2,6 +2,7 @@ using System; using System.Diagnostics; using LearningHub.Nhs.WebUI; +using LearningHub.Nhs.WebUI.BlazorPageHosting; using LearningHub.Nhs.WebUI.Interfaces; using LearningHub.Nhs.WebUI.JsDetection; using LearningHub.Nhs.WebUI.Middleware; @@ -18,7 +19,6 @@ using tusdotnet; using tusdotnet.Models; using tusdotnet.Models.Configuration; - #pragma warning restore SA1200 // Using directives should be placed correctly var logger = NLogBuilder.ConfigureNLog("nlog.config").GetCurrentClassLogger(); @@ -43,10 +43,13 @@ var appLifetime = app.Services.GetRequiredService(); var jsDetectionLogger = app.Services.GetRequiredService(); appLifetime.ApplicationStopping.Register(async () => await jsDetectionLogger.FlushCounters()); + app.UseBlazorFrameworkFiles(); + app.UseStaticFiles(); if (app.Environment.IsDevelopment()) { app.UseDeveloperExceptionPage(); + app.UseWebAssemblyDebugging(); } else { @@ -84,7 +87,6 @@ app.UseAuthorization(); app.UseMiddleware(); - app.UseStaticFiles(); app.Map(TimezoneInfoMiddleware.TimezoneInfoUrl, b => b.UseMiddleware()); @@ -108,6 +110,12 @@ }; }); + app.MapRazorComponents() + .AddInteractiveServerRenderMode() + .AddInteractiveWebAssemblyRenderMode() + .AddAdditionalAssemblies(typeof(LearningHub.Nhs.WebUI.BlazorClient._Imports).Assembly) + .AddAdditionalAssemblies(typeof(TELBlazor.Components._Imports).Assembly); + app.Run(); } catch (Exception ex) diff --git a/LearningHub.Nhs.WebUI/Services/NLogLogLevelSwitcherService.cs b/LearningHub.Nhs.WebUI/Services/NLogLogLevelSwitcherService.cs new file mode 100644 index 00000000..bae626eb --- /dev/null +++ b/LearningHub.Nhs.WebUI/Services/NLogLogLevelSwitcherService.cs @@ -0,0 +1,97 @@ +namespace LearningHub.Nhs.WebUI.Services +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Threading.Tasks; + using Blazored.LocalStorage; + using Microsoft.Extensions.Logging; + using TELBlazor.Components.Core.Models.Logging; + using TELBlazor.Components.Core.Services.HelperServices; + + /// + /// Provides functionality for managing log levels in applications using NLog. + /// + /// This service implements the interface to provide log + /// level management. Note that NLog does not support runtime log level switching in WASM environments. As a result, + /// methods in this service primarily serve to fulfill the interface contract and provide default + /// behaviors. + public class NLogLogLevelSwitcherService : ILogLevelSwitcherService + { + private const string LogLevelKey = "logLevel"; + private readonly ILogger logger; + private readonly ILocalStorageService localStorage; + + /// + /// Initializes a new instance of the class. + /// + /// The local storage service. + /// The logger instance. + public NLogLogLevelSwitcherService(ILocalStorageService localStorage, ILogger logger) + { + this.localStorage = localStorage; + this.logger = logger; + } + + /// + public bool IsInitialized { get; set; } = false; + + /// + public async Task InitializeLogLevelFromAsyncSourceIfAvailable() + { + // NLog does not support runtime level switching in WASM + // Here only to mirror the interface + this.logger.LogInformation("NLog does not support dynamic runtime log level switching."); + await Task.CompletedTask; + } + + /// + public List GetAvailableLogLevels() => + Enum.GetNames(typeof(LogLevel)).ToList(); + + /// + public string GetCurrentLogLevel() + { + this.logger.LogInformation("Returning default log level (NLog does not support querying runtime level)."); + return "Information"; + } + + /// + public string SetLogLevel(string level) + { + this.logger.LogInformation("Requested to change log level to {Level}, but NLog does not support runtime changes in WASM.", level); + this.LogAllLevels("After 'Change'"); + + _ = this.StoreLogLevelWithTimestamp(level); // Fire and forget + return this.GetCurrentLogLevel(); + } + + private void LogAllLevels(string phase) + { + this.logger.LogTrace("[{Phase}] TRACE log", phase); + this.logger.LogDebug("[{Phase}] DEBUG log", phase); + this.logger.LogInformation("[{Phase}] INFO log", phase); + this.logger.LogWarning("[{Phase}] WARN log", phase); + this.logger.LogError("[{Phase}] ERROR log", phase); + this.logger.LogCritical("[{Phase}] CRITICAL log", phase); + } + + private async Task StoreLogLevelWithTimestamp(string level) + { + try + { + var newItem = new LocalStorageLogLevel + { + Level = level, + Expires = DateTime.UtcNow.AddHours(24), + }; + + await this.localStorage.SetItemAsync(LogLevelKey, newItem); + } + catch (Exception ex) + { + this.logger.LogError(ex, "Error storing log level to local storage."); + } + } + } +} diff --git a/LearningHub.Nhs.WebUI/Startup/ServiceMappings.cs b/LearningHub.Nhs.WebUI/Startup/ServiceMappings.cs index 50d52eaa..7e016825 100644 --- a/LearningHub.Nhs.WebUI/Startup/ServiceMappings.cs +++ b/LearningHub.Nhs.WebUI/Startup/ServiceMappings.cs @@ -1,6 +1,8 @@ namespace LearningHub.Nhs.WebUI.Startup { + using System; using System.Net.Http; + using Blazored.LocalStorage; using GDS.MultiPageFormData; using LearningHub.Nhs.Models.OpenAthens; using LearningHub.Nhs.Services; @@ -15,9 +17,13 @@ using LearningHub.Nhs.WebUI.JsDetection; using LearningHub.Nhs.WebUI.Services; using Microsoft.AspNetCore.Hosting; + using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; + using TELBlazor.Components.Core.Configuration; + using TELBlazor.Components.Core.Services.HelperServices; + using TELBlazor.Components.OptionalImplementations.Test.TestComponents.SearchExperiment; /// /// The service mappings. @@ -85,7 +91,13 @@ public static void AddLearningHubMappings(this IServiceCollection services, ICon // Config services.Configure(configuration.GetSection("OpenAthensScopes")); - services.Configure(configuration.GetSection(BFFPathValidationOptions.SectionName)); + services.Configure(configuration.GetSection("Settings:" + BFFPathValidationOptions.SectionName)); // qqqq + + // Blazor + services.AddRazorComponents() + .AddInteractiveServerComponents() + .AddCircuitOptions(opt => opt.DetailedErrors = true) + .AddInteractiveWebAssemblyComponents(); // Learning Hub Services services.AddTransient(); @@ -132,6 +144,36 @@ public static void AddLearningHubMappings(this IServiceCollection services, ICon services.AddScoped(); services.AddScoped(); services.AddScoped(); + + // + + // + // Future candidates for DI collection + services.AddBlazoredLocalStorage(); + + /* The base TELBlazor Configuration inherited by other components uses this configuration to tell blazor components ahead of time if the browser has Javascript (need to load the wasm and hydrate) via JsEnabled. + This allows for logic and UI to be implemented specifically for no js if desired without a second load of the component, where this may be desireable. + Host information is also provided which is useful for debugging. + */ + services.AddSingleton(provider => + { + var httpContextAccessor = provider.GetRequiredService(); + var context = httpContextAccessor.HttpContext; + bool jsEnabled = false; + + if (context != null && context.Request.Cookies.TryGetValue("jsEnabled", out var jsCookieValue)) + { + jsEnabled = jsCookieValue == "true"; + } + + return new TELBlazorBaseComponentConfiguration + { + JSEnabled = jsEnabled, + HostType = $"{configuration["Properties:Environment"]} {configuration["Properties:Application"]}", + }; + }); + + services.AddScoped(); } } } diff --git a/LearningHub.Nhs.WebUI/Views/Home/LandingPage.cshtml b/LearningHub.Nhs.WebUI/Views/Home/LandingPage.cshtml index 3a681c90..0fa63d1a 100644 --- a/LearningHub.Nhs.WebUI/Views/Home/LandingPage.cshtml +++ b/LearningHub.Nhs.WebUI/Views/Home/LandingPage.cshtml @@ -37,6 +37,11 @@ Sign up, explore and learn + +
diff --git a/LearningHub.Nhs.WebUI/Views/Home/_MyAccessedLearningTray.cshtml b/LearningHub.Nhs.WebUI/Views/Home/_MyAccessedLearningTray.cshtml index 47cf3390..46b40fc1 100644 --- a/LearningHub.Nhs.WebUI/Views/Home/_MyAccessedLearningTray.cshtml +++ b/LearningHub.Nhs.WebUI/Views/Home/_MyAccessedLearningTray.cshtml @@ -37,7 +37,9 @@ }

My accessed learning

- + +