diff options
24 files changed, 521 insertions, 253 deletions
diff --git a/backend/api/api/Controllers/WebSocketController.cs b/backend/api/api/Controllers/WebSocketController.cs new file mode 100644 index 00000000..184b47e7 --- /dev/null +++ b/backend/api/api/Controllers/WebSocketController.cs @@ -0,0 +1,63 @@ +using api.Services; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using System.Net.WebSockets; + +namespace api.Controllers +{ + [Route("api")] + [ApiController] + public class WebSocketController : ControllerBase + { + private IMLWebSocketService mlWS; + public WebSocketController(IMLWebSocketService mlWS) + { + this.mlWS = mlWS; + } + + [HttpGet("wstest")] + public string Test() + { + this.mlWS.Send("ABC123"); + return "MESSAGE SENT!"; + } + + [HttpGet("ws")] + public async Task Get() + { + if (HttpContext.WebSockets.IsWebSocketRequest) + { + using var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync(); + await Echo(webSocket); + } + else + { + HttpContext.Response.StatusCode = StatusCodes.Status400BadRequest; + } + } + + private static async Task Echo(WebSocket webSocket) + { + var buffer = new byte[1024 * 4]; + var receiveResult = await webSocket.ReceiveAsync( + new ArraySegment<byte>(buffer), CancellationToken.None); + + while (!receiveResult.CloseStatus.HasValue) + { + await webSocket.SendAsync( + new ArraySegment<byte>(buffer, 0, receiveResult.Count), + receiveResult.MessageType, + receiveResult.EndOfMessage, + CancellationToken.None); + + receiveResult = await webSocket.ReceiveAsync( + new ArraySegment<byte>(buffer), CancellationToken.None); + } + + await webSocket.CloseAsync( + receiveResult.CloseStatus.Value, + receiveResult.CloseStatusDescription, + CancellationToken.None); + } + } +} diff --git a/backend/api/api/Program.cs b/backend/api/api/Program.cs index c8cf0784..5913c2d3 100644 --- a/backend/api/api/Program.cs +++ b/backend/api/api/Program.cs @@ -33,6 +33,10 @@ builder.Services.AddScoped<IModelService, ModelService>(); builder.Services.AddScoped<IPredictorService, PredictorService>(); builder.Services.AddScoped<IFileService, FileService>(); +var mlwss = new MLWebSocketService(); + +builder.Services.AddSingleton<IMLWebSocketService>(mlwss); +builder.Services.AddHostedService(_ => mlwss); builder.Services.AddHostedService<TempFileService>(); @@ -54,6 +58,12 @@ builder.Services.AddControllers(); var app = builder.Build(); +var webSocketOptions = new WebSocketOptions +{ + KeepAliveInterval = TimeSpan.FromMinutes(2) +}; + +app.UseWebSockets(webSocketOptions); //Add Cors app.UseCors( diff --git a/backend/api/api/Services/IMLWebSocketService.cs b/backend/api/api/Services/IMLWebSocketService.cs new file mode 100644 index 00000000..52efb7fc --- /dev/null +++ b/backend/api/api/Services/IMLWebSocketService.cs @@ -0,0 +1,7 @@ +namespace api.Services +{ + public interface IMLWebSocketService + { + void Send(string message); + } +} diff --git a/backend/api/api/Services/MLWebSocketService.cs b/backend/api/api/Services/MLWebSocketService.cs new file mode 100644 index 00000000..ca6919bf --- /dev/null +++ b/backend/api/api/Services/MLWebSocketService.cs @@ -0,0 +1,68 @@ +using System.Net.WebSockets; +using System.Text; + +namespace api.Services +{ + public class MLWebSocketService: BackgroundService, IMLWebSocketService + { + private static readonly string Connection = "ws://localhost:5027"; + + private Queue<string> dataQueue = new Queue<string>(); + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + using (var socket = new ClientWebSocket()) + try + { + await socket.ConnectAsync(new Uri(Connection), stoppingToken); + + + while(dataQueue.Count > 0) + { + await Send(socket, dataQueue.Dequeue(), stoppingToken); + } + + Receive(socket, stoppingToken); + + await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "", stoppingToken); + } + catch (Exception ex) + { + Console.WriteLine($"ERROR - {ex.Message}"); + } + } + + private async Task Send(ClientWebSocket socket, string data, CancellationToken stoppingToken) => + await socket.SendAsync(Encoding.UTF8.GetBytes(data), WebSocketMessageType.Text, true, stoppingToken); + + private async Task Receive(ClientWebSocket socket, CancellationToken stoppingToken) + { + var buffer = new ArraySegment<byte>(new byte[2048]); + while (!stoppingToken.IsCancellationRequested) + { + WebSocketReceiveResult result; + using (var ms = new MemoryStream()) + { + do + { + result = await socket.ReceiveAsync(buffer, stoppingToken); + ms.Write(buffer.Array, buffer.Offset, result.Count); + } while (!result.EndOfMessage); + + if (result.MessageType == WebSocketMessageType.Close) + break; + + ms.Seek(0, SeekOrigin.Begin); + using (var reader = new StreamReader(ms, Encoding.UTF8)) + Console.WriteLine(await reader.ReadToEndAsync()); + } + }; + } + + public void Send(string data) + { + dataQueue.Enqueue(data); + } + } +} diff --git a/backend/api/api/Services/MlConnectionService.cs b/backend/api/api/Services/MlConnectionService.cs index 7adade0c..9b167537 100644 --- a/backend/api/api/Services/MlConnectionService.cs +++ b/backend/api/api/Services/MlConnectionService.cs @@ -1,4 +1,6 @@ using RestSharp; +using System.Net.WebSockets; +using System.Text; namespace api.Services { @@ -11,7 +13,6 @@ namespace api.Services request.AddJsonBody(model); var result = await client.ExecuteAsync(request); return result.Content;//Response od ML microservisa - } } } diff --git a/backend/microservice/__pycache__/mlservice.cpython-310.pyc b/backend/microservice/__pycache__/mlservice.cpython-310.pyc Binary files differnew file mode 100644 index 00000000..c079459a --- /dev/null +++ b/backend/microservice/__pycache__/mlservice.cpython-310.pyc diff --git a/backend/microservice/ml_socket.py b/backend/microservice/ml_socket.py new file mode 100644 index 00000000..5489b787 --- /dev/null +++ b/backend/microservice/ml_socket.py @@ -0,0 +1,25 @@ +import asyncio +import websockets +import json + +def get_or_create_eventloop(): + try: + return asyncio.get_event_loop() + except RuntimeError as ex: + if "There is no current event loop in thread" in str(ex): + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + return asyncio.get_event_loop() + +# create handler for each connection +async def handler(websocket, path): + #data = json.loads(await websocket.recv()) + #reply = f"Data recieved as: {data}!" + #print(data['test']) + msg = await websocket.recv() + await websocket.send("[" + msg + "]") + +start_server = websockets.serve(handler, "localhost", 5027) + +get_or_create_eventloop().run_until_complete(start_server) +get_or_create_eventloop().run_forever()
\ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index b9fd17f8..8c025c8b 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -34,6 +34,7 @@ "ngx-csv-parser": "^0.0.7", "rxjs": "~7.5.0", "tslib": "^2.3.0", + "websocket-ts": "^1.1.1", "zone.js": "~0.11.4" }, "devDependencies": { @@ -446,7 +447,6 @@ "version": "13.2.5", "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-13.2.5.tgz", "integrity": "sha512-Xd8xj2Z0ilA4TJAM/JkTtA1CAa6SuebFsEEvabHCRO5MDvtdsIUP91ADUZIqDHy7qe6Qift/rAVN2PXxT2aaNA==", - "dev": true, "dependencies": { "@babel/core": "^7.17.2", "chokidar": "^3.0.0", @@ -476,7 +476,6 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.1.2.tgz", "integrity": "sha512-hoyByceqwKirw7w3Z7gnIIZC3Wx3J484Y3L/cMpXFbr7d9ZQj2mODrirNzcJa+SM3UlpWXYvKV4RlRpFXlWgXg==", - "dev": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.0" }, @@ -488,7 +487,6 @@ "version": "7.17.5", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.17.5.tgz", "integrity": "sha512-/BBMw4EvjmyquN5O+t5eh0+YqB3XXJkYD2cjKpYtWOfFy4lQ4UozNSmxAcWT8r2XtZs0ewG+zrfsqeR15i1ajA==", - "dev": true, "dependencies": { "@ampproject/remapping": "^2.1.0", "@babel/code-frame": "^7.16.7", @@ -518,7 +516,6 @@ "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true, "bin": { "semver": "bin/semver.js" } @@ -527,7 +524,6 @@ "version": "7.17.3", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.3.tgz", "integrity": "sha512-+R6Dctil/MgUsZsZAkYgK+ADNSZzJRRy0TvY65T71z/CR854xHQ1EweBYXdfT+HNeN7w0cSJJEzgxZMv40pxsg==", - "dev": true, "dependencies": { "@babel/types": "^7.17.0", "jsesc": "^2.5.1", @@ -541,7 +537,6 @@ "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -781,7 +776,6 @@ "version": "7.16.12", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.16.12.tgz", "integrity": "sha512-dK5PtG1uiN2ikk++5OzSYsitZKny4wOCD0nrO4TqnW4BVBTQ2NGS3NgilvT/TEyxTST7LNyWV/T4tXDoD3fOgg==", - "dev": true, "dependencies": { "@babel/code-frame": "^7.16.7", "@babel/generator": "^7.16.8", @@ -811,7 +805,6 @@ "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true, "bin": { "semver": "bin/semver.js" } @@ -820,7 +813,6 @@ "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -829,7 +821,6 @@ "version": "7.16.8", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.16.8.tgz", "integrity": "sha512-1ojZwE9+lOXzcWdWmO6TbUzDfqLD39CmEhN8+2cX9XkDo5yW1OpgfejfliysR2AWLpMamTiOiAp/mtroaymhpw==", - "dev": true, "dependencies": { "@babel/types": "^7.16.8", "jsesc": "^2.5.1", @@ -843,7 +834,6 @@ "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -3276,7 +3266,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", - "dev": true, "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -3550,7 +3539,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", - "dev": true, "engines": { "node": ">=8" } @@ -3647,7 +3635,6 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, "dependencies": { "fill-range": "^7.0.1" }, @@ -3825,7 +3812,6 @@ "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "dev": true, "funding": [ { "type": "individual", @@ -4751,7 +4737,6 @@ "version": "0.11.0", "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.11.0.tgz", "integrity": "sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==", - "dev": true, "engines": { "node": ">= 0.6.0" } @@ -5700,7 +5685,6 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, "dependencies": { "to-regex-range": "^5.0.1" }, @@ -5869,7 +5853,6 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, "hasInstallScript": true, "optional": true, "os": [ @@ -5979,7 +5962,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, "dependencies": { "is-glob": "^4.0.1" }, @@ -6572,7 +6554,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, "dependencies": { "binary-extensions": "^2.0.0" }, @@ -6626,7 +6607,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -6643,7 +6623,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, "dependencies": { "is-extglob": "^2.1.1" }, @@ -6670,7 +6649,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, "engines": { "node": ">=0.12.0" } @@ -7531,7 +7509,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, "dependencies": { "yallist": "^4.0.0" }, @@ -7543,7 +7520,6 @@ "version": "0.25.7", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz", "integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==", - "dev": true, "dependencies": { "sourcemap-codec": "^1.4.4" } @@ -8149,7 +8125,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -8837,7 +8812,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, "engines": { "node": ">=8.6" }, @@ -9633,7 +9607,6 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, "dependencies": { "picomatch": "^2.2.1" }, @@ -9644,8 +9617,7 @@ "node_modules/reflect-metadata": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", - "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==", - "dev": true + "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==" }, "node_modules/regenerate": { "version": "1.4.2", @@ -10066,7 +10038,6 @@ "version": "7.3.5", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "dev": true, "dependencies": { "lru-cache": "^6.0.0" }, @@ -10451,8 +10422,7 @@ "node_modules/sourcemap-codec": { "version": "1.4.8", "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", - "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", - "dev": true + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==" }, "node_modules/spdy": { "version": "4.0.2", @@ -10853,7 +10823,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, "dependencies": { "is-number": "^7.0.0" }, @@ -10919,7 +10888,6 @@ "version": "4.5.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.5.tgz", "integrity": "sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==", - "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -11415,6 +11383,11 @@ "node": ">=0.8.0" } }, + "node_modules/websocket-ts": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/websocket-ts/-/websocket-ts-1.1.1.tgz", + "integrity": "sha512-rm+S60J74Ckw5iizzgID12ju+OfaHAa6dhXhULIOrXkl0e05RzxfY42/vMStpz5jWL3iz9mkyjPcFUY1IgI0fw==" + }, "node_modules/which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", @@ -11525,8 +11498,7 @@ "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/yaml": { "version": "1.10.2", @@ -11861,7 +11833,6 @@ "version": "13.2.5", "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-13.2.5.tgz", "integrity": "sha512-Xd8xj2Z0ilA4TJAM/JkTtA1CAa6SuebFsEEvabHCRO5MDvtdsIUP91ADUZIqDHy7qe6Qift/rAVN2PXxT2aaNA==", - "dev": true, "requires": { "@babel/core": "^7.17.2", "chokidar": "^3.0.0", @@ -11879,7 +11850,6 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.1.2.tgz", "integrity": "sha512-hoyByceqwKirw7w3Z7gnIIZC3Wx3J484Y3L/cMpXFbr7d9ZQj2mODrirNzcJa+SM3UlpWXYvKV4RlRpFXlWgXg==", - "dev": true, "requires": { "@jridgewell/trace-mapping": "^0.3.0" } @@ -11888,7 +11858,6 @@ "version": "7.17.5", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.17.5.tgz", "integrity": "sha512-/BBMw4EvjmyquN5O+t5eh0+YqB3XXJkYD2cjKpYtWOfFy4lQ4UozNSmxAcWT8r2XtZs0ewG+zrfsqeR15i1ajA==", - "dev": true, "requires": { "@ampproject/remapping": "^2.1.0", "@babel/code-frame": "^7.16.7", @@ -11910,8 +11879,7 @@ "semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" } } }, @@ -11919,7 +11887,6 @@ "version": "7.17.3", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.3.tgz", "integrity": "sha512-+R6Dctil/MgUsZsZAkYgK+ADNSZzJRRy0TvY65T71z/CR854xHQ1EweBYXdfT+HNeN7w0cSJJEzgxZMv40pxsg==", - "dev": true, "requires": { "@babel/types": "^7.17.0", "jsesc": "^2.5.1", @@ -11929,8 +11896,7 @@ "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" } } }, @@ -12075,7 +12041,6 @@ "version": "7.16.12", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.16.12.tgz", "integrity": "sha512-dK5PtG1uiN2ikk++5OzSYsitZKny4wOCD0nrO4TqnW4BVBTQ2NGS3NgilvT/TEyxTST7LNyWV/T4tXDoD3fOgg==", - "dev": true, "requires": { "@babel/code-frame": "^7.16.7", "@babel/generator": "^7.16.8", @@ -12097,14 +12062,12 @@ "semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" }, "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" } } }, @@ -12112,7 +12075,6 @@ "version": "7.16.8", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.16.8.tgz", "integrity": "sha512-1ojZwE9+lOXzcWdWmO6TbUzDfqLD39CmEhN8+2cX9XkDo5yW1OpgfejfliysR2AWLpMamTiOiAp/mtroaymhpw==", - "dev": true, "requires": { "@babel/types": "^7.16.8", "jsesc": "^2.5.1", @@ -12122,8 +12084,7 @@ "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" } } }, @@ -13281,7 +13242,8 @@ "version": "13.2.5", "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-13.2.5.tgz", "integrity": "sha512-obiPvwPe+UJUO8cfNbBxukLKG30F+gLF5/erexwklRknJzS4KP8ciH2on6XlTuXUahpDjbO0pffugFE2I/IszQ==", - "dev": true + "dev": true, + "requires": {} }, "@nodelib/fs.scandir": { "version": "2.1.5", @@ -13822,7 +13784,8 @@ "version": "1.8.0", "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz", "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==", - "dev": true + "dev": true, + "requires": {} }, "adjust-sourcemap-loader": { "version": "4.0.0", @@ -13945,7 +13908,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", - "dev": true, "requires": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -14143,8 +14105,7 @@ "binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", - "dev": true + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==" }, "bl": { "version": "4.1.0", @@ -14215,7 +14176,8 @@ "bootstrap": { "version": "5.1.3", "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.1.3.tgz", - "integrity": "sha512-fcQztozJ8jToQWXxVuEyXWW+dSo8AiXWKwiSSrKWsRB/Qt+Ewwza+JWoLKiTuQLaEPhdNAJ7+Dosc9DOIqNy7Q==" + "integrity": "sha512-fcQztozJ8jToQWXxVuEyXWW+dSo8AiXWKwiSSrKWsRB/Qt+Ewwza+JWoLKiTuQLaEPhdNAJ7+Dosc9DOIqNy7Q==", + "requires": {} }, "brace-expansion": { "version": "1.1.11", @@ -14230,7 +14192,6 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, "requires": { "fill-range": "^7.0.1" } @@ -14359,7 +14320,6 @@ "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "dev": true, "requires": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -14387,7 +14347,8 @@ "version": "5.2.2", "resolved": "https://registry.npmjs.org/circular-dependency-plugin/-/circular-dependency-plugin-5.2.2.tgz", "integrity": "sha512-g38K9Cm5WRwlaH6g03B9OEz/0qRizI+2I7n+Gz+L5DxXJAPAiWQvwlYNm1V1jkdpUv95bOe/ASm2vfi/G560jQ==", - "dev": true + "dev": true, + "requires": {} }, "clean-stack": { "version": "2.2.0", @@ -14865,7 +14826,8 @@ "version": "6.0.3", "resolved": "https://registry.npmjs.org/css-prefers-color-scheme/-/css-prefers-color-scheme-6.0.3.tgz", "integrity": "sha512-4BqMbZksRkJQx2zAjrokiGMd07RqOa2IxIrrN10lyBe9xhn9DEvjUK79J6jkeiv9D9hQFXKb6g1jwU62jziJZA==", - "dev": true + "dev": true, + "requires": {} }, "css-select": { "version": "4.2.1", @@ -15038,8 +15000,7 @@ "dependency-graph": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.11.0.tgz", - "integrity": "sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==", - "dev": true + "integrity": "sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==" }, "destroy": { "version": "1.0.4", @@ -15687,7 +15648,6 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, "requires": { "to-regex-range": "^5.0.1" } @@ -15810,7 +15770,6 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, "optional": true }, "function-bind": { @@ -15886,7 +15845,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, "requires": { "is-glob": "^4.0.1" } @@ -16143,7 +16101,8 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", - "dev": true + "dev": true, + "requires": {} }, "ieee754": { "version": "1.2.1", @@ -16340,7 +16299,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, "requires": { "binary-extensions": "^2.0.0" } @@ -16372,8 +16330,7 @@ "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" }, "is-fullwidth-code-point": { "version": "3.0.0", @@ -16384,7 +16341,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, "requires": { "is-extglob": "^2.1.1" } @@ -16404,8 +16360,7 @@ "is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" }, "is-path-cwd": { "version": "2.2.0", @@ -16824,7 +16779,8 @@ "version": "1.7.0", "resolved": "https://registry.npmjs.org/karma-jasmine-html-reporter/-/karma-jasmine-html-reporter-1.7.0.tgz", "integrity": "sha512-pzum1TL7j90DTE86eFt48/s12hqwQuiD+e5aXx2Dc9wDEn2LfGq6RoAxEZZjFiN0RDSCOnosEKRZWxbQ+iMpQQ==", - "dev": true + "dev": true, + "requires": {} }, "karma-source-map-support": { "version": "1.4.0", @@ -17046,7 +17002,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, "requires": { "yallist": "^4.0.0" } @@ -17055,7 +17010,6 @@ "version": "0.25.7", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz", "integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==", - "dev": true, "requires": { "sourcemap-codec": "^1.4.4" } @@ -17508,8 +17462,7 @@ "normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" }, "normalize-range": { "version": "0.1.2", @@ -18034,8 +17987,7 @@ "picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==" }, "pify": { "version": "2.3.0", @@ -18146,7 +18098,8 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-8.0.0.tgz", "integrity": "sha512-FvO2GzMUaTN0t1fBULDeIvxr5IvbDXcIatt6pnJghc736nqNgsGao5NT+5+WVLAQiTt6Cb3YUms0jiPaXhL//g==", - "dev": true + "dev": true, + "requires": {} }, "postcss-custom-properties": { "version": "12.1.4", @@ -18216,13 +18169,15 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/postcss-font-variant/-/postcss-font-variant-5.0.0.tgz", "integrity": "sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA==", - "dev": true + "dev": true, + "requires": {} }, "postcss-gap-properties": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/postcss-gap-properties/-/postcss-gap-properties-3.0.3.tgz", "integrity": "sha512-rPPZRLPmEKgLk/KlXMqRaNkYTUpE7YC+bOIQFN5xcu1Vp11Y4faIXv6/Jpft6FMnl6YRxZqDZG0qQOW80stzxQ==", - "dev": true + "dev": true, + "requires": {} }, "postcss-image-set-function": { "version": "4.0.6", @@ -18248,7 +18203,8 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/postcss-initial/-/postcss-initial-4.0.1.tgz", "integrity": "sha512-0ueD7rPqX8Pn1xJIjay0AZeIuDoF+V+VvMt/uOnn+4ezUKhZM/NokDeP6DwMNyIoYByuN/94IQnt5FEkaN59xQ==", - "dev": true + "dev": true, + "requires": {} }, "postcss-lab-function": { "version": "4.1.1", @@ -18275,19 +18231,22 @@ "version": "5.0.4", "resolved": "https://registry.npmjs.org/postcss-logical/-/postcss-logical-5.0.4.tgz", "integrity": "sha512-RHXxplCeLh9VjinvMrZONq7im4wjWGlRJAqmAVLXyZaXwfDWP73/oq4NdIp+OZwhQUMj0zjqDfM5Fj7qby+B4g==", - "dev": true + "dev": true, + "requires": {} }, "postcss-media-minmax": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/postcss-media-minmax/-/postcss-media-minmax-5.0.0.tgz", "integrity": "sha512-yDUvFf9QdFZTuCUg0g0uNSHVlJ5X1lSzDZjPSFaiCWvjgsvu8vEVxtahPrLMinIDEEGnx6cBe6iqdx5YWz08wQ==", - "dev": true + "dev": true, + "requires": {} }, "postcss-modules-extract-imports": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", - "dev": true + "dev": true, + "requires": {} }, "postcss-modules-local-by-default": { "version": "4.0.0", @@ -18331,13 +18290,15 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/postcss-overflow-shorthand/-/postcss-overflow-shorthand-3.0.3.tgz", "integrity": "sha512-CxZwoWup9KXzQeeIxtgOciQ00tDtnylYIlJBBODqkgS/PU2jISuWOL/mYLHmZb9ZhZiCaNKsCRiLp22dZUtNsg==", - "dev": true + "dev": true, + "requires": {} }, "postcss-page-break": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/postcss-page-break/-/postcss-page-break-3.0.4.tgz", "integrity": "sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ==", - "dev": true + "dev": true, + "requires": {} }, "postcss-place": { "version": "7.0.4", @@ -18402,7 +18363,8 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-4.0.0.tgz", "integrity": "sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw==", - "dev": true + "dev": true, + "requires": {} }, "postcss-selector-not": { "version": "5.0.0", @@ -18567,7 +18529,6 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, "requires": { "picomatch": "^2.2.1" } @@ -18575,8 +18536,7 @@ "reflect-metadata": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", - "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==", - "dev": true + "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==" }, "regenerate": { "version": "1.4.2", @@ -18852,7 +18812,8 @@ "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true + "dev": true, + "requires": {} }, "json-schema-traverse": { "version": "0.4.1", @@ -18881,7 +18842,6 @@ "version": "7.3.5", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "dev": true, "requires": { "lru-cache": "^6.0.0" } @@ -19197,8 +19157,7 @@ "sourcemap-codec": { "version": "1.4.8", "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", - "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", - "dev": true + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==" }, "spdy": { "version": "4.0.2", @@ -19406,7 +19365,8 @@ "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true + "dev": true, + "requires": {} }, "json-schema-traverse": { "version": "0.4.1", @@ -19480,7 +19440,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, "requires": { "is-number": "^7.0.0" } @@ -19527,8 +19486,7 @@ "typescript": { "version": "4.5.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.5.tgz", - "integrity": "sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==", - "dev": true + "integrity": "sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==" }, "ua-parser-js": { "version": "0.7.31", @@ -19718,7 +19676,8 @@ "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true + "dev": true, + "requires": {} }, "json-schema-traverse": { "version": "0.4.1", @@ -19874,6 +19833,11 @@ "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", "dev": true }, + "websocket-ts": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/websocket-ts/-/websocket-ts-1.1.1.tgz", + "integrity": "sha512-rm+S60J74Ckw5iizzgID12ju+OfaHAa6dhXhULIOrXkl0e05RzxfY42/vMStpz5jWL3iz9mkyjPcFUY1IgI0fw==" + }, "which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", @@ -19940,7 +19904,8 @@ "version": "8.2.3", "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz", "integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==", - "dev": true + "dev": true, + "requires": {} }, "y18n": { "version": "5.0.8", @@ -19950,8 +19915,7 @@ "yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "yaml": { "version": "1.10.2", diff --git a/frontend/package.json b/frontend/package.json index 678efec1..8cd6db58 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -36,6 +36,7 @@ "ngx-csv-parser": "^0.0.7", "rxjs": "~7.5.0", "tslib": "^2.3.0", + "websocket-ts": "^1.1.1", "zone.js": "~0.11.4" }, "devDependencies": { diff --git a/frontend/src/app/_data/Model.ts b/frontend/src/app/_data/Model.ts index 0768a374..07364564 100644 --- a/frontend/src/app/_data/Model.ts +++ b/frontend/src/app/_data/Model.ts @@ -26,7 +26,9 @@ export default class Model { public inputLayerActivationFunction: ActivationFunction = ActivationFunction.Sigmoid, public hiddenLayerActivationFunction: ActivationFunction = ActivationFunction.Sigmoid, public outputLayerActivationFunction: ActivationFunction = ActivationFunction.Sigmoid, - public username: string = '' + public username: string = '', + public nullValues: NullValueOptions = NullValueOptions.DeleteRows, + public nullValuesReplacers = [] ) { } } @@ -96,4 +98,16 @@ export enum Optimizer { SGD = 'SGD', SGDMomentum = 'SGDMomentum', RMSprop = 'RMSprop' +} + +export enum NullValueOptions { + DeleteRows = 'delete_rows', + DeleteColumns = 'delete_columns', + Replace = 'replace' +} + +export enum ReplaceWith { + None = '...', + Mean = 'Srednja vrednost', + Median = 'Medijana' }
\ No newline at end of file diff --git a/frontend/src/app/_elements/dataset-load/dataset-load.component.html b/frontend/src/app/_elements/dataset-load/dataset-load.component.html index 76fc40e2..b159c748 100644 --- a/frontend/src/app/_elements/dataset-load/dataset-load.component.html +++ b/frontend/src/app/_elements/dataset-load/dataset-load.component.html @@ -5,57 +5,28 @@ </div> <div class="col-3"> <label for="name" class="col-form-label">Naziv dataseta:</label> - <input type="text" class="form-control mb-3" name="name" placeholder="Naziv..." - [(ngModel)]="dataset.name"> - <label for="desc" class="col-sm-2 col-form-label">Opis:</label> - <div> - <textarea class="form-control" name="desc" rows="3" [(ngModel)]="dataset.description"></textarea> - </div> + <input type="text" class="form-control mb-3" name="name" placeholder="Naziv..." [(ngModel)]="dataset.name"> + <label for="desc" class="col-sm-2 col-form-label">Opis:</label> + <div> + <textarea class="form-control" name="desc" rows="3" [(ngModel)]="dataset.description"></textarea> + </div> </div> <div class="col-1"> </div> <div class="col-4 mt-4"> - <input list=delimiterOptions - placeholder="Izaberite ili ukucajte delimiter za .csv fajl" class="form-control mt-2" [(ngModel)]="delimiter" - (input)="update()"> + <input list=delimiterOptions placeholder="Izaberite ili ukucajte delimiter za .csv fajl" class="form-control mt-2" + [(ngModel)]="delimiter" (input)="update()"> <datalist id=delimiterOptions> <option *ngFor="let option of delimiterOptions">{{option}}</option> </datalist> <label for="type" class="form-check-label my-5">Da li .csv ima header? - <input class="mx-3 form-check-input" type="checkbox" (input)="update()" [(ngModel)]="hasHeader" type="checkbox" value="" id="checkboxHeader" checked> + <input class="mx-3 form-check-input" type="checkbox" (input)="update()" [(ngModel)]="hasHeader" type="checkbox" + value="" id="checkboxHeader" checked> </label> <br> - <input id="fileInput" class="form-control" type="file" class="upload" (change)="changeListener($event)" accept=".csv"> + <input id="fileInput" class="form-control" type="file" class="upload" (change)="changeListener($event)" + accept=".csv"> </div> </div> - - <div class="table-responsive" *ngIf="hasInput"> - <table *ngIf="csvRecords.length > 0 && hasHeader" class="table table-bordered table-light mt-4"> - <thead> - <tr> - <th *ngFor="let item of csvRecords[0]; let i = index">{{item}}</th> - </tr> - </thead> - <tbody> - <tr *ngFor="let row of csvRecords | slice:1:11"> - <td *ngFor="let col of row">{{col}}</td> - </tr> - </tbody> - </table> - - <table *ngIf="csvRecords.length > 0 && !hasHeader" class="table table-bordered table-light mt-4"> - <tbody> - <tr *ngFor="let row of csvRecords | slice:0:10"> - <td *ngFor="let col of row">{{col}}</td> - </tr> - </tbody> - </table> - </div> - - <div *ngIf="csvRecords.length > 0 && hasInput" id="info"> - . . . <br> - {{rowsNumber}} x {{colsNumber}} - </div> - </div>
\ No newline at end of file diff --git a/frontend/src/app/_elements/dataset-load/dataset-load.component.ts b/frontend/src/app/_elements/dataset-load/dataset-load.component.ts index bccf13b7..7f432b9a 100644 --- a/frontend/src/app/_elements/dataset-load/dataset-load.component.ts +++ b/frontend/src/app/_elements/dataset-load/dataset-load.component.ts @@ -15,7 +15,6 @@ export class DatasetLoadComponent { delimiterOptions: Array<string> = [",", ";", "\t", "razmak", "|"]; //podrazumevano "," hasHeader: boolean = true; - hasInput: boolean = false; csvRecords: any[] = []; @@ -39,10 +38,9 @@ export class DatasetLoadComponent { this.hasInput = false; return; } - else + else this.hasInput = true; - console.log(this.files); this.update(); } diff --git a/frontend/src/app/_elements/datatable/datatable.component.css b/frontend/src/app/_elements/datatable/datatable.component.css new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/frontend/src/app/_elements/datatable/datatable.component.css diff --git a/frontend/src/app/_elements/datatable/datatable.component.html b/frontend/src/app/_elements/datatable/datatable.component.html new file mode 100644 index 00000000..2c469ecc --- /dev/null +++ b/frontend/src/app/_elements/datatable/datatable.component.html @@ -0,0 +1,29 @@ +<div *ngIf="data"> + <div class="table-responsive"> + <table *ngIf="hasHeader" class="table table-bordered table-light mt-4"> + <thead> + <tr> + <th *ngFor="let item of data[0]; let i = index">{{item}}</th> + </tr> + </thead> + <tbody> + <tr *ngFor="let row of data | slice:1:11"> + <td *ngFor="let col of row">{{col}}</td> + </tr> + </tbody> + </table> + + <table *ngIf="data.length > 0 && !hasHeader" class="table table-bordered table-light mt-4"> + <tbody> + <tr *ngFor="let row of data | slice:0:10"> + <td *ngFor="let col of row">{{col}}</td> + </tr> + </tbody> + </table> + </div> + + <div id="info"> + . . . <br> + {{data.length}} x {{data[0].length}} + </div> +</div>
\ No newline at end of file diff --git a/frontend/src/app/_elements/datatable/datatable.component.spec.ts b/frontend/src/app/_elements/datatable/datatable.component.spec.ts new file mode 100644 index 00000000..3cf06160 --- /dev/null +++ b/frontend/src/app/_elements/datatable/datatable.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DatatableComponent } from './datatable.component'; + +describe('DatatableComponent', () => { + let component: DatatableComponent; + let fixture: ComponentFixture<DatatableComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ DatatableComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(DatatableComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/_elements/datatable/datatable.component.ts b/frontend/src/app/_elements/datatable/datatable.component.ts new file mode 100644 index 00000000..d3740d83 --- /dev/null +++ b/frontend/src/app/_elements/datatable/datatable.component.ts @@ -0,0 +1,19 @@ +import { Component, Input, OnInit } from '@angular/core'; + +@Component({ + selector: 'app-datatable', + templateUrl: './datatable.component.html', + styleUrls: ['./datatable.component.css'] +}) +export class DatatableComponent implements OnInit { + + @Input() hasHeader?: boolean = true; + + @Input() data?: any[] = []; + + constructor() { } + + ngOnInit(): void { + } + +} diff --git a/frontend/src/app/_pages/add-model/add-model.component.html b/frontend/src/app/_pages/add-model/add-model.component.html index 33066f80..afd4ceb4 100644 --- a/frontend/src/app/_pages/add-model/add-model.component.html +++ b/frontend/src/app/_pages/add-model/add-model.component.html @@ -3,9 +3,7 @@ </div> <div id="wrapper"> - <div id="container" class="container p-5" style="background-color: white; min-height: 100%;"> - <div class="form-group row mt-3 mb-2 d-flex justify-content-center"> <!--justify-content-center--> <h2 class="col-2"> Nov model: </h2> @@ -38,42 +36,39 @@ </button> <h3 class="mt-3 mx-3">ili</h3> <button type="button" id="btnNewDataset" class="btn" (click)="viewNewDatasetForm()" - [ngClass]="{'btnType1': !showMyDatasets, 'btnType2': showMyDatasets}"> + [ngClass]="{'btnType1': !showMyDatasets, 'btnType2': showMyDatasets}"> Dodajte novi dataset </button> </div> - <!-- POSTOJECI ILI NOVI DATASET --> - - <!-- POSTOJECI --> - <div class="px-5"> - <div *ngIf="showMyDatasets" class="overflow-auto" style="max-height: 500px;"> - <ul class="list-group"> - <li class="list-group-item p-3" *ngFor="let dataset of myDatasets" [ngClass]="{'selectedDatasetClass': this.selectedDataset == dataset}"> - <app-item-dataset name="usersDataset" [dataset]="dataset" (click)="selectThisDataset(dataset)"></app-item-dataset> - </li> - </ul> - </div> + <div class="px-5"> + <div *ngIf="showMyDatasets" class="overflow-auto" style="max-height: 500px;"> + <ul class="list-group"> + <li class="list-group-item p-3" *ngFor="let dataset of myDatasets" + [ngClass]="{'selectedDatasetClass': this.selectedDataset == dataset}"> + <app-item-dataset name="usersDataset" [dataset]="dataset" + (click)="selectThisDataset(dataset)"></app-item-dataset> + </li> + </ul> </div> - - <!-- NOVI --> - <app-dataset-load *ngIf="!showMyDatasets" id="dataset" (loaded)="datasetLoaded = true"></app-dataset-load> - </div> + </div> + <app-dataset-load *ngIf="!showMyDatasets" id="dataset" + (loaded)="datasetLoaded = true; selectedDataset = datasetLoadComponent?.dataset; datasetFile = datasetLoadComponent?.csvRecords; datasetHasHeader = datasetLoadComponent?.hasHeader"> + </app-dataset-load> + <app-datatable [data]="datasetFile" [hasHeader]="datasetHasHeader"></app-datatable> + </div> - <!-- ULAZNE/IZLAZNE KOLONE - POSTOJECI DATASET --> - <div *ngIf="showMyDatasets && this.selectedDataset" class="mt-4"> - <h2 class="text-center"> - Izabrali ste dataset: <span style="color: #003459; font-weight: bold">{{this.selectedDataset.name}}</span> - </h2> - <div class="row mt-5"> + <!-- ULAZNE/IZLAZNE KOLONE --> + <div *ngIf="selectedDataset"> + <div class="row"> <div class="col d-flex justify-content-center"> <h3>Izaberite ulazne kolone:</h3> <div id="divInputs" class="form-check mt-2"> <br> - <div *ngFor="let item of this.selectedDataset.header; let i = index"> - <input class="form-check-input" type="checkbox" value="{{item}}" - id="cb_{{item}}" name="cbsExisting" checked [disabled]="this.selectedOutputColumnVal == item"> + <div *ngFor="let item of selectedDataset.header; let i = index"> + <input class="form-check-input" type="checkbox" value="{{item}}" id="cb_{{item}}" + name="cbsNew" checked [disabled]="this.selectedOutputColumnVal == item"> <label class="form-check-label" for="cb_{{item}}"> {{item}} </label> @@ -84,45 +79,57 @@ <h3>Izaberite izlaznu kolonu:</h3> <div id="divOutputs" class="form-check mt-2"> <br> - <div *ngFor="let item of this.selectedDataset.header; let i = index"> - <input class="form-check-input" type="radio" value="{{item}}" - id="rb_{{item}}" name="rbsExisting" (change)="this.selectedOutputColumnVal = item"> + <div *ngFor="let item of selectedDataset.header; let i = index"> + <input class="form-check-input" type="radio" value="{{item}}" id="rb_{{item}}" name="rbsNew" + (change)="this.selectedOutputColumnVal = item"> <label class="form-check-label" for="rb_{{item}}"> {{item}} </label> </div> </div> </div> - </div> - </div> - <!-- ULAZNE/IZLAZNE KOLONE - NOVI DATASET--> - <div *ngIf="!showMyDatasets && datasetLoaded"> - <div *ngIf="datasetLoadComponent && datasetLoadComponent.files[0]" class="row"> - <div class="col d-flex justify-content-center"> - <h3>Izaberite ulazne kolone:</h3> - <div id="divInputs" class="form-check mt-2"> - <br> - <div *ngFor="let item of datasetLoadComponent.dataset.header; let i = index"> - <input class="form-check-input" type="checkbox" value="{{item}}" - id="cb_{{item}}" name="cbsNew" checked [disabled]="this.selectedOutputColumnVal == item"> - <label class="form-check-label" for="cb_{{item}}"> - {{item}} - </label> - </div> - </div> - </div> - <div class="col d-flex justify-content-left"> - <h3>Izaberite izlaznu kolonu:</h3> - <div id="divOutputs" class="form-check mt-2"> - <br> - <div *ngFor="let item of datasetLoadComponent.dataset.header; let i = index"> - <input class="form-check-input" type="radio" value="{{item}}" - id="rb_{{item}}" name="rbsNew" (change)="this.selectedOutputColumnVal = item"> - <label class="form-check-label" for="rb_{{item}}"> - {{item}} - </label> + <div class="my-2" *ngIf="datasetFile"> + <h2>Popunjavanje nedostajućih vrednosti:</h2> + <div class="form-check"> + <input type="radio" [(ngModel)]="newModel.nullValues" [value]="NullValueOptions.DeleteRows" + class="form-check-input" value="deleteRows" name="fillMissing" id="delRows" checked + data-bs-toggle="collapse" data-bs-target="#fillMissingCustom.show"> + <label for="delRows" class="form-check-label">Obriši + redove sa nedostajućim vrednostima</label><br> + <input type="radio" [(ngModel)]="newModel.nullValues" [value]="NullValueOptions.DeleteColumns" + class="form-check-input" value="deleteCols" name="fillMissing" id="delCols" + data-bs-toggle="collapse" data-bs-target="#fillMissingCustom.show"> + <label for="delCols" class="form-check-label">Obriši + kolone sa nedostajućim vrednostima</label><br> + <input type="radio" [(ngModel)]="newModel.nullValues" [value]="NullValueOptions.Replace" + class="form-check-input" name="fillMissing" id="replace" data-bs-toggle="collapse" + data-bs-target="#fillMissingCustom:not(.show)"> + <label for="replace" class="form-check-label">Izabraću + vrednosti koje će da zamene nedostajuće vrednosti za svaku kolonu...</label><br><br> + <div class="collapse" id="fillMissingCustom"> + <div> + <label for="columnReplacers" class="form-label">Unesite zamenu za svaku kolonu:</label> + <div id="columnReplacers"> + <div *ngFor="let column of selectedDataset.header; let i = index" class="my-3"> + <div class="input-group row" *ngIf="getInputById('cb_'+column).checked"> + <span class="input-group-text col-4 text-center"> + {{column}} + </span> + <input type="text" class="form-control col-4"> + <select [id]="'replaceOptions'+i" class="form-control col-4" + *ngIf="isNumber(datasetFile[1][i])"> + <option + *ngFor="let option of Object.keys(ReplaceWith); let optionName of Object.values(ReplaceWith)" + [value]="option"> + {{ optionName }} + </option> + </select> + </div> + </div> + </div> + </div> </div> </div> </div> @@ -257,7 +264,8 @@ </div> <div class="col-5"> <label for="splitYesNo" class="form-check-label">Podela test skupa: - <input id="splitYesNo" class="form-check-input" type="checkbox" [checked]="newModel.randomTestSet" + <input id="splitYesNo" class="form-check-input" type="checkbox" + [checked]="newModel.randomTestSet" (change)="newModel.randomTestSet = !newModel.randomTestSet"> </label> </div> @@ -288,8 +296,8 @@ <label for="percentage" class="form-label">Procenat podataka koji se uzima za trening skup:</label> </div> <div class="col-1"> - <input id="percentage" type="number" class="form-control" min="10" max="90" step="10" value="90" [(ngModel)]="tempTestSetDistribution" - [disabled] = "!newModel.randomTestSet"> + <input id="percentage" type="number" class="form-control" min="10" max="90" step="10" value="90" + [(ngModel)]="tempTestSetDistribution" [disabled]="!newModel.randomTestSet"> </div> </div> diff --git a/frontend/src/app/_pages/add-model/add-model.component.ts b/frontend/src/app/_pages/add-model/add-model.component.ts index 7bfb7204..1c9198a3 100644 --- a/frontend/src/app/_pages/add-model/add-model.component.ts +++ b/frontend/src/app/_pages/add-model/add-model.component.ts @@ -1,10 +1,11 @@ import { Component, OnInit, ViewChild } from '@angular/core'; -import Model from 'src/app/_data/Model'; -import { ANNType, Encoding, ActivationFunction, LossFunction, Optimizer } from 'src/app/_data/Model'; +import Model, { ReplaceWith } from 'src/app/_data/Model'; +import { ANNType, Encoding, ActivationFunction, LossFunction, Optimizer, NullValueOptions } from 'src/app/_data/Model'; import { DatasetLoadComponent } from 'src/app/_elements/dataset-load/dataset-load.component'; import { ModelsService } from 'src/app/_services/models.service'; import shared from 'src/app/Shared'; import Dataset from 'src/app/_data/Dataset'; +import { DatatableComponent } from 'src/app/_elements/datatable/datatable.component'; @Component({ @@ -15,6 +16,7 @@ import Dataset from 'src/app/_data/Dataset'; export class AddModelComponent implements OnInit { @ViewChild(DatasetLoadComponent) datasetLoadComponent?: DatasetLoadComponent; + @ViewChild(DatatableComponent) datatable?: DatatableComponent; datasetLoaded: boolean = false; newModel: Model; @@ -24,7 +26,10 @@ export class AddModelComponent implements OnInit { ActivationFunction = ActivationFunction; LossFunction = LossFunction; Optimizer = Optimizer; + NullValueOptions = NullValueOptions; + ReplaceWith = ReplaceWith; Object = Object; + document = document; shared = shared; selectedOutputColumnVal: string = ''; @@ -33,6 +38,8 @@ export class AddModelComponent implements OnInit { myDatasets?: Dataset[]; existingDatasetSelected: boolean = false; selectedDataset?: Dataset; + datasetFile?: any[]; + datasetHasHeader?: boolean = true; tempTestSetDistribution: number = 90; @@ -62,9 +69,9 @@ export class AddModelComponent implements OnInit { addModel() { if (!this.showMyDatasets) - this.saveModelWithNewDataset(); + this.saveModelWithNewDataset(); else - this.saveModelWithExistingDataset(); + this.saveModelWithExistingDataset(); } trainModel() { @@ -124,14 +131,14 @@ export class AddModelComponent implements OnInit { if (this.selectedDataset) { //dataset je izabran this.getCheckedInputCols(); this.getCheckedOutputCol(); - - if (this.validationInputsOutput()) { + + if (this.validationInputsOutput()) { this.newModel.datasetId = this.selectedDataset._id; - + this.newModel.randomTestSetDistribution = 1 - Math.round(this.tempTestSetDistribution / 100 * 10) / 10; this.tempTestSetDistribution = 90; this.newModel.username = shared.username; - + this.models.addModel(this.newModel).subscribe((response) => { console.log('ADD MODEL: DONE! REPLY:\n', response); }, (error) => { @@ -147,11 +154,9 @@ export class AddModelComponent implements OnInit { getCheckedInputCols() { this.newModel.inputColumns = []; let checkboxes: any; - if (this.showMyDatasets) - checkboxes = document.getElementsByName("cbsExisting"); - else - checkboxes = document.getElementsByName("cbsNew"); - + + checkboxes = document.getElementsByName("cbsNew"); + for (let i = 0; i < checkboxes.length; i++) { let thatCb = <HTMLInputElement>checkboxes[i]; if (thatCb.checked == true && thatCb.disabled == false) @@ -162,10 +167,8 @@ export class AddModelComponent implements OnInit { getCheckedOutputCol() { this.newModel.columnToPredict = ''; let radiobuttons: any; - if (this.showMyDatasets) - radiobuttons = document.getElementsByName("rbsExisting"); - else - radiobuttons = document.getElementsByName("rbsNew"); + + radiobuttons = document.getElementsByName("rbsNew"); for (let i = 0; i < radiobuttons.length; i++) { let thatRb = <HTMLInputElement>radiobuttons[i]; @@ -208,6 +211,10 @@ export class AddModelComponent implements OnInit { if (datasets[i]._id == dataset._id) }*/ + + //this.datasetFile = csvRecords; + this.datasetHasHeader = false; + this.resetCbsAndRbs(); } @@ -223,17 +230,8 @@ export class AddModelComponent implements OnInit { } checkAllCbs() { let checkboxes: any; - //if (this.showMyDatasets) - checkboxes = document.getElementsByName("cbsExisting"); - //else - //checkboxes = document.getElementsByName("cbsNew"); - - for (let i = 0; i < checkboxes.length; i++) { - (<HTMLInputElement>checkboxes[i]).checked = true; - (<HTMLInputElement>checkboxes[i]).disabled = false; - } - checkboxes = document.getElementsByName("cbsNew"); + checkboxes = document.getElementsByName("cbsNew"); for (let i = 0; i < checkboxes.length; i++) { (<HTMLInputElement>checkboxes[i]).checked = true; (<HTMLInputElement>checkboxes[i]).disabled = false; @@ -242,16 +240,10 @@ export class AddModelComponent implements OnInit { uncheckRbs() { this.selectedOutputColumnVal = ''; let radiobuttons: any; - //if (this.showMyDatasets) - radiobuttons = document.getElementsByName("rbsExisting"); - //else - //radiobuttons = document.getElementsByName("rbsNew"); + radiobuttons = document.getElementsByName("rbsNew"); for (let i = 0; i < radiobuttons.length; i++) (<HTMLInputElement>radiobuttons[i]).checked = false; - radiobuttons = document.getElementsByName("rbsNew"); - for (let i = 0; i < radiobuttons.length; i++) - (<HTMLInputElement>radiobuttons[i]).checked = false; } refreshMyDatasetList() { @@ -260,4 +252,14 @@ export class AddModelComponent implements OnInit { }); } + isNumber(value: string | number): boolean { + return ((value != null) && + (value !== '') && + !isNaN(Number(value.toString()))); + } + + + getInputById(id: string): HTMLInputElement { + return document.getElementById(id) as HTMLInputElement; + } } diff --git a/frontend/src/app/_services/web-socket.service.spec.ts b/frontend/src/app/_services/web-socket.service.spec.ts new file mode 100644 index 00000000..a86aeca7 --- /dev/null +++ b/frontend/src/app/_services/web-socket.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { WebSocketService } from './web-socket.service'; + +describe('WebSocketService', () => { + let service: WebSocketService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(WebSocketService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/_services/web-socket.service.ts b/frontend/src/app/_services/web-socket.service.ts new file mode 100644 index 00000000..890ada6b --- /dev/null +++ b/frontend/src/app/_services/web-socket.service.ts @@ -0,0 +1,39 @@ +import { Injectable } from '@angular/core'; +import { ConstantBackoff, Websocket, WebsocketBuilder } from 'websocket-ts'; +import { API_SETTINGS } from 'src/config'; + +@Injectable({ + providedIn: 'root' +}) +export class WebSocketService { + + ws?: Websocket; + + private handlers: Function[] = []; + + constructor() { + this.ws = new WebsocketBuilder(API_SETTINGS.apiWSUrl) + .withBackoff(new ConstantBackoff(30000)) + .onOpen((i, e) => { console.log('WS: Connected to ' + API_SETTINGS.apiWSUrl) }) + .onMessage((i, e) => { + console.log('WS MESSAGE: ', e.data); + this.handlers.forEach(handler => { + handler(e.data); + }) + }) + .onClose((i, e) => { console.log('WS: Connection closed!') }) + .build(); + } + + send(msg: string) { + this.ws?.send(msg); + } + + addHandler(handler: Function) { + this.handlers.push(handler); + } + + removeHandler(handler: Function) { + this.handlers.splice(this.handlers.indexOf(handler), 1); + } +} diff --git a/frontend/src/app/app.component.html b/frontend/src/app/app.component.html index 2ec16fc2..f44a6d00 100644 --- a/frontend/src/app/app.component.html +++ b/frontend/src/app/app.component.html @@ -3,4 +3,5 @@ <router-outlet></router-outlet> <!--<app-barchart></app-barchart> <app-scatterchart></app-scatterchart>--> -</div>
\ No newline at end of file +</div> +<app-notifications></app-notifications>
\ No newline at end of file diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 9d2bbc26..4612e3a7 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -6,8 +6,8 @@ import { HttpClientModule } from '@angular/common/http'; import { MatSliderModule } from '@angular/material/slider'; import { MatIconModule } from '@angular/material/icon'; -import {NgChartsModule} from 'ng2-charts'; -import { Ng2SearchPipe, Ng2SearchPipeModule } from 'ng2-search-filter'; +import { NgChartsModule } from 'ng2-charts'; +import { Ng2SearchPipeModule } from 'ng2-search-filter'; import { AppComponent } from './app.component'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { DatasetLoadComponent } from './_elements/dataset-load/dataset-load.component'; @@ -33,6 +33,8 @@ import { BrowsePredictorsComponent } from './_pages/browse-predictors/browse-pre import { PredictComponent } from './_pages/predict/predict.component'; import { ScatterchartComponent } from './scatterchart/scatterchart.component'; import { BarchartComponent } from './barchart/barchart.component'; +import { NotificationsComponent } from './_elements/notifications/notifications.component'; +import { DatatableComponent } from './_elements/datatable/datatable.component'; import { FilterDatasetsComponent } from './_pages/filter-datasets/filter-datasets.component'; @NgModule({ @@ -57,6 +59,8 @@ import { FilterDatasetsComponent } from './_pages/filter-datasets/filter-dataset PredictComponent, ScatterchartComponent, BarchartComponent, + NotificationsComponent, + DatatableComponent, FilterDatasetsComponent ], imports: [ diff --git a/frontend/src/config.ts b/frontend/src/config.ts index 8c48672e..f1c14194 100644 --- a/frontend/src/config.ts +++ b/frontend/src/config.ts @@ -1,3 +1,4 @@ export const API_SETTINGS = { - apiURL: 'http://localhost:5283/api' + apiURL: 'http://localhost:5283/api', + apiWSUrl: 'ws://localhost:5283/api/websocket/ws' }
\ No newline at end of file diff --git a/frontend/src/index.html b/frontend/src/index.html index 0079969e..1461c9ae 100644 --- a/frontend/src/index.html +++ b/frontend/src/index.html @@ -1,5 +1,6 @@ <!doctype html> <html lang="en"> + <head> <meta charset="utf-8"> <title>Frontend</title> @@ -10,8 +11,9 @@ <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"> </head> + <body class="mat-typography"> <app-root></app-root> - <script src="node_modules/chart.js/src/chart.js"></script> </body> -</html> + +</html>
\ No newline at end of file |