diff --git a/src/IPA.Bcfier.App/Controllers/ViewpointsController.cs b/src/IPA.Bcfier.App/Controllers/ViewpointsController.cs
index c7456cb..4c699fc 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 0000000..0e8b4fe
--- /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 850a394..92abc03 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 0000000..9390da7
--- /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 13f6157..beb4643 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 ee85ade..b7c0f28 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 f15879e..0e0a079 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 fd0c439..0404efd 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 a9b5d65..523a006 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 b63c448..ce3c632 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 e654676..c913bb4 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 1b6c5d5..b14f91d 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 0000000..ff584fe
--- /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 0000000..e69de29
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 0000000..46ecc16
--- /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 0000000..b51c6e1
--- /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 0000000..28d91f1
--- /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 0000000..c1ae9e7
--- /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();
+ });
+ });
+ }
+}