From d0e174f4f552e48fa207f8e5ed9ced613160dd06 Mon Sep 17 00:00:00 2001 From: Georg Dangl Date: Wed, 29 May 2024 13:57:01 +0200 Subject: [PATCH] Add error handling for Navisworks internal errors --- .../Controllers/ViewpointsController.cs | 4 + src/IPA.Bcfier.App/Hubs/BcfierHub.cs | 8 + src/IPA.Bcfier.App/IPA.Bcfier.App.csproj | 1 + .../Services/PluginErrorListenerService.cs | 55 ++++++ src/IPA.Bcfier.App/Startup.cs | 9 + .../IpcNavisworksCommandListener.cs | 10 ++ .../NavisworksTaskQueueHandler.cs | 106 +++++++----- .../NavisworksViewpointCreationService.cs | 18 +- src/IPA.Bcfier/Ipc/IpcMessageCommand.cs | 4 +- src/ipa-bcfier-ui/package-lock.json | 158 +++++++++++++++++- src/ipa-bcfier-ui/package.json | 1 + src/ipa-bcfier-ui/src/app/app.component.ts | 2 + .../cad-error-dialog.component.html | 7 + .../cad-error-dialog.component.scss | 0 .../cad-error-dialog.component.spec.ts | 23 +++ .../cad-error-dialog.component.ts | 26 +++ .../bcfier-hub-connector.service.spec.ts | 16 ++ .../bcfier-hub-connector.service.ts | 52 ++++++ 18 files changed, 439 insertions(+), 61 deletions(-) create mode 100644 src/IPA.Bcfier.App/Hubs/BcfierHub.cs create mode 100644 src/IPA.Bcfier.App/Services/PluginErrorListenerService.cs create mode 100644 src/ipa-bcfier-ui/src/app/components/cad-error-dialog/cad-error-dialog.component.html create mode 100644 src/ipa-bcfier-ui/src/app/components/cad-error-dialog/cad-error-dialog.component.scss create mode 100644 src/ipa-bcfier-ui/src/app/components/cad-error-dialog/cad-error-dialog.component.spec.ts create mode 100644 src/ipa-bcfier-ui/src/app/components/cad-error-dialog/cad-error-dialog.component.ts create mode 100644 src/ipa-bcfier-ui/src/app/services/connectors/bcfier-hub-connector.service.spec.ts create mode 100644 src/ipa-bcfier-ui/src/app/services/connectors/bcfier-hub-connector.service.ts diff --git a/src/IPA.Bcfier.App/Controllers/ViewpointsController.cs b/src/IPA.Bcfier.App/Controllers/ViewpointsController.cs index c7456cba..4c699fc3 100644 --- a/src/IPA.Bcfier.App/Controllers/ViewpointsController.cs +++ b/src/IPA.Bcfier.App/Controllers/ViewpointsController.cs @@ -62,6 +62,7 @@ await ipcHandler.SendMessageAsync(JsonConvert.SerializeObject(new IpcMessage else { IpcHandler.ReceivedMessages.Enqueue(message); + await Task.Delay(100); } } } @@ -108,6 +109,7 @@ await ipcHandler.SendMessageAsync(JsonConvert.SerializeObject(new IpcMessage else { IpcHandler.ReceivedMessages.Enqueue(message); + await Task.Delay(100); } } } @@ -152,6 +154,7 @@ await ipcHandler.SendMessageAsync(JsonConvert.SerializeObject(new IpcMessage else { IpcHandler.ReceivedMessages.Enqueue(message); + await Task.Delay(100); } } } @@ -197,6 +200,7 @@ await ipcHandler.SendMessageAsync(JsonConvert.SerializeObject(new IpcMessage else { IpcHandler.ReceivedMessages.Enqueue(message); + await Task.Delay(100); } } } diff --git a/src/IPA.Bcfier.App/Hubs/BcfierHub.cs b/src/IPA.Bcfier.App/Hubs/BcfierHub.cs new file mode 100644 index 00000000..0e8b4fe8 --- /dev/null +++ b/src/IPA.Bcfier.App/Hubs/BcfierHub.cs @@ -0,0 +1,8 @@ +using Microsoft.AspNetCore.SignalR; + +namespace IPA.Bcfier.App.Hubs +{ + public class BcfierHub : Hub + { + } +} diff --git a/src/IPA.Bcfier.App/IPA.Bcfier.App.csproj b/src/IPA.Bcfier.App/IPA.Bcfier.App.csproj index 850a3944..92abc03d 100644 --- a/src/IPA.Bcfier.App/IPA.Bcfier.App.csproj +++ b/src/IPA.Bcfier.App/IPA.Bcfier.App.csproj @@ -17,6 +17,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/src/IPA.Bcfier.App/Services/PluginErrorListenerService.cs b/src/IPA.Bcfier.App/Services/PluginErrorListenerService.cs new file mode 100644 index 00000000..9390da7c --- /dev/null +++ b/src/IPA.Bcfier.App/Services/PluginErrorListenerService.cs @@ -0,0 +1,55 @@ +using IPA.Bcfier.App.Hubs; +using IPA.Bcfier.Ipc; +using Microsoft.AspNetCore.SignalR; +using Newtonsoft.Json; + +namespace IPA.Bcfier.App.Services +{ + public class PluginErrorListenerService : IHostedService + { + private bool _isListening; + private readonly IServiceProvider _serviceProvider; + + public PluginErrorListenerService(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + public Task StartAsync(CancellationToken cancellationToken) + { + _isListening = true; + Task.Run(() => ListenAsync()); + return Task.CompletedTask; + } + + private async Task ListenAsync() + { + while (_isListening) + { + if (IpcHandler.ReceivedMessages.TryDequeue(out var message)) + { + var ipcMessage = JsonConvert.DeserializeObject(message)!; + if (ipcMessage.Command == IpcMessageCommand.PluginErrorEncountered) + { + using var scope = _serviceProvider.CreateScope(); + var hubContext = scope.ServiceProvider.GetRequiredService>(); + await hubContext.Clients.All.SendAsync("InternalError", ipcMessage.Data); + } + else + { + IpcHandler.ReceivedMessages.Enqueue(message); + } + } + + + await Task.Delay(500); + } + } + + public Task StopAsync(CancellationToken cancellationToken) + { + _isListening = true; + return Task.CompletedTask; + } + } +} diff --git a/src/IPA.Bcfier.App/Startup.cs b/src/IPA.Bcfier.App/Startup.cs index 13f61575..beb4643e 100644 --- a/src/IPA.Bcfier.App/Startup.cs +++ b/src/IPA.Bcfier.App/Startup.cs @@ -2,10 +2,13 @@ using ElectronNET.API; using IPA.Bcfier.App.Configuration; using IPA.Bcfier.App.Data; +using IPA.Bcfier.App.Hubs; using IPA.Bcfier.App.Services; using IPA.Bcfier.Services; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.SignalR; using Microsoft.EntityFrameworkCore; +using Newtonsoft.Json.Converters; namespace IPA.Bcfier.App { @@ -33,6 +36,11 @@ public void ConfigureServices(IServiceCollection services) services.AddTransient(); AddDatabaseServices(services); + + services.AddHostedService(); + + services.AddSignalR() + .AddNewtonsoftJsonProtocol(c => c.PayloadSerializerSettings.Converters.Add(new StringEnumConverter())); } private static void AddDatabaseServices(IServiceCollection services) @@ -75,6 +83,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseEndpoints(endpoints => { endpoints.MapControllers(); + endpoints.MapHub("/hubs/bcfier"); }); if (env.IsDevelopment()) diff --git a/src/IPA.Bcfier.Navisworks/IpcNavisworksCommandListener.cs b/src/IPA.Bcfier.Navisworks/IpcNavisworksCommandListener.cs index ee85ade2..b7c0f282 100644 --- a/src/IPA.Bcfier.Navisworks/IpcNavisworksCommandListener.cs +++ b/src/IPA.Bcfier.Navisworks/IpcNavisworksCommandListener.cs @@ -2,6 +2,7 @@ using Newtonsoft.Json; using IPA.Bcfier.Models.Bcf; using IPA.Bcfier.Navisworks.Models; +using System.Collections.Concurrent; namespace IPA.Bcfier.Navisworks { @@ -101,6 +102,15 @@ await _ipcHandler.SendMessageAsync(JsonConvert.SerializeObject(new IpcMessage } } + if (_navisworksTaskHandler.CadErrorMessages.TryDequeue(out var errorMessage)) + { + await _ipcHandler.SendMessageAsync(JsonConvert.SerializeObject(new IpcMessage + { + Command = IpcMessageCommand.PluginErrorEncountered, + Data = errorMessage + })); + } + await Task.Delay(100); } diff --git a/src/IPA.Bcfier.Navisworks/NavisworksTaskQueueHandler.cs b/src/IPA.Bcfier.Navisworks/NavisworksTaskQueueHandler.cs index f15879ec..0e0a0791 100644 --- a/src/IPA.Bcfier.Navisworks/NavisworksTaskQueueHandler.cs +++ b/src/IPA.Bcfier.Navisworks/NavisworksTaskQueueHandler.cs @@ -4,6 +4,7 @@ using Autodesk.Navisworks.Api; using IPA.Bcfier.Navisworks.Services; using Newtonsoft.Json.Serialization; +using System.Collections.Concurrent; namespace IPA.Bcfier.Navisworks { @@ -14,6 +15,8 @@ public class NavisworksTaskQueueHandler public Queue ShowViewpointQueueItems { get; } = new Queue(); public Queue> GetAvailableNavisworksClashes { get; } = new Queue>(); private bool shouldUnregister = false; + public ConcurrentQueue CadErrorMessages { get; } = new ConcurrentQueue(); + public void OnIdling(object sender, EventArgs args) { @@ -84,56 +87,70 @@ private void HandleGetAvailableNavisworksClashes(Func callback, Do private void HandleCreateNavisworksViewpointCallback(Func callback, Document uiDocument) { - var viewpointService = new NavisworksViewpointCreationService(uiDocument); - var viewpoint = viewpointService.GenerateViewpoint(); - var contractResolver = new DefaultContractResolver - { - NamingStrategy = new CamelCaseNamingStrategy() - }; - var serializerSettings = new JsonSerializerSettings - { - ContractResolver = contractResolver, - Formatting = Formatting.Indented - }; - Task.Run(async () => + try { - if (viewpoint == null) + var viewpointService = new NavisworksViewpointCreationService(uiDocument); + var viewpoint = viewpointService.GenerateViewpoint(); + var contractResolver = new DefaultContractResolver { - await callback("{}"); - } - else + NamingStrategy = new CamelCaseNamingStrategy() + }; + var serializerSettings = new JsonSerializerSettings { - await callback(JsonConvert.SerializeObject(viewpoint, serializerSettings)); - } - }); + ContractResolver = contractResolver, + Formatting = Formatting.Indented + }; + Task.Run(async () => + { + if (viewpoint == null) + { + await callback("{}"); + } + else + { + await callback(JsonConvert.SerializeObject(viewpoint, serializerSettings)); + } + }); + } + catch (Exception e) + { + CadErrorMessages.Enqueue($"Error during viewpoint creation: {Environment.NewLine}{e}"); + } } private void HandleCreateNavisworksClashIssuesCallback(Func callback, Document uiDocument, Guid clashId) { - var viewpointService = new NavisworksViewpointCreationService(uiDocument); - var clashIssues = viewpointService.CreateClashIssues(clashId); - var contractResolver = new DefaultContractResolver + try { - NamingStrategy = new CamelCaseNamingStrategy() - }; - var serializerSettings = new JsonSerializerSettings - { - ContractResolver = contractResolver, - Formatting = Formatting.Indented - }; - Task.Run(async () => - { - if (clashIssues == null) + var viewpointService = new NavisworksViewpointCreationService(uiDocument); + var clashIssues = viewpointService.CreateClashIssues(clashId); + var contractResolver = new DefaultContractResolver { - await callback("[]"); - } - else + NamingStrategy = new CamelCaseNamingStrategy() + }; + var serializerSettings = new JsonSerializerSettings { - await callback(JsonConvert.SerializeObject(clashIssues, serializerSettings)); - } - }); + ContractResolver = contractResolver, + Formatting = Formatting.Indented + }; + Task.Run(async () => + { + if (clashIssues == null) + { + await callback("[]"); + } + else + { + await callback(JsonConvert.SerializeObject(clashIssues, serializerSettings)); + } + }); + } + catch (Exception e) + { + CadErrorMessages.Enqueue($"Error during clash issues creation: {Environment.NewLine}{e}"); + } } private void HandleShowNavisworksViewpointCallback(Func? callback, BcfViewpoint? viewpoint, Document uiDocument) @@ -143,9 +160,16 @@ private void HandleShowNavisworksViewpointCallback(Func? callback, BcfView return; } - var viewpointService = new NavisworksViewpointDisplayService(uiDocument); - viewpointService.DisplayViewpoint(viewpoint); - Task.Run(async () => await callback()); + try + { + var viewpointService = new NavisworksViewpointDisplayService(uiDocument); + viewpointService.DisplayViewpoint(viewpoint); + Task.Run(async () => await callback()); + } + catch (Exception e) + { + CadErrorMessages.Enqueue($"Error during viewpoint rendering: {Environment.NewLine}{e}"); + } } } } diff --git a/src/IPA.Bcfier.Navisworks/Services/NavisworksViewpointCreationService.cs b/src/IPA.Bcfier.Navisworks/Services/NavisworksViewpointCreationService.cs index fd0c439f..0404efd8 100644 --- a/src/IPA.Bcfier.Navisworks/Services/NavisworksViewpointCreationService.cs +++ b/src/IPA.Bcfier.Navisworks/Services/NavisworksViewpointCreationService.cs @@ -1,4 +1,4 @@ -using Autodesk.Navisworks.Api; +using Autodesk.Navisworks.Api; using Autodesk.Navisworks.Api.Clash; using IPA.Bcfier.Models.Bcf; using IPA.Bcfier.Models.Clashes; @@ -21,18 +21,10 @@ public NavisworksViewpointCreationService(Document doc) /// public BcfViewpoint? GenerateViewpoint() { - try - { - var viewpoint = _doc.CurrentViewpoint.Value; - NavisUtils.GetGunits(_doc); - var v = GetViewpointFromNavisworksViewpoint(viewpoint); - return v; - } - catch - { - // We're not handling errors here at the moment, we just fail☹ - return null; - } + var viewpoint = _doc.CurrentViewpoint.Value; + NavisUtils.GetGunits(_doc); + var v = GetViewpointFromNavisworksViewpoint(viewpoint); + return v; } private BcfViewpoint GetViewpointFromNavisworksViewpoint(Viewpoint viewpoint) diff --git a/src/IPA.Bcfier/Ipc/IpcMessageCommand.cs b/src/IPA.Bcfier/Ipc/IpcMessageCommand.cs index a9b5d653..523a006e 100644 --- a/src/IPA.Bcfier/Ipc/IpcMessageCommand.cs +++ b/src/IPA.Bcfier/Ipc/IpcMessageCommand.cs @@ -18,6 +18,8 @@ public enum IpcMessageCommand GetNavisworksAvailableClashes = 7, - NavisworksAvailableClashes = 8 + NavisworksAvailableClashes = 8, + + PluginErrorEncountered = 9 } } diff --git a/src/ipa-bcfier-ui/package-lock.json b/src/ipa-bcfier-ui/package-lock.json index b63c4480..ce3c6324 100644 --- a/src/ipa-bcfier-ui/package-lock.json +++ b/src/ipa-bcfier-ui/package-lock.json @@ -18,6 +18,7 @@ "@angular/platform-browser": "^17.3.0", "@angular/platform-browser-dynamic": "^17.3.0", "@angular/router": "^17.3.0", + "@microsoft/signalr": "6.0.4", "@ngx-dropzone/cdk": "^17.2.0", "@ngx-dropzone/material": "^17.2.0", "ng-lightquery": "^2.4.0", @@ -3670,6 +3671,38 @@ "tslib": "^2.1.0" } }, + "node_modules/@microsoft/signalr": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@microsoft/signalr/-/signalr-6.0.4.tgz", + "integrity": "sha512-YeWRh4LxfYnq4I5CKw17/HOq8rY+ouTv6Bq+s55122StE3pK29j8j2OpP+1PA3D1ksHPfy7dFIgC33yr/E+01A==", + "dependencies": { + "abort-controller": "^3.0.0", + "eventsource": "^1.0.7", + "fetch-cookie": "^0.11.0", + "node-fetch": "^2.6.7", + "ws": "^7.4.5" + } + }, + "node_modules/@microsoft/signalr/node_modules/ws": { + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", + "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/@ngtools/webpack": { "version": "17.3.2", "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-17.3.2.tgz", @@ -4760,6 +4793,17 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -6452,7 +6496,6 @@ "version": "0.1.13", "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "dev": true, "optional": true, "dependencies": { "iconv-lite": "^0.6.2" @@ -6462,7 +6505,6 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, "optional": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -6745,6 +6787,14 @@ "node": ">= 0.6" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "engines": { + "node": ">=6" + } + }, "node_modules/eventemitter3": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", @@ -6760,6 +6810,14 @@ "node": ">=0.8.x" } }, + "node_modules/eventsource": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-1.1.2.tgz", + "integrity": "sha512-xAH3zWhgO2/3KIniEKYPr8plNSzlGINOUqYj0m0u7AB81iRw8b/3E73W6AuU+6klLbaSFmZnaETQ2lXPfAydrA==", + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -6951,6 +7009,17 @@ "node": ">=0.8.0" } }, + "node_modules/fetch-cookie": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/fetch-cookie/-/fetch-cookie-0.11.0.tgz", + "integrity": "sha512-BQm7iZLFhMWFy5CZ/162sAGjBfdNWb7a8LEqqnzsHFhxT/X/SVj/z2t2nu3aJvjlbQkrAlTUApplPRjWyH4mhA==", + "dependencies": { + "tough-cookie": "^2.3.3 || ^3.0.1 || ^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/figures": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", @@ -9422,6 +9491,25 @@ "dev": true, "optional": true }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/node-fetch-native": { "version": "1.6.4", "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.4.tgz", @@ -10511,11 +10599,15 @@ "dev": true, "optional": true }, + "node_modules/psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, "engines": { "node": ">=6" } @@ -10544,6 +10636,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -10813,8 +10910,7 @@ "node_modules/requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", - "dev": true + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" }, "node_modules/resolve": { "version": "1.22.8", @@ -11032,7 +11128,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true + "devOptional": true }, "node_modules/safevalues": { "version": "0.3.4", @@ -12067,6 +12163,33 @@ "node": ">=0.6" } }, + "node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tough-cookie/node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, "node_modules/tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", @@ -12304,6 +12427,15 @@ "punycode": "^2.1.0" } }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -12863,6 +12995,11 @@ "defaults": "^1.0.3" } }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, "node_modules/webpack": { "version": "5.90.3", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.90.3.tgz", @@ -13163,6 +13300,15 @@ "node": ">=0.8.0" } }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", diff --git a/src/ipa-bcfier-ui/package.json b/src/ipa-bcfier-ui/package.json index e654676d..c913bb42 100644 --- a/src/ipa-bcfier-ui/package.json +++ b/src/ipa-bcfier-ui/package.json @@ -21,6 +21,7 @@ "@angular/platform-browser": "^17.3.0", "@angular/platform-browser-dynamic": "^17.3.0", "@angular/router": "^17.3.0", + "@microsoft/signalr": "6.0.4", "@ngx-dropzone/cdk": "^17.2.0", "@ngx-dropzone/material": "^17.2.0", "ng-lightquery": "^2.4.0", diff --git a/src/ipa-bcfier-ui/src/app/app.component.ts b/src/ipa-bcfier-ui/src/app/app.component.ts index 1b6c5d5e..b14f91d5 100644 --- a/src/ipa-bcfier-ui/src/app/app.component.ts +++ b/src/ipa-bcfier-ui/src/app/app.component.ts @@ -22,6 +22,7 @@ import { BackendService } from './services/BackendService'; import { BcfFileAutomaticallySaveService } from './services/bcf-file-automaticaly-save.service'; import { BcfFileComponent } from './components/bcf-file/bcf-file.component'; import { BcfFilesMessengerService } from './services/bcf-files-messenger.service'; +import { BcfierHubConnectorService } from './services/connectors/bcfier-hub-connector.service'; import { CommonModule } from '@angular/common'; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; @@ -54,6 +55,7 @@ export class AppComponent implements OnDestroy { private backendService: BackendService, private notificationsService: NotificationsService, private bcfFileAutomaticallySaveService: BcfFileAutomaticallySaveService, + private bcfierHubConnectorService: BcfierHubConnectorService, // We want to initialize it so it's listening to SignalR messages appConfigService: AppConfigService, projectsClient: ProjectsClient, selectedProjectMessengerService: SelectedProjectMessengerService diff --git a/src/ipa-bcfier-ui/src/app/components/cad-error-dialog/cad-error-dialog.component.html b/src/ipa-bcfier-ui/src/app/components/cad-error-dialog/cad-error-dialog.component.html new file mode 100644 index 00000000..ff584feb --- /dev/null +++ b/src/ipa-bcfier-ui/src/app/components/cad-error-dialog/cad-error-dialog.component.html @@ -0,0 +1,7 @@ +

CAD Plugin Error

+ +
{{ errorMessage }}
+
+ + + diff --git a/src/ipa-bcfier-ui/src/app/components/cad-error-dialog/cad-error-dialog.component.scss b/src/ipa-bcfier-ui/src/app/components/cad-error-dialog/cad-error-dialog.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/ipa-bcfier-ui/src/app/components/cad-error-dialog/cad-error-dialog.component.spec.ts b/src/ipa-bcfier-ui/src/app/components/cad-error-dialog/cad-error-dialog.component.spec.ts new file mode 100644 index 00000000..46ecc16d --- /dev/null +++ b/src/ipa-bcfier-ui/src/app/components/cad-error-dialog/cad-error-dialog.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CadErrorDialogComponent } from './cad-error-dialog.component'; + +describe('CadErrorDialogComponent', () => { + let component: CadErrorDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [CadErrorDialogComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(CadErrorDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/ipa-bcfier-ui/src/app/components/cad-error-dialog/cad-error-dialog.component.ts b/src/ipa-bcfier-ui/src/app/components/cad-error-dialog/cad-error-dialog.component.ts new file mode 100644 index 00000000..b51c6e18 --- /dev/null +++ b/src/ipa-bcfier-ui/src/app/components/cad-error-dialog/cad-error-dialog.component.ts @@ -0,0 +1,26 @@ +import { Component, Inject } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; +import { + MAT_DIALOG_DATA, + MatDialogModule, + MatDialogRef, +} from '@angular/material/dialog'; + +@Component({ + selector: 'bcfier-cad-error-dialog', + standalone: true, + imports: [MatButtonModule, MatDialogModule], + templateUrl: './cad-error-dialog.component.html', + styleUrl: './cad-error-dialog.component.scss', +}) +export class CadErrorDialogComponent { + constructor( + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) + public errorMessage: string + ) {} + + close(): void { + this.dialogRef.close(); + } +} diff --git a/src/ipa-bcfier-ui/src/app/services/connectors/bcfier-hub-connector.service.spec.ts b/src/ipa-bcfier-ui/src/app/services/connectors/bcfier-hub-connector.service.spec.ts new file mode 100644 index 00000000..28d91f19 --- /dev/null +++ b/src/ipa-bcfier-ui/src/app/services/connectors/bcfier-hub-connector.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { BcfierHubConnectorService } from './bcfier-hub-connector.service'; + +describe('BcfierHubConnectorService', () => { + let service: BcfierHubConnectorService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(BcfierHubConnectorService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/ipa-bcfier-ui/src/app/services/connectors/bcfier-hub-connector.service.ts b/src/ipa-bcfier-ui/src/app/services/connectors/bcfier-hub-connector.service.ts new file mode 100644 index 00000000..c1ae9e7c --- /dev/null +++ b/src/ipa-bcfier-ui/src/app/services/connectors/bcfier-hub-connector.service.ts @@ -0,0 +1,52 @@ +import { + HubConnection, + HubConnectionBuilder, + HubConnectionState, +} from '@microsoft/signalr'; +import { Injectable, NgZone } from '@angular/core'; + +import { CadErrorDialogComponent } from '../../components/cad-error-dialog/cad-error-dialog.component'; +import { LoadingService } from '../loading.service'; +import { MatDialog } from '@angular/material/dialog'; +import { NotificationsService } from '../notifications.service'; + +@Injectable({ + providedIn: 'root', +}) +export class BcfierHubConnectorService { + private connection: HubConnection; + + constructor( + private notificationsService: NotificationsService, + private ngZone: NgZone, + private loadingService: LoadingService, + private matDialog: MatDialog + ) { + this.connection = new HubConnectionBuilder() + .withAutomaticReconnect() + .withUrl(window.location.origin + '/hubs/bcfier') + .build(); + + if (this.connection.state !== HubConnectionState.Connected) { + this.ngZone.runOutsideAngular(() => { + this.connection.start(); + }); + } + + this.setUpMessageListeners(); + } + + private setUpMessageListeners(): void { + this.connection.on('InternalError', (errorMessage: string) => { + this.ngZone.run(() => { + this.notificationsService.error(errorMessage, 'CAD Error'); + this.matDialog.open(CadErrorDialogComponent, { + data: errorMessage, + }); + // We usually want to hide the loading screen if we receive an error from the CAD + // system, since that means something went wrong and waiting further is pointless + this.loadingService.hideLoadingScreen(); + }); + }); + } +}