diff --git a/.github/workflows/CodeQuality.yml b/.github/workflows/CodeQuality.yml index d93a745..5098563 100644 --- a/.github/workflows/CodeQuality.yml +++ b/.github/workflows/CodeQuality.yml @@ -16,7 +16,7 @@ jobs: - name: Setup dotnet uses: actions/setup-dotnet@v3 with: - dotnet-version: '7.0.x' + dotnet-version: '8.0.x' - name: install wasm-tools run: dotnet workload install wasm-tools diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 63a63b6..ff31889 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -29,7 +29,7 @@ jobs: - name: Setup .NET Core uses: actions/setup-dotnet@v3 with: - dotnet-version: 7.0.x + dotnet-version: 8.0.x - name: install wasm-tools run: dotnet workload install wasm-tools - name: Install dependencies diff --git a/reqifviewer.Tests/Pages/Index/IndexPageTestFixture.cs b/reqifviewer.Tests/Pages/Index/IndexPageTestFixture.cs new file mode 100644 index 0000000..28cbf48 --- /dev/null +++ b/reqifviewer.Tests/Pages/Index/IndexPageTestFixture.cs @@ -0,0 +1,101 @@ +// ------------------------------------------------------------------------------------------------- +// +// +// Copyright 2023-2024 RHEA System S.A. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// +// ------------------------------------------------------------------------------------------------- + +namespace ReqifViewer.Tests.Pages.Index +{ + using System.IO; + using System.Threading; + using System.Threading.Tasks; + + using Bunit; + + using Microsoft.AspNetCore.Components.Forms; + using Microsoft.Extensions.DependencyInjection; + + using Moq; + + using NUnit.Framework; + + using Radzen.Blazor; + + using ReqIFSharp; + using ReqIFSharp.Extensions.Services; + + using reqifviewer.Pages.Index; + + using TestContext = Bunit.TestContext; + + /// + /// Suite of tests for the + /// + [TestFixture] + public class IndexPageTestFixture + { + private Mock reqIfLoaderService; + private TestContext context; + private const long MaxFileSize = 5 * 1024 * 1024; + + [SetUp] + public void SetUp() + { + this.context = new TestContext(); + this.reqIfLoaderService = new Mock(); + + this.context.Services.AddSingleton(this.reqIfLoaderService.Object); + } + + [TearDown] + public void TearDown() + { + this.context.Dispose(); + } + + [Test] + public async Task VerifyComponent() + { + var renderer = this.context.RenderComponent(); + var uploadComponent = renderer.FindComponent(); + + var file = new Mock(); + file.Setup(x => x.Size).Returns(MaxFileSize + 1); + file.Setup(x => x.Name).Returns("file.reqif"); + file.Setup(x => x.OpenReadStream(It.IsAny(), It.IsAny())).Returns(new MemoryStream()); + + await renderer.InvokeAsync(() => uploadComponent.Instance.OnChange.InvokeAsync(new InputFileChangeEventArgs([file.Object]))); + file.Verify(x => x.OpenReadStream(It.IsAny(), It.IsAny()), Times.Never); + + file.Setup(x => x.Size).Returns(MaxFileSize - 1); + await renderer.InvokeAsync(() => uploadComponent.Instance.OnChange.InvokeAsync(new InputFileChangeEventArgs([file.Object]))); + file.Verify(x => x.OpenReadStream(It.IsAny(), It.IsAny()), Times.Once); + + var loadButton = renderer.FindComponent(); + await renderer.InvokeAsync(loadButton.Instance.Click.InvokeAsync); + this.reqIfLoaderService.Verify(x => x.Load(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + + var cancelButton = renderer.FindComponents()[1]; + await renderer.InvokeAsync(cancelButton.Instance.Click.InvokeAsync); + Assert.That(renderer.Instance.IsLoading, Is.EqualTo(false)); + + var clearButton = renderer.FindComponents()[2]; + await renderer.InvokeAsync(clearButton.Instance.Click.InvokeAsync); + this.reqIfLoaderService.Verify(x => x.Reset(), Times.AtLeastOnce); + } + } +} diff --git a/reqifviewer.Tests/reqifviewer.Tests.csproj b/reqifviewer.Tests/reqifviewer.Tests.csproj index 89f3638..2913d9c 100644 --- a/reqifviewer.Tests/reqifviewer.Tests.csproj +++ b/reqifviewer.Tests/reqifviewer.Tests.csproj @@ -1,7 +1,7 @@  - net7.0 + net8.0 RHEA System S.A. reqifviewer.Tests reqifviewer test project @@ -31,10 +31,6 @@ - - - - Always diff --git a/reqifviewer/Dockerfile b/reqifviewer/Dockerfile index 7c0c675..19d8a02 100644 --- a/reqifviewer/Dockerfile +++ b/reqifviewer/Dockerfile @@ -1,16 +1,16 @@ -FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build - -WORKDIR /src - -COPY Nuget.Config . -COPY reqifviewer reqifviewer -RUN dotnet restore --configfile Nuget.Config reqifviewer -RUN dotnet build --no-restore reqifviewer -c Release -o /app/build - -FROM build AS publish -RUN dotnet publish reqifviewer -c Release -o /app/publish - -FROM nginx:alpine AS final -WORKDIR /usr/share/nginx/html -COPY --from=publish /app/publish/wwwroot . +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build + +WORKDIR /src + +COPY Nuget.Config . +COPY reqifviewer reqifviewer +RUN dotnet restore --configfile Nuget.Config reqifviewer +RUN dotnet build --no-restore reqifviewer -c Release -o /app/build + +FROM build AS publish +RUN dotnet publish reqifviewer -c Release -o /app/publish + +FROM nginx:alpine AS final +WORKDIR /usr/share/nginx/html +COPY --from=publish /app/publish/wwwroot . COPY reqifviewer/nginx.conf /etc/nginx/nginx.conf \ No newline at end of file diff --git a/reqifviewer/Pages/Index/IndexPage.razor b/reqifviewer/Pages/Index/IndexPage.razor index c4e32c8..81ed3f6 100644 --- a/reqifviewer/Pages/Index/IndexPage.razor +++ b/reqifviewer/Pages/Index/IndexPage.razor @@ -1,4 +1,5 @@ - -@page "/" - -@using System.Diagnostics @using System.Globalization -@using System.IO -@using System.Threading @using ReqIFSharp -@using ReqIFSharp.Extensions.Services -@using Serilog; - -@inject IReqIFLoaderService ReqIfLoaderService
@@ -35,17 +27,18 @@
-
- -
+
+ +
+ @(this.ErrorMessage)
- - - + + + -@if (this.isLoading) +@if (this.IsLoading) { } @@ -83,137 +76,4 @@ else } -} - -@code { - - private string fileSelectionText = "Select a file"; - - private MemoryStream reqifStream; - - private bool reqifisAvailable = false; - - private bool isLoading = false; - - private IEnumerable reqIfs; - - private CancellationTokenSource cancellationTokenSource; - - /// - /// Invoked when the component is initialized after having received its initial parameters - /// - protected override void OnInitialized() - { - if (this.ReqIfLoaderService.ReqIFData == null || !this.ReqIfLoaderService.ReqIFData.Any()) - { - this.reqIfs = null; - - Log.ForContext().Debug("no ReqIF loaded"); - } - else - { - this.reqIfs = this.ReqIfLoaderService.ReqIFData; - - Log.ForContext().Debug("a Total of {amount} ReqIF loaded", this.reqIfs.Count()); - } - } - - /// - /// handles file selection - /// - /// - /// The to be used to handle the selected file - /// - /// - /// an awaitable - /// - private async Task HandleSelection(InputFileChangeEventArgs e) - { - var sw = Stopwatch.StartNew(); - - this.reqifisAvailable = false; - - this.reqifStream = new MemoryStream(); - - await e.File.OpenReadStream(long.MaxValue).CopyToAsync(this.reqifStream); - - this.fileSelectionText = e.File.Name; - - this.reqifisAvailable = true; - - Log.ForContext().Information("file read into stream in {time} [ms]" , sw.ElapsedMilliseconds); - } - - /// - /// Loads the from the selected file - /// - /// - /// an awaitable - /// - private async Task OnLoadReqIF() - { - try - { - var sw = Stopwatch.StartNew(); - - this.isLoading = true; - - this.StateHasChanged(); - - await Task.Delay(500); - - this.cancellationTokenSource = new CancellationTokenSource(); - - this.reqIfs = null; - - if (this.reqifStream.Position != 0) - { - this.reqifStream.Seek(0, SeekOrigin.Begin); - } - - var convertPathToSupportedFileExtensionKind = this.fileSelectionText.ConvertPathToSupportedFileExtensionKind(); - - await this.ReqIfLoaderService.Load(this.reqifStream,convertPathToSupportedFileExtensionKind, this.cancellationTokenSource.Token); - this.reqIfs = this.ReqIfLoaderService.ReqIFData; - - Log.ForContext().Information("a total of {amount} ReqIF objects deserialized in {time} [ms]", this.reqIfs.Count(), sw.ElapsedMilliseconds); - } - catch (TaskCanceledException) - { - Log.ForContext().Information("Load was cancelled"); - } - catch (Exception e) - { - Log.ForContext().Error(e, "load reqif failed"); - } - finally - { - isLoading = false; - this.StateHasChanged(); - } - } - - /// - /// Cancel loading the ReqIF file - /// - private void OnCancel() - { - if (this.cancellationTokenSource != null) - { - this.cancellationTokenSource.Cancel(); - isLoading = false; - this.StateHasChanged(); - } - } - - /// - /// Clear the ReqIF file and reset the - /// - private void OnClear() - { - this.reqIfs = null; - this.ReqIfLoaderService.Reset(); - isLoading = false; - this.StateHasChanged(); - } -} +} \ No newline at end of file diff --git a/reqifviewer/Pages/Index/IndexPage.razor.cs b/reqifviewer/Pages/Index/IndexPage.razor.cs new file mode 100644 index 0000000..355770c --- /dev/null +++ b/reqifviewer/Pages/Index/IndexPage.razor.cs @@ -0,0 +1,251 @@ +// ------------------------------------------------------------------------------------------------- +// +// +// Copyright 2023-2024 RHEA System S.A. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// +// ------------------------------------------------------------------------------------------------- + +namespace reqifviewer.Pages.Index +{ + using Microsoft.AspNetCore.Components; + using Microsoft.AspNetCore.Components.Forms; + + using ReqIFSharp; + using ReqIFSharp.Extensions.Services; + + using Serilog; + + using System.Collections.Generic; + using System.Diagnostics; + using System.IO; + using System.Linq; + using System.Threading.Tasks; + using System.Threading; + using System; + + /// + /// The code behind for the component + /// + public partial class IndexPage : ComponentBase, IDisposable + { + /// + /// The + /// + [Inject] + public IReqIFLoaderService ReqIfLoaderService { get; set; } + + /// + /// Gets or sets the error message that is displayed in the component + /// + public string ErrorMessage { get; private set; } + + /// + /// The value to check if the component is loading + /// + public bool IsLoading { get; private set; } + + /// + /// The text of file selection + /// + private string fileSelectionText = "Select a file"; + + /// + /// The directory where uploaded files are stored + /// + private const string UploadsDirectory = "wwwroot/uploads/"; + + /// + /// The directory where uploaded files are stored + /// + private const long MaxFileSizeInBytes = 5 * 1024 * 1024; + + /// + /// Gets or sets the file path + /// + private string ReqIfFilePath { get; set; } + + /// + /// The value to verify if the ReqIF upload is available + /// + private bool reqifisAvailable; + + /// + /// A collection of + /// + private IEnumerable reqIfs; + + /// + /// The to cancel ReqIf loading + /// + private CancellationTokenSource cancellationTokenSource; + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public void Dispose() + { + TryDeleteFile(this.ReqIfFilePath); + this.cancellationTokenSource?.Dispose(); + } + + /// + /// Invoked when the component is initialized after having received its initial parameters + /// + protected override void OnInitialized() + { + if (this.ReqIfLoaderService.ReqIFData == null || !this.ReqIfLoaderService.ReqIFData.Any()) + { + this.reqIfs = null; + + Log.ForContext().Debug("no ReqIF loaded"); + } + else + { + this.reqIfs = this.ReqIfLoaderService.ReqIFData; + + Log.ForContext().Debug("a Total of {amount} ReqIF loaded", this.reqIfs.Count()); + } + } + + /// + /// handles file selection + /// + /// + /// The to be used to handle the selected file + /// + /// + /// an awaitable + /// + private async Task HandleSelection(InputFileChangeEventArgs e) + { + if (e.File.Size > MaxFileSizeInBytes) + { + this.ErrorMessage = $"The max file size is {MaxFileSizeInBytes/(1024*1024)} MB"; + return; + } + + if (!string.IsNullOrEmpty(this.ReqIfFilePath)) + { + TryDeleteFile(this.ReqIfFilePath); + } + + this.ErrorMessage = string.Empty; + var sw = Stopwatch.StartNew(); + this.reqifisAvailable = false; + + this.ReqIfFilePath = Path.Combine(UploadsDirectory, Guid.NewGuid().ToString()); + this.fileSelectionText = e.File.Name; + Directory.CreateDirectory(UploadsDirectory); + + await using (var fileStream = new FileStream(this.ReqIfFilePath, FileMode.Create)) + { + await e.File.OpenReadStream(MaxFileSizeInBytes).CopyToAsync(fileStream); + } + + this.reqifisAvailable = true; + await this.InvokeAsync(this.StateHasChanged); + + Log.ForContext().Information("file read into stream in {time} [ms]", sw.ElapsedMilliseconds); + } + + /// + /// Loads the from the selected file + /// + /// + /// an awaitable + /// + private async Task OnLoadReqIF() + { + try + { + var sw = Stopwatch.StartNew(); + this.IsLoading = true; + this.cancellationTokenSource = new CancellationTokenSource(); + this.reqIfs = null; + this.ReqIfLoaderService.Reset(); + await Task.Delay(500); + + var convertPathToSupportedFileExtensionKind = this.fileSelectionText.ConvertPathToSupportedFileExtensionKind(); + + await using (var reqIfFileMemoryStream = File.Open(this.ReqIfFilePath, FileMode.Open)) + { + await this.ReqIfLoaderService.Load(reqIfFileMemoryStream, convertPathToSupportedFileExtensionKind, this.cancellationTokenSource.Token); + this.reqIfs = this.ReqIfLoaderService.ReqIFData; + } + + this.reqIfs = this.ReqIfLoaderService.ReqIFData; + Log.ForContext().Information("a total of {amount} ReqIF objects deserialized in {time} [ms]", this.reqIfs.Count(), sw.ElapsedMilliseconds); + } + catch (TaskCanceledException) + { + Log.ForContext().Information("Load was cancelled"); + } + catch (Exception e) + { + Log.ForContext().Error(e, "load reqif failed"); + } + finally + { + this.IsLoading = false; + await this.InvokeAsync(this.StateHasChanged); + } + } + + /// + /// Cancel loading the ReqIF file + /// + private async Task OnCancel() + { + if (this.cancellationTokenSource != null) + { + await this.cancellationTokenSource.CancelAsync(); + TryDeleteFile(this.ReqIfFilePath); + this.IsLoading = false; + await this.InvokeAsync(this.StateHasChanged); + } + } + + /// + /// Clear the ReqIF file and reset the + /// + private async Task OnClear() + { + this.reqIfs = null; + this.ReqIfLoaderService.Reset(); + TryDeleteFile(this.ReqIfFilePath); + this.IsLoading = false; + await this.InvokeAsync(this.StateHasChanged); + } + + /// + /// Tries to delete a given physical file + /// + /// The file path + /// true if the file was removed, otherwise false + private static bool TryDeleteFile(string filePath) + { + try + { + File.Delete(filePath); + return true; + } + catch + { + return false; + } + } + } +} \ No newline at end of file diff --git a/reqifviewer/Pages/_Host.cshtml b/reqifviewer/Pages/_Host.cshtml new file mode 100644 index 0000000..372d13b --- /dev/null +++ b/reqifviewer/Pages/_Host.cshtml @@ -0,0 +1,60 @@ +@page "/" + + +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.AspNetCore.Mvc.TagHelpers +@using reqifviewer +@namespace ReqifViewer.Pages + +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers + + + + + + + + + + + + + ReqIF Viewer + + + + + + + + + + + +
+ An unhandled error has occurred. + Reload + 🗙 +
+ + + + + + + + diff --git a/reqifviewer/Program.cs b/reqifviewer/Program.cs index 07aeb0e..9032e6d 100644 --- a/reqifviewer/Program.cs +++ b/reqifviewer/Program.cs @@ -20,17 +20,14 @@ namespace reqifviewer { - using System.Net.Http; using System.Threading.Tasks; using Blazor.Analytics; using BlazorStrap; - using Microsoft.AspNetCore.Components.Web; - using Microsoft.AspNetCore.Components.WebAssembly.Hosting; - using Microsoft.Extensions.DependencyInjection; + using Microsoft.AspNetCore.Builder; using ReqIFSharp; using ReqIFSharp.Extensions.Services; @@ -62,28 +59,32 @@ public static async Task Main(string[] args) .WriteTo.BrowserConsole() .CreateLogger(); - var builder = WebAssemblyHostBuilder.CreateDefault(args); + var builder = WebApplication.CreateBuilder(args); + + builder.Services.AddRazorPages(); + builder.Services.AddServerSideBlazor(); + builder.Services.AddLogging(loggingBuilder => loggingBuilder.AddSerilog(dispose: true)); - builder.RootComponents.Add("#app"); - builder.RootComponents.Add("head::after"); - - builder.Services.AddScoped(sp => new HttpClient()); - builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); - + builder.Services.AddScoped(); + builder.Services.AddScoped(); builder.Services.AddGoogleAnalytics("295704041"); + builder.Services.AddBlazorStrap(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); + var app = builder.Build(); - builder.Services.AddBlazorStrap(); + app.UseStaticFiles(); + app.UseRouting(); + app.MapBlazorHub(); + app.MapRazorPages(); + app.MapFallbackToPage("/_Host"); - await builder.Build().RunAsync(); + await app.RunAsync(); } } } diff --git a/reqifviewer/Properties/launchSettings.json b/reqifviewer/Properties/launchSettings.json index c6cd4a6..6179bd8 100644 --- a/reqifviewer/Properties/launchSettings.json +++ b/reqifviewer/Properties/launchSettings.json @@ -11,7 +11,6 @@ "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, - "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } @@ -20,7 +19,6 @@ "commandName": "Project", "dotnetRunMessages": "true", "launchBrowser": true, - "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", "applicationUrl": "http://localhost:5000", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" diff --git a/reqifviewer/reqifviewer.csproj b/reqifviewer/reqifviewer.csproj index 9222449..a98cce5 100644 --- a/reqifviewer/reqifviewer.csproj +++ b/reqifviewer/reqifviewer.csproj @@ -1,10 +1,10 @@ - + reqifviewer 0.18.0 Web Application to inspect ReqIF files - net7.0 + net8.0 @@ -29,12 +29,10 @@ - - - + diff --git a/reqifviewer/wwwroot/index.html b/reqifviewer/wwwroot/index.html deleted file mode 100644 index a2459da..0000000 --- a/reqifviewer/wwwroot/index.html +++ /dev/null @@ -1,122 +0,0 @@ - - - - - - - - - - - - - - - ReqIF Viewer - - - - - - - - -
-
-

Getting Ready for take-off

-
-
-
-
-
-
-
- - - -
- An unhandled error has occurred. - Reload - 🗙 -
- - - - - - - -