From 86008abd269905d759c162182c1c6f924c15f53b Mon Sep 17 00:00:00 2001 From: andchiind Date: Thu, 8 Jun 2023 14:34:29 +0200 Subject: [PATCH 01/10] Add MissionDefinition/MissionRun change --- backend/api.test/EndpointTest.cs | 55 +-- .../EventHandlers/TestMissionScheduler.cs | 18 +- backend/api.test/Mocks/EchoServiceMock.cs | 11 +- backend/api.test/Mocks/IsarServiceMock.cs | 2 +- backend/api.test/Mocks/MapServiceMock.cs | 2 +- backend/api.test/Mocks/RobotControllerMock.cs | 10 +- backend/api.test/Services/MissionService.cs | 16 +- backend/api/Controllers/AreaController.cs | 239 +++++++++++++ .../api/Controllers/AssetDeckController.cs | 256 -------------- backend/api/Controllers/EchoController.cs | 4 +- backend/api/Controllers/MissionController.cs | 316 ++++++++++++++++-- ...ition.cs => CondensedMissionDefinition.cs} | 2 +- .../api/Controllers/Models/CreateAreaQuery.cs | 14 + .../Models/CreateAssetDeckQuery.cs | 2 +- .../Controllers/Models/CustomMissionQuery.cs | 4 + .../MissionDefinitionQueryStringParameters.cs | 33 ++ ....cs => MissionRunQueryStringParameters.cs} | 10 +- .../Models/RescheduleMissionQuery.cs | 12 + .../ScheduleLocalizationMissionQuery.cs | 2 +- .../Models/ScheduledMissionQuery.cs | 2 + backend/api/Controllers/RobotController.cs | 72 ++-- .../api/Database/Context/FlotillaDbContext.cs | 25 +- backend/api/Database/Context/InitDb.cs | 182 ++++++++-- backend/api/Database/Models/Area.cs | 48 +++ backend/api/Database/Models/Asset.cs | 21 ++ backend/api/Database/Models/AssetDeck.cs | 21 +- backend/api/Database/Models/Deck.cs | 21 ++ backend/api/Database/Models/Installation.cs | 24 ++ .../api/Database/Models/MissionDefinition.cs | 32 ++ .../Models/{Mission.cs => MissionRun.cs} | 7 +- backend/api/Database/Models/Robot.cs | 4 +- backend/api/Database/Models/RobotModel.cs | 2 +- backend/api/Database/Models/Source.cs | 24 ++ .../IsarConnectionEventHandler.cs | 4 +- backend/api/EventHandlers/MissionScheduler.cs | 10 +- backend/api/EventHandlers/MqttEventHandler.cs | 10 +- backend/api/Options/StorageOptions.cs | 8 + backend/api/Program.cs | 7 +- backend/api/Services/AreaService.cs | 233 +++++++++++++ backend/api/Services/AssetDeckService.cs | 140 -------- backend/api/Services/EchoService.cs | 37 +- backend/api/Services/IsarService.cs | 4 +- backend/api/Services/MapService.cs | 4 +- .../api/Services/MissionDefinitionService.cs | 226 +++++++++++++ ...MissionService.cs => MissionRunService.cs} | 89 ++--- .../Services/Models/IsarMissionDefinition.cs | 6 +- backend/api/Services/RobotService.cs | 2 +- backend/api/Services/SourceService.cs | 100 ++++++ backend/api/appsettings.Development.json | 4 + backend/api/appsettings.Production.json | 4 + backend/api/appsettings.Staging.json | 4 + backend/api/appsettings.Test.json | 4 + flotilla.code-workspace | 28 ++ frontend/src/api/ApiCaller.tsx | 58 +++- .../FailedMissionAlertView.tsx | 2 +- .../MissionOverview/MissionQueueView.tsx | 2 +- .../MissionOverview/OngoingMissionView.tsx | 2 +- .../MissionHistoryPage/MissionHistoryView.tsx | 2 +- .../MissionDefinitionQueryParameters.ts | 14 + ...meters.ts => MissionRunQueryParameters.ts} | 3 +- 60 files changed, 1837 insertions(+), 663 deletions(-) create mode 100644 backend/api/Controllers/AreaController.cs delete mode 100644 backend/api/Controllers/AssetDeckController.cs rename backend/api/Controllers/Models/{MissionDefinition.cs => CondensedMissionDefinition.cs} (82%) create mode 100644 backend/api/Controllers/Models/CreateAreaQuery.cs create mode 100644 backend/api/Controllers/Models/MissionDefinitionQueryStringParameters.cs rename backend/api/Controllers/Models/{MissionQueryStringParameters.cs => MissionRunQueryStringParameters.cs} (91%) create mode 100644 backend/api/Controllers/Models/RescheduleMissionQuery.cs create mode 100644 backend/api/Database/Models/Area.cs create mode 100644 backend/api/Database/Models/Asset.cs create mode 100644 backend/api/Database/Models/Deck.cs create mode 100644 backend/api/Database/Models/Installation.cs create mode 100644 backend/api/Database/Models/MissionDefinition.cs rename backend/api/Database/Models/{Mission.cs => MissionRun.cs} (98%) create mode 100644 backend/api/Database/Models/Source.cs create mode 100644 backend/api/Options/StorageOptions.cs create mode 100644 backend/api/Services/AreaService.cs delete mode 100644 backend/api/Services/AssetDeckService.cs create mode 100644 backend/api/Services/MissionDefinitionService.cs rename backend/api/Services/{MissionService.cs => MissionRunService.cs} (80%) create mode 100644 backend/api/Services/SourceService.cs create mode 100644 flotilla.code-workspace create mode 100644 frontend/src/models/MissionDefinitionQueryParameters.ts rename frontend/src/models/{MissionQueryParameters.ts => MissionRunQueryParameters.ts} (88%) diff --git a/backend/api.test/EndpointTest.cs b/backend/api.test/EndpointTest.cs index 623b614d0..253b92d6f 100644 --- a/backend/api.test/EndpointTest.cs +++ b/backend/api.test/EndpointTest.cs @@ -106,9 +106,9 @@ public void Dispose() [Fact] public async Task MissionsTest() { - string url = "/missions"; + string url = "/missions/runs"; var response = await _client.GetAsync(url); - var missions = await response.Content.ReadFromJsonAsync>( + var missions = await response.Content.ReadFromJsonAsync>( _serializerOptions ); Assert.True(response.IsSuccessStatusCode); @@ -119,7 +119,7 @@ public async Task MissionsTest() public async Task GetMissionById_ShouldReturnNotFound() { string missionId = "RandomString"; - string url = "/missions/" + missionId; + string url = "/missions/runs/" + missionId; var response = await _client.GetAsync(url); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } @@ -128,7 +128,7 @@ public async Task GetMissionById_ShouldReturnNotFound() public async Task DeleteMission_ShouldReturnNotFound() { string missionId = "RandomString"; - string url = "/missions/" + missionId; + string url = "/missions/runs/" + missionId; var response = await _client.DeleteAsync(url); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } @@ -190,7 +190,8 @@ public async Task StartMissionTest() var query = new ScheduledMissionQuery { RobotId = robotId, - AssetCode = "test", + AssetCode = "JSV", + AreaName = "testArea", EchoMissionId = 95, DesiredStartTime = DateTimeOffset.UtcNow }; @@ -203,19 +204,21 @@ public async Task StartMissionTest() // Assert Assert.True(response.IsSuccessStatusCode); - var mission = await response.Content.ReadFromJsonAsync(_serializerOptions); + var mission = await response.Content.ReadFromJsonAsync(_serializerOptions); Assert.True(mission != null); Assert.True(mission.Id != null); Assert.True(mission.Status == MissionStatus.Pending); } [Fact] - public async Task AssetDeckTest() + public async Task AreaTest() { // Arrange - string testAsset = "testAsset"; - string testDeck = "testDeck"; - string assetDeckUrl = $"/asset-decks"; + string testAsset = "TestAsset"; + string testInstallation = "TestInstallation"; + string testDeck = "testDeck2"; + string testArea = "testArea"; + string areaUrl = $"/areas"; var testPose = new Pose { Position = new Position @@ -233,10 +236,12 @@ public async Task AssetDeckTest() } }; - var query = new CreateAssetDeckQuery + var query = new CreateAreaQuery { AssetCode = testAsset, + InstallationName = testInstallation, DeckName = testDeck, + AreaName = testArea, DefaultLocalizationPose = testPose }; @@ -247,12 +252,12 @@ public async Task AssetDeckTest() ); // Act - var assetDeckResponse = await _client.PostAsync(assetDeckUrl, content); + var areaResponse = await _client.PostAsync(areaUrl, content); // Assert - Assert.True(assetDeckResponse.IsSuccessStatusCode); - var assetDeck = await assetDeckResponse.Content.ReadFromJsonAsync(_serializerOptions); - Assert.True(assetDeck != null); + Assert.True(areaResponse.IsSuccessStatusCode); + var area = await areaResponse.Content.ReadFromJsonAsync(_serializerOptions); + Assert.True(area != null); } [Fact] @@ -260,8 +265,10 @@ public async Task SafePositionTest() { // Arrange - Add Safe Position string testAsset = "testAsset"; + string testInstallation = "testInstallation"; string testDeck = "testDeck"; - string addSafePositionUrl = $"/asset-decks/{testAsset}/{testDeck}/safe-position"; + string testArea = "testArea"; + string addSafePositionUrl = $"/areas/{testAsset}/{testInstallation}/{testDeck}/{testArea}/safe-position"; var testPosition = new Position { X = 1, @@ -284,10 +291,10 @@ public async Task SafePositionTest() null, "application/json" ); - var assetDeckResponse = await _client.PostAsync(addSafePositionUrl, content); - Assert.True(assetDeckResponse.IsSuccessStatusCode); - var assetDeck = await assetDeckResponse.Content.ReadFromJsonAsync(_serializerOptions); - Assert.True(assetDeck != null); + var areaResponse = await _client.PostAsync(addSafePositionUrl, content); + Assert.True(areaResponse.IsSuccessStatusCode); + var area = await areaResponse.Content.ReadFromJsonAsync(_serializerOptions); + Assert.True(area != null); // Arrange - Get a Robot string url = "/robots"; @@ -299,12 +306,12 @@ public async Task SafePositionTest() string robotId = robot.Id; // Act - string goToSafePositionUrl = $"/robots/{robotId}/{testAsset}/{testDeck}/go-to-safe-position"; + string goToSafePositionUrl = $"/robots/{robotId}/{testAsset}/{testArea}/go-to-safe-position"; var missionResponse = await _client.PostAsync(goToSafePositionUrl, null); // Assert Assert.True(missionResponse.IsSuccessStatusCode); - var mission = await missionResponse.Content.ReadFromJsonAsync(_serializerOptions); + var mission = await missionResponse.Content.ReadFromJsonAsync(_serializerOptions); Assert.True(mission != null); Assert.True( JsonSerializer.Serialize(mission.Tasks[0].RobotPose.Position) == @@ -322,8 +329,8 @@ public async Task GetMapMetadata() foreach (string input in inputOutputPairs.Keys) { - string assetDeckId = input; - string url = $"/asset-decks/{assetDeckId}/map-metadata"; + string areaId = input; + string url = $"/areas/{areaId}/map-metadata"; var response = await _client.GetAsync(url); Assert.Equal(inputOutputPairs[input], response.StatusCode); diff --git a/backend/api.test/EventHandlers/TestMissionScheduler.cs b/backend/api.test/EventHandlers/TestMissionScheduler.cs index b67c9e4d8..cadf4a15a 100644 --- a/backend/api.test/EventHandlers/TestMissionScheduler.cs +++ b/backend/api.test/EventHandlers/TestMissionScheduler.cs @@ -22,24 +22,24 @@ namespace Api.Test.EventHandlers [Collection("Database collection")] public class TestMissionScheduler : IDisposable { - private static Mission ScheduledMission => + private static MissionRun ScheduledMission => new() { Name = "testMission", - EchoMissionId = 2, + MissionId = Guid.NewGuid().ToString(), Status = MissionStatus.Pending, DesiredStartTime = DateTimeOffset.Now, - AssetCode = "TestAsset", MapMetadata = new MapMetadata() { MapName = "TestMap", Boundary = new(), TransformationMatrices = new() - } + }, + Area = new Area() }; private readonly MissionScheduler _scheduledMissionEventHandler; - private readonly IMissionService _missionService; + private readonly IMissionRunService _missionService; private readonly IRobotService _robotService; private readonly RobotControllerMock _robotControllerMock; private readonly FlotillaDbContext _context; @@ -49,11 +49,11 @@ public TestMissionScheduler(DatabaseFixture fixture) // Using Moq https://github.com/moq/moq4 var schedulerLogger = new Mock>().Object; - var missionLogger = new Mock>().Object; + var missionLogger = new Mock>().Object; // Mock ScheduledMissionService: _context = fixture.NewContext; - _missionService = new MissionService(_context, missionLogger); + _missionService = new MissionRunService(_context, missionLogger); _robotService = new RobotService(_context); _robotControllerMock = new RobotControllerMock(); @@ -61,7 +61,7 @@ public TestMissionScheduler(DatabaseFixture fixture) // Mock injection of MissionService: mockServiceProvider - .Setup(p => p.GetService(typeof(IMissionService))) + .Setup(p => p.GetService(typeof(IMissionRunService))) .Returns(_missionService); // Mock injection of RobotService: mockServiceProvider @@ -105,7 +105,7 @@ public void Dispose() private async void AssertExpectedStatusChange( MissionStatus preStatus, MissionStatus postStatus, - Mission mission + MissionRun mission ) { // ARRANGE diff --git a/backend/api.test/Mocks/EchoServiceMock.cs b/backend/api.test/Mocks/EchoServiceMock.cs index ef50014e0..4331eb18b 100644 --- a/backend/api.test/Mocks/EchoServiceMock.cs +++ b/backend/api.test/Mocks/EchoServiceMock.cs @@ -19,17 +19,17 @@ public class MockEchoService : IEchoService Tags = new List() }; - public MissionDefinition MockMissionDefinition = + public CondensedMissionDefinition MockMissionDefinition = new() { EchoMissionId = 1, Name = "test", }; - public async Task> GetAvailableMissions(string? installationCode) + public async Task> GetAvailableMissions(string? installationCode) { await Task.Run(() => Thread.Sleep(1)); - return new List(new MissionDefinition[] { MockMissionDefinition }); + return new List(new CondensedMissionDefinition[] { MockMissionDefinition }); } public async Task GetMissionById(int missionId) @@ -48,5 +48,10 @@ public async Task GetRobotPoseFromPoseId(int poseId) await Task.Run(() => Thread.Sleep(1)); return new EchoPoseResponse(); } + + public Task GetMissionByPath(string relativePath) + { + throw new NotImplementedException(); + } } } diff --git a/backend/api.test/Mocks/IsarServiceMock.cs b/backend/api.test/Mocks/IsarServiceMock.cs index 6194c27c4..ec928e02a 100644 --- a/backend/api.test/Mocks/IsarServiceMock.cs +++ b/backend/api.test/Mocks/IsarServiceMock.cs @@ -9,7 +9,7 @@ namespace Api.Test.Mocks { public class MockIsarService : IIsarService { - public async Task StartMission(Robot robot, Mission mission) + public async Task StartMission(Robot robot, MissionRun mission) { await Task.Run(() => Thread.Sleep(1)); var isarServiceMissionResponse = new IsarMission( diff --git a/backend/api.test/Mocks/MapServiceMock.cs b/backend/api.test/Mocks/MapServiceMock.cs index 70f352339..808f71db3 100644 --- a/backend/api.test/Mocks/MapServiceMock.cs +++ b/backend/api.test/Mocks/MapServiceMock.cs @@ -15,7 +15,7 @@ public class MockMapService : IMapService return new MapMetadata(); } - public async Task AssignMapToMission(Mission mission) + public async Task AssignMapToMission(MissionRun mission) { await Task.Run(() => Thread.Sleep(1)); } diff --git a/backend/api.test/Mocks/RobotControllerMock.cs b/backend/api.test/Mocks/RobotControllerMock.cs index 6cdf207c5..33d097a2d 100644 --- a/backend/api.test/Mocks/RobotControllerMock.cs +++ b/backend/api.test/Mocks/RobotControllerMock.cs @@ -10,17 +10,17 @@ internal class RobotControllerMock public Mock IsarServiceMock; public Mock RobotServiceMock; public Mock RobotModelServiceMock; - public Mock MissionServiceMock; + public Mock MissionServiceMock; public Mock Mock; - public Mock AssetDeckServiceMock; + public Mock AreaServiceMock; public RobotControllerMock() { - MissionServiceMock = new Mock(); + MissionServiceMock = new Mock(); IsarServiceMock = new Mock(); RobotServiceMock = new Mock(); RobotModelServiceMock = new Mock(); - AssetDeckServiceMock = new Mock(); + AreaServiceMock = new Mock(); var mockLoggerController = new Mock>(); @@ -30,7 +30,7 @@ public RobotControllerMock() IsarServiceMock.Object, MissionServiceMock.Object, RobotModelServiceMock.Object, - AssetDeckServiceMock.Object + AreaServiceMock.Object ) { CallBase = true diff --git a/backend/api.test/Services/MissionService.cs b/backend/api.test/Services/MissionService.cs index 22e54d3f4..1872dec6e 100644 --- a/backend/api.test/Services/MissionService.cs +++ b/backend/api.test/Services/MissionService.cs @@ -15,14 +15,14 @@ namespace Api.Test.Services public class MissionServiceTest : IDisposable { private readonly FlotillaDbContext _context; - private readonly ILogger _logger; - private readonly MissionService _missionService; + private readonly ILogger _logger; + private readonly MissionRunService _missionService; public MissionServiceTest(DatabaseFixture fixture) { _context = fixture.NewContext; - _logger = new Mock>().Object; - _missionService = new MissionService(_context, _logger); + _logger = new Mock>().Object; + _missionService = new MissionRunService(_context, _logger); } public void Dispose() @@ -43,22 +43,22 @@ public async Task Create() { var robot = _context.Robots.First(); int nReportsBefore = _missionService - .ReadAll(new MissionQueryStringParameters()) + .ReadAll(new MissionRunQueryStringParameters()) .Result.Count; - Mission mission = + MissionRun mission = new() { Name = "testMission", Robot = robot, MapMetadata = new MapMetadata() { MapName = "testMap" }, - AssetCode = "testAsset", + Area = new Area(), DesiredStartTime = DateTime.Now }; await _missionService.Create(mission); int nReportsAfter = _missionService - .ReadAll(new MissionQueryStringParameters()) + .ReadAll(new MissionRunQueryStringParameters()) .Result.Count; Assert.Equal(nReportsBefore + 1, nReportsAfter); diff --git a/backend/api/Controllers/AreaController.cs b/backend/api/Controllers/AreaController.cs new file mode 100644 index 000000000..9db513838 --- /dev/null +++ b/backend/api/Controllers/AreaController.cs @@ -0,0 +1,239 @@ +using Api.Controllers.Models; +using Api.Database.Models; +using Api.Services; +using Api.Utilities; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Api.Controllers +{ + [ApiController] + [Route("areas")] + public class AreaController : ControllerBase + { + private readonly IAreaService _areaService; + + private readonly IMapService _mapService; + + private readonly ILogger _logger; + + public AreaController( + ILogger logger, + IMapService mapService, + IAreaService areaService + ) + { + _logger = logger; + _mapService = mapService; + _areaService = areaService; + } + + /// + /// List all asset areas in the Flotilla database + /// + /// + /// This query gets all asset areas + /// + [HttpGet] + [Authorize(Roles = Role.Any)] + [ProducesResponseType(typeof(IList), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task>> GetAreas() + { + try + { + var areas = await _areaService.ReadAll(); + return Ok(areas); + } + catch (Exception e) + { + _logger.LogError(e, "Error during GET of areas from database"); + throw; + } + } + + /// + /// Lookup area by specified id. + /// + [HttpGet] + [Authorize(Roles = Role.Any)] + [Route("{id}")] + [ProducesResponseType(typeof(Area), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task> GetAreaById([FromRoute] string id) + { + var area = await _areaService.ReadById(id); + if (area == null) + return NotFound($"Could not find area with id {id}"); + return Ok(area); + } + + /// + /// Add a new area + /// + /// + /// This query adds a new area to the database + /// + [HttpPost] + [Authorize(Roles = Role.User)] + [ProducesResponseType(typeof(Area), StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task> Create([FromBody] CreateAreaQuery area) + { + _logger.LogInformation("Creating new area"); + try + { + var existingArea = await _areaService.ReadByAssetAndName(area.AssetCode, area.AreaName); + if (existingArea != null) + { + _logger.LogInformation("An ara for given name and asset already exists"); + return BadRequest($"Area already exists"); + } + + var newArea = await _areaService.Create(area); + _logger.LogInformation( + "Succesfully created new area with id '{areaId}'", + newArea.Id + ); + return CreatedAtAction( + nameof(GetAreaById), + new { id = newArea.Id }, + newArea + ); + } + catch (Exception e) + { + _logger.LogError(e, "Error while creating new area"); + throw; + } + } + + + /// + /// Add a safe position to a area + /// + /// + /// This query adds a new safe position to the database + /// + [HttpPost] + [Authorize(Roles = Role.Admin)] + [Route("{asset}/{installationName}/{deckName}/{areaName}/safe-position")] + [ProducesResponseType(typeof(Area), StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task> AddSafePosition( + [FromRoute] string asset, + [FromRoute] string installationName, + [FromRoute] string deckName, + [FromRoute] string areaName, + [FromBody] Pose safePosition + ) + { + _logger.LogInformation("Adding new safe position"); + try + { + var area = await _areaService.AddSafePosition(asset, areaName, new SafePosition(safePosition)); + if (area != null) + { + _logger.LogInformation("Succesfully added new safe position for asset '{assetId}' and name '{name}'", asset, areaName); + return CreatedAtAction(nameof(GetAreaById), new { id = area.Id }, area); ; + } + else + { + _logger.LogInformation("Creating Area for asset '{assetId}' and name '{name}'", asset, areaName); + // Cloning to avoid tracking same object + var tempPose = ObjectCopier.Clone(safePosition); + area = await _areaService.Create( + new CreateAreaQuery + { + AssetCode = asset, + AreaName = areaName, + InstallationName = installationName, + DeckName = deckName, + DefaultLocalizationPose = new Pose() + }, + new List { tempPose } + ); + return CreatedAtAction(nameof(GetAreaById), new { id = area.Id }, area); + } + } + catch (Exception e) + { + _logger.LogError(e, "Error while creating or adding new safe zone"); + throw; + } + } + + /// + /// Deletes the area with the specified id from the database. + /// + [HttpDelete] + [Authorize(Roles = Role.Admin)] + [Route("{id}")] + [ProducesResponseType(typeof(Area), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task> DeleteArea([FromRoute] string id) + { + var area = await _areaService.Delete(id); + if (area is null) + return NotFound($"Area with id {id} not found"); + return Ok(area); + } + + /// + /// Gets map metadata for localization poses belonging to area with specified id + /// + [HttpGet] + [Authorize(Roles = Role.Any)] + [Route("{id}/map-metadata")] + [ProducesResponseType(typeof(MapMetadata), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task> GetMapMetadata([FromRoute] string id) + { + var area = await _areaService.ReadById(id); + if (area is null) + { + _logger.LogError("Area not found for area ID {areaId}", id); + return NotFound("Could not find this area"); + } + + MapMetadata? map; + var positions = new List + { + area.DefaultLocalizationPose.Position + }; + try + { + map = await _mapService.ChooseMapFromPositions(positions, area.Deck.Installation.Asset.ShortName); + } + catch (ArgumentOutOfRangeException) + { + _logger.LogWarning("Unable to find a map for area '{areaId}'", area.Id); + return NotFound("Could not find map suited for the positions in this area"); + } + + if (map == null) + { + return NotFound("Could not find map for this area"); + } + return Ok(map); + } + } +} diff --git a/backend/api/Controllers/AssetDeckController.cs b/backend/api/Controllers/AssetDeckController.cs deleted file mode 100644 index 3927f17a7..000000000 --- a/backend/api/Controllers/AssetDeckController.cs +++ /dev/null @@ -1,256 +0,0 @@ -using Api.Controllers.Models; -using Api.Database.Models; -using Api.Services; -using Api.Utilities; -using Azure; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -namespace Api.Controllers -{ - [ApiController] - [Route("asset-decks")] - public class AssetDeckController : ControllerBase - { - private readonly IAssetDeckService _assetDeckService; - - private readonly ILogger _logger; - - private readonly IMapService _mapService; - - public AssetDeckController( - ILogger logger, - IMapService mapService, - IAssetDeckService assetDeckService - ) - { - _logger = logger; - _mapService = mapService; - _assetDeckService = assetDeckService; - } - - /// - /// List all asset decks in the Flotilla database - /// - /// - /// This query gets all asset decks - /// - [HttpGet] - [Authorize(Roles = Role.Any)] - [ProducesResponseType(typeof(IList), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task>> GetAssetDecks() - { - try - { - var assetDecks = await _assetDeckService.ReadAll(); - return Ok(assetDecks); - } - catch (Exception e) - { - _logger.LogError(e, "Error during GET of asset decks from database"); - throw; - } - } - - /// - /// Lookup asset deck by specified id. - /// - [HttpGet] - [Authorize(Roles = Role.Any)] - [Route("{id}")] - [ProducesResponseType(typeof(AssetDeck), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task> GetAssetDeckById([FromRoute] string id) - { - var assetDeck = await _assetDeckService.ReadById(id); - if (assetDeck == null) - { - return NotFound($"Could not find assetDeck with id {id}"); - } - return Ok(assetDeck); - } - - /// - /// Add a new asset deck - /// - /// - /// This query adds a new asset deck to the database - /// - [HttpPost] - [Authorize(Roles = Role.User)] - [ProducesResponseType(typeof(AssetDeck), StatusCodes.Status201Created)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task> Create([FromBody] CreateAssetDeckQuery assetDeck) - { - _logger.LogInformation("Creating new asset deck"); - try - { - var existingAssetDeck = await _assetDeckService.ReadByAssetAndDeck(assetDeck.AssetCode, assetDeck.DeckName); - if (existingAssetDeck != null) - { - _logger.LogInformation("An asset deck for given deck and asset already exists"); - return BadRequest("Asset deck already exists"); - } - - var newAssetDeck = await _assetDeckService.Create(assetDeck); - _logger.LogInformation( - "Successfully created new asset deck with id '{AssetDeckId}'", - newAssetDeck.Id - ); - return CreatedAtAction( - nameof(GetAssetDeckById), - new - { - id = newAssetDeck.Id - }, - newAssetDeck - ); - } - catch (Exception e) - { - _logger.LogError(e, "Error while creating new asset deck"); - throw; - } - } - - - /// - /// Add a safe position to a asset deck - /// - /// - /// This query adds a new safe position to the database - /// - [HttpPost] - [Authorize(Roles = Role.Admin)] - [Route("{asset}/{deck}/safe-position")] - [ProducesResponseType(typeof(AssetDeck), StatusCodes.Status201Created)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task> AddSafePosition( - [FromRoute] string asset, - [FromRoute] string deck, - [FromBody] Pose safePosition - ) - { - _logger.LogInformation("Adding new safe position"); - try - { - var assetDeck = await _assetDeckService.AddSafePosition(asset, deck, new SafePosition(safePosition)); - if (assetDeck != null) - { - _logger.LogInformation("Successfully added new safe position for asset '{AssetId}' and deck '{DeckId}'", asset, deck); - return CreatedAtAction(nameof(GetAssetDeckById), new - { - id = assetDeck.Id - }, assetDeck); - ; - } - _logger.LogInformation("Creating AssetDeck for asset '{AssetId}' and deck '{DeckId}'", asset, deck); - // Cloning to avoid tracking same object - var tempPose = ObjectCopier.Clone(safePosition); - assetDeck = await _assetDeckService.Create( - new CreateAssetDeckQuery - { - AssetCode = asset, - DeckName = deck, - DefaultLocalizationPose = new Pose() - }, - new List - { - tempPose - } - ); - return CreatedAtAction(nameof(GetAssetDeckById), new - { - id = assetDeck.Id - }, assetDeck); - } - catch (Exception e) - { - _logger.LogError(e, "Error while creating or adding new safe zone"); - throw; - } - } - - /// - /// Deletes the asset deck with the specified id from the database. - /// - [HttpDelete] - [Authorize(Roles = Role.Admin)] - [Route("{id}")] - [ProducesResponseType(typeof(AssetDeck), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task> DeleteAssetDeck([FromRoute] string id) - { - var assetDeck = await _assetDeckService.Delete(id); - if (assetDeck is null) - { - return NotFound($"Asset deck with id {id} not found"); - } - return Ok(assetDeck); - } - - /// - /// Gets map metadata for localization poses belonging to asset deck with specified id - /// - [HttpGet] - [Authorize(Roles = Role.Any)] - [Route("{id}/map-metadata")] - [ProducesResponseType(typeof(MapMetadata), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task> GetMapMetadata([FromRoute] string id) - { - var assetDeck = await _assetDeckService.ReadById(id); - if (assetDeck is null) - { - _logger.LogError("Asset deck not found for asset deck ID {AssetDeckId}", id); - return NotFound("Could not find this asset deck"); - } - - MapMetadata? mapMetadata; - var positions = new List - { - assetDeck.DefaultLocalizationPose.Position - }; - try - { - mapMetadata = await _mapService.ChooseMapFromPositions(positions, assetDeck.AssetCode); - } - catch (RequestFailedException e) - { - string errorMessage = $"An error occurred while retrieving the map for asset deck {assetDeck.Id}"; - _logger.LogError(e, "{ErrorMessage}", errorMessage); - return StatusCode(StatusCodes.Status502BadGateway, errorMessage); - } - catch (ArgumentOutOfRangeException e) - { - string errorMessage = $"Could not find a suitable map for asset deck {assetDeck.Id}"; - _logger.LogError(e, "{ErrorMessage}", errorMessage); - return NotFound(errorMessage); - } - - if (mapMetadata == null) - { - return NotFound("A map which contained at least half of the points in this mission could not be found"); - } - return Ok(mapMetadata); - } - } -} diff --git a/backend/api/Controllers/EchoController.cs b/backend/api/Controllers/EchoController.cs index 631d53426..16aac397c 100644 --- a/backend/api/Controllers/EchoController.cs +++ b/backend/api/Controllers/EchoController.cs @@ -30,13 +30,13 @@ public EchoController(ILogger logger, IEchoService echoService) /// [HttpGet] [Route("available-missions")] - [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] [ProducesResponseType(StatusCodes.Status502BadGateway)] - public async Task>> GetAvailableEchoMissions(string? installationCode) + public async Task>> GetAvailableEchoMissions(string? installationCode) { try { diff --git a/backend/api/Controllers/MissionController.cs b/backend/api/Controllers/MissionController.cs index 54dd190d7..9c40a8767 100644 --- a/backend/api/Controllers/MissionController.cs +++ b/backend/api/Controllers/MissionController.cs @@ -12,45 +12,54 @@ namespace Api.Controllers; [Route("missions")] public class MissionController : ControllerBase { - private readonly IMissionService _missionService; + private readonly IMissionDefinitionService _missionDefinitionService; + private readonly IMissionRunService _missionRunService; + private readonly IAreaService _areaService; private readonly IRobotService _robotService; private readonly IEchoService _echoService; + private readonly ISourceService _sourceService; private readonly ILogger _logger; private readonly IStidService _stidService; private readonly IMapService _mapService; public MissionController( - IMissionService missionService, + IMissionDefinitionService missionDefinitionService, + IMissionRunService missionRunService, + IAreaService areaService, IRobotService robotService, IEchoService echoService, + ISourceService sourceService, ILogger logger, IMapService mapService, IStidService stidService ) { - _missionService = missionService; + _missionDefinitionService = missionDefinitionService; + _missionRunService = missionRunService; + _areaService = areaService; _robotService = robotService; _echoService = echoService; + _sourceService = sourceService; _mapService = mapService; _stidService = stidService; _logger = logger; } /// - /// List all missions in the Flotilla database + /// List all mission runs in the Flotilla database /// /// /// This query gets all missions /// - [HttpGet] + [HttpGet("runs")] [Authorize(Roles = Role.Any)] - [ProducesResponseType(typeof(IList), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(IList), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task>> GetMissions( - [FromQuery] MissionQueryStringParameters parameters + public async Task>> GetMissionRuns( + [FromQuery] MissionRunQueryStringParameters parameters ) { if (parameters.MaxDesiredStartTime < parameters.MinDesiredStartTime) @@ -66,10 +75,57 @@ [FromQuery] MissionQueryStringParameters parameters return BadRequest("Max EndTime cannot be less than min EndTime"); } - PagedList missions; + PagedList missions; + try + { + missions = await _missionRunService.ReadAll(parameters); + } + catch (InvalidDataException e) + { + _logger.LogError(e.Message); + return BadRequest(e.Message); + } + + var metadata = new + { + missions.TotalCount, + missions.PageSize, + missions.CurrentPage, + missions.TotalPages, + missions.HasNext, + missions.HasPrevious + }; + + Response.Headers.Add( + QueryStringParameters.PaginationHeader, + JsonSerializer.Serialize(metadata) + ); + + return Ok(missions); + } + + /// + /// List all mission definitions in the Flotilla database + /// + /// + /// This query gets all missions + /// + [HttpGet("definitions")] + [Authorize(Roles = Role.Any)] + [ProducesResponseType(typeof(IList), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task>> GetMissionDefinitions( + [FromQuery] MissionDefinitionQueryStringParameters parameters + ) + { + // TODO: define new parameters using primarily area and source type + PagedList missions; try { - missions = await _missionService.ReadAll(parameters); + missions = await _missionDefinitionService.ReadAll(parameters); } catch (InvalidDataException e) { @@ -96,19 +152,38 @@ [FromQuery] MissionQueryStringParameters parameters } /// - /// Lookup mission by specified id. + /// Lookup mission run by specified id. /// [HttpGet] [Authorize(Roles = Role.Any)] - [Route("{id}")] - [ProducesResponseType(typeof(Mission), StatusCodes.Status200OK)] + [Route("runs/{id}")] + [ProducesResponseType(typeof(MissionRun), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task> GetMissionById([FromRoute] string id) + public async Task> GetMissionRunById([FromRoute] string id) { - var mission = await _missionService.ReadById(id); + var mission = await _missionRunService.ReadById(id); + if (mission == null) + return NotFound($"Could not find mission with id {id}"); + return Ok(mission); + } + + /// + /// Lookup mission definition by specified id. + /// + [HttpGet] + [Authorize(Roles = Role.Any)] + [Route("definitions/{id}")] + [ProducesResponseType(typeof(MissionRun), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task> GetMissionDefinitionById([FromRoute] string id) + { + var mission = await _missionDefinitionService.ReadById(id); if (mission == null) return NotFound($"Could not find mission with id {id}"); return Ok(mission); @@ -141,20 +216,114 @@ public async Task> GetMap([FromRoute] string assetCode, str } /// - /// Schedule a new mission + /// Reschedule an existing mission /// /// - /// This query schedules a new mission and adds it to the database + /// This query reschedules an existing mission and adds it to the database + /// + [HttpPost("{missionId}/reschedule")] + [Authorize(Roles = Role.User)] + [ProducesResponseType(typeof(MissionRun), StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task> Rechedule( + [FromRoute] string missionId, + [FromBody] RescheduleMissionQuery rescheduledMissionQuery + ) + { + var robot = await _robotService.ReadById(rescheduledMissionQuery.RobotId); + if (robot is null) + return NotFound($"Could not find robot with id {rescheduledMissionQuery.RobotId}"); + + MissionDefinition? missionDefinition; + try + { + missionDefinition = await _missionDefinitionService.ReadById(missionId); + if (missionDefinition == null) + return NotFound("Mission definition not found"); + } + catch (HttpRequestException e) + { + if (e.StatusCode.HasValue && (int)e.StatusCode.Value == 404) + { + _logger.LogWarning( + "Could not find mission definition with id={id}", + missionId + ); + return NotFound("Mission definition not found"); + } + + _logger.LogError(e, "Error getting mission database"); + return StatusCode(StatusCodes.Status502BadGateway, $"{e.Message}"); + } + + List? missionTasks; + switch (missionDefinition.Source.Type) + { + case MissionSourceType.Echo: + missionTasks = _echoService.GetMissionById(Int32.Parse(missionDefinition.Source.Id)).Result.Tags + .Select( + t => + { + var tagPosition = _stidService + .GetTagPosition(t.TagId, missionDefinition.AssetCode) + .Result; + return new MissionTask(t, tagPosition); + } + ) + .ToList(); + break; + case MissionSourceType.Custom: + missionTasks = _sourceService.GetMissionTasksFromURL(missionDefinition.Source.URL); + break; + default: + return BadRequest("Invalid mission source type provided"); + } + + if (missionTasks == null) + return NotFound("No mission tasks were found for the requested mission"); + + var scheduledMission = new MissionRun + { + Name = missionDefinition.Name, + Robot = robot, + MissionId = missionDefinition.Id, + Status = MissionStatus.Pending, + DesiredStartTime = rescheduledMissionQuery.DesiredStartTime, + Tasks = missionTasks, + AssetCode = missionDefinition.AssetCode, + Area = missionDefinition.Area, + MapMetadata = new MapMetadata() + }; + + await _mapService.AssignMapToMission(scheduledMission); + + if (scheduledMission.Tasks.Any()) + scheduledMission.CalculateEstimatedDuration(); + + var newMission = await _missionRunService.Create(scheduledMission); + + return CreatedAtAction(nameof(GetMissionRunById), new { id = newMission.Id }, newMission); + } + + /// + /// Schedule a new echo mission + /// + /// + /// This query schedules a new echo mission and adds it to the database /// [HttpPost] [Authorize(Roles = Role.User)] - [ProducesResponseType(typeof(Mission), StatusCodes.Status201Created)] + [ProducesResponseType(typeof(MissionRun), StatusCodes.Status201Created)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task> Create( + public async Task> Create( [FromBody] ScheduledMissionQuery scheduledMissionQuery ) { @@ -207,15 +376,40 @@ [FromBody] ScheduledMissionQuery scheduledMissionQuery ) .ToList(); - var scheduledMission = new Mission + var area = await _areaService.ReadByAssetAndName(scheduledMissionQuery.AssetCode, scheduledMissionQuery.AreaName); + + if (area == null) + { + //return NotFound($"Could not find area with name {scheduledMissionQuery.AreaName} in asset {scheduledMissionQuery.AssetCode}"); + } + + // TODO: search for if a source with the given type and URL exists, then reuse it + + var scheduledMissionDefinition = new MissionDefinition + { + Id = Guid.NewGuid().ToString(), + Source = new Source + { + Id = Guid.NewGuid().ToString(), + URL = $"robots/robot-plan/{echoMission.Id}", // Could use echoMission.URL here, but that would necessitate new retrieval methods + Type = MissionSourceType.Echo + }, + Name = echoMission.Name, + InspectionFrequency = scheduledMissionQuery.InspectionFrequency, + AssetCode = scheduledMissionQuery.AssetCode, + Area = area + }; + + var scheduledMission = new MissionRun { Name = echoMission.Name, Robot = robot, - EchoMissionId = scheduledMissionQuery.EchoMissionId, + MissionId = scheduledMissionDefinition.Id, Status = MissionStatus.Pending, DesiredStartTime = scheduledMissionQuery.DesiredStartTime, Tasks = missionTasks, AssetCode = scheduledMissionQuery.AssetCode, + Area = area, MapMetadata = new MapMetadata() }; @@ -224,9 +418,11 @@ [FromBody] ScheduledMissionQuery scheduledMissionQuery if (scheduledMission.Tasks.Any()) scheduledMission.CalculateEstimatedDuration(); - var newMission = await _missionService.Create(scheduledMission); + var newMissionDefinition = await _missionDefinitionService.Create(scheduledMissionDefinition); - return CreatedAtAction(nameof(GetMissionById), new { id = newMission.Id }, newMission); + var newMission = await _missionRunService.Create(scheduledMission); + + return CreatedAtAction(nameof(GetMissionRunById), new { id = newMission.Id }, newMission); } /// @@ -238,32 +434,63 @@ [FromBody] ScheduledMissionQuery scheduledMissionQuery [HttpPost] [Authorize(Roles = Role.User)] [Route("custom")] - [ProducesResponseType(typeof(Mission), StatusCodes.Status201Created)] + [ProducesResponseType(typeof(MissionRun), StatusCodes.Status201Created)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task> Create( + public async Task> Create( [FromBody] CustomMissionQuery customMissionQuery ) { + + // TODO: only allow admins + + // TODO: create new endpoint for scheduling existing custom missions + var robot = await _robotService.ReadById(customMissionQuery.RobotId); if (robot is null) return NotFound($"Could not find robot with id {customMissionQuery.RobotId}"); var missionTasks = customMissionQuery.Tasks.Select(task => new MissionTask(task)).ToList(); - var scheduledMission = new Mission + var area = await _areaService.ReadByAssetAndName(customMissionQuery.AssetCode, customMissionQuery.AreaName); + + if (area == null) + return NotFound($"Could not find area with name {customMissionQuery.AreaName} in asset {customMissionQuery.AssetCode}"); + + // TODO: upload file to blobstorage and then pass the URL to MissionDefinition Source + var customMissionId = Guid.NewGuid().ToString(); + var sourceURL = await _sourceService.UploadSource(customMissionId, missionTasks); + + var customMissionDefinition = new MissionDefinition + { + Id = customMissionId, + Source = new Source + { + Id = Guid.NewGuid().ToString(), + URL = sourceURL.ToString(), + Type = MissionSourceType.Echo + }, + Name = customMissionQuery.Name, + InspectionFrequency = customMissionQuery.InspectionFrequency, + AssetCode = customMissionQuery.AssetCode, + Area = area + }; + + var scheduledMission = new MissionRun { Name = customMissionQuery.Name, Description = customMissionQuery.Description, + MissionId = customMissionDefinition.Id, Comment = customMissionQuery.Comment, Robot = robot, Status = MissionStatus.Pending, DesiredStartTime = customMissionQuery.DesiredStartTime ?? DateTimeOffset.UtcNow, Tasks = missionTasks, AssetCode = customMissionQuery.AssetCode, + Area = area, MapMetadata = new MapMetadata() }; @@ -272,27 +499,48 @@ [FromBody] CustomMissionQuery customMissionQuery if (scheduledMission.Tasks.Any()) scheduledMission.CalculateEstimatedDuration(); - var newMission = await _missionService.Create(scheduledMission); + var newMissionDefinition = await _missionDefinitionService.Create(customMissionDefinition); - return CreatedAtAction(nameof(GetMissionById), new { id = newMission.Id }, newMission); + var newMission = await _missionRunService.Create(scheduledMission); + + return CreatedAtAction(nameof(GetMissionRunById), new { id = newMission.Id }, newMission); + } + + /// + /// Deletes the mission definition with the specified id from the database. + /// + [HttpDelete] + [Authorize(Roles = Role.Admin)] + [Route("definitions/{id}")] + [ProducesResponseType(typeof(MissionDefinition), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task> DeleteMissionDefinition([FromRoute] string id) + { + var mission = await _missionDefinitionService.Delete(id); + if (mission is null) + return NotFound($"Mission definition with id {id} not found"); + return Ok(mission); } /// - /// Deletes the mission with the specified id from the database. + /// Deletes the mission run with the specified id from the database. /// [HttpDelete] [Authorize(Roles = Role.Admin)] - [Route("{id}")] - [ProducesResponseType(typeof(Mission), StatusCodes.Status200OK)] + [Route("runs/{id}")] + [ProducesResponseType(typeof(MissionRun), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task> DeleteMission([FromRoute] string id) + public async Task> DeleteMissionRun([FromRoute] string id) { - var mission = await _missionService.Delete(id); + var mission = await _missionRunService.Delete(id); if (mission is null) - return NotFound($"Mission with id {id} not found"); + return NotFound($"Mission run with id {id} not found"); return Ok(mission); } } diff --git a/backend/api/Controllers/Models/MissionDefinition.cs b/backend/api/Controllers/Models/CondensedMissionDefinition.cs similarity index 82% rename from backend/api/Controllers/Models/MissionDefinition.cs rename to backend/api/Controllers/Models/CondensedMissionDefinition.cs index 415287018..15e28f569 100644 --- a/backend/api/Controllers/Models/MissionDefinition.cs +++ b/backend/api/Controllers/Models/CondensedMissionDefinition.cs @@ -1,7 +1,7 @@ # nullable disable namespace Api.Controllers.Models { - public class MissionDefinition + public class CondensedMissionDefinition { public int EchoMissionId { get; set; } diff --git a/backend/api/Controllers/Models/CreateAreaQuery.cs b/backend/api/Controllers/Models/CreateAreaQuery.cs new file mode 100644 index 000000000..ac25914af --- /dev/null +++ b/backend/api/Controllers/Models/CreateAreaQuery.cs @@ -0,0 +1,14 @@ +using Api.Database.Models; + +namespace Api.Controllers.Models +{ + public struct CreateAreaQuery + { + public string AssetCode { get; set; } + public string InstallationName { get; set; } + public string DeckName { get; set; } + public string AreaName { get; set; } + + public Pose DefaultLocalizationPose { get; set; } + } +} diff --git a/backend/api/Controllers/Models/CreateAssetDeckQuery.cs b/backend/api/Controllers/Models/CreateAssetDeckQuery.cs index 107249548..c42fd473f 100644 --- a/backend/api/Controllers/Models/CreateAssetDeckQuery.cs +++ b/backend/api/Controllers/Models/CreateAssetDeckQuery.cs @@ -1,4 +1,4 @@ -using Api.Database.Models; +using Api.Database.Models; namespace Api.Controllers.Models { diff --git a/backend/api/Controllers/Models/CustomMissionQuery.cs b/backend/api/Controllers/Models/CustomMissionQuery.cs index ccb14fca6..0e7fc7fad 100644 --- a/backend/api/Controllers/Models/CustomMissionQuery.cs +++ b/backend/api/Controllers/Models/CustomMissionQuery.cs @@ -34,6 +34,10 @@ public struct CustomMissionQuery public string AssetCode { get; set; } + public TimeSpan? InspectionFrequency { get; set; } + + public string AreaName { get; set; } + public string Name { get; set; } public string? Description { get; set; } diff --git a/backend/api/Controllers/Models/MissionDefinitionQueryStringParameters.cs b/backend/api/Controllers/Models/MissionDefinitionQueryStringParameters.cs new file mode 100644 index 000000000..7bcfb995d --- /dev/null +++ b/backend/api/Controllers/Models/MissionDefinitionQueryStringParameters.cs @@ -0,0 +1,33 @@ +using Api.Database.Models; + +namespace Api.Controllers.Models +{ + public class MissionDefinitionQueryStringParameters : QueryStringParameters + { + public MissionDefinitionQueryStringParameters() + { + // Default order is desired start time + OrderBy = "DesiredStartTime desc"; + } + + /// + /// Filter for the asset code of the mission + /// + public string? AssetCode { get; set; } + + /// + /// Filter for the area of the mission + /// + public string? Area { get; set; } + + /// + /// The search parameter for the mission name + /// + public string? NameSearch { get; set; } + + /// + /// The search parameter for the mission source type + /// + public MissionSourceType? SourceType { get; set; } + } +} diff --git a/backend/api/Controllers/Models/MissionQueryStringParameters.cs b/backend/api/Controllers/Models/MissionRunQueryStringParameters.cs similarity index 91% rename from backend/api/Controllers/Models/MissionQueryStringParameters.cs rename to backend/api/Controllers/Models/MissionRunQueryStringParameters.cs index daf735c5a..3a69e63d9 100644 --- a/backend/api/Controllers/Models/MissionQueryStringParameters.cs +++ b/backend/api/Controllers/Models/MissionRunQueryStringParameters.cs @@ -2,9 +2,9 @@ namespace Api.Controllers.Models { - public class MissionQueryStringParameters : QueryStringParameters + public class MissionRunQueryStringParameters : QueryStringParameters { - public MissionQueryStringParameters() + public MissionRunQueryStringParameters() { // Default order is desired start time OrderBy = "DesiredStartTime desc"; @@ -20,6 +20,11 @@ public MissionQueryStringParameters() /// public string? AssetCode { get; set; } + /// + /// Filter for the area of the mission + /// + public string? Area { get; set; } + /// /// Filter for the robot id of the robot assigned to the mission /// @@ -83,6 +88,5 @@ public MissionQueryStringParameters() public long MaxDesiredStartTime { get; set; } = DateTimeOffset.MaxValue.ToUnixTimeSeconds(); #endregion Time Filters - } } diff --git a/backend/api/Controllers/Models/RescheduleMissionQuery.cs b/backend/api/Controllers/Models/RescheduleMissionQuery.cs new file mode 100644 index 000000000..de0109324 --- /dev/null +++ b/backend/api/Controllers/Models/RescheduleMissionQuery.cs @@ -0,0 +1,12 @@ +using Api.Database.Models; + +namespace Api.Controllers.Models +{ + public class RescheduleMissionQuery + { + public MissionSourceType MissionType { get; set; } + public string RobotId { get; set; } + public DateTimeOffset DesiredStartTime { get; set; } + public TimeSpan? InspectionFrequency { get; set; } + } +} diff --git a/backend/api/Controllers/Models/ScheduleLocalizationMissionQuery.cs b/backend/api/Controllers/Models/ScheduleLocalizationMissionQuery.cs index 7a160bd06..7a9fd8f23 100644 --- a/backend/api/Controllers/Models/ScheduleLocalizationMissionQuery.cs +++ b/backend/api/Controllers/Models/ScheduleLocalizationMissionQuery.cs @@ -5,7 +5,7 @@ namespace Api.Controllers.Models public struct ScheduleLocalizationMissionQuery { public string RobotId { get; set; } - public string DeckId { get; set; } + public string AreaId { get; set; } public Pose LocalizationPose { get; set; } } } diff --git a/backend/api/Controllers/Models/ScheduledMissionQuery.cs b/backend/api/Controllers/Models/ScheduledMissionQuery.cs index 1960936fc..4e34ae1c3 100644 --- a/backend/api/Controllers/Models/ScheduledMissionQuery.cs +++ b/backend/api/Controllers/Models/ScheduledMissionQuery.cs @@ -6,5 +6,7 @@ public struct ScheduledMissionQuery public int EchoMissionId { get; set; } public DateTimeOffset DesiredStartTime { get; set; } public string AssetCode { get; set; } + public string AreaName { get; set; } + public TimeSpan? InspectionFrequency { get; set; } } } diff --git a/backend/api/Controllers/RobotController.cs b/backend/api/Controllers/RobotController.cs index 9c195b077..3103649d8 100644 --- a/backend/api/Controllers/RobotController.cs +++ b/backend/api/Controllers/RobotController.cs @@ -16,17 +16,17 @@ public class RobotController : ControllerBase private readonly ILogger _logger; private readonly IRobotService _robotService; private readonly IIsarService _isarService; - private readonly IMissionService _missionService; + private readonly IMissionRunService _missionService; private readonly IRobotModelService _robotModelService; - private readonly IAssetDeckService _assetDeckService; + private readonly IAreaService _areaService; public RobotController( ILogger logger, IRobotService robotService, IIsarService isarService, - IMissionService missionService, + IMissionRunService missionService, IRobotModelService robotModelService, - IAssetDeckService assetDeckService + IAreaService areaService ) { _logger = logger; @@ -34,7 +34,7 @@ IAssetDeckService assetDeckService _isarService = isarService; _missionService = missionService; _robotModelService = robotModelService; - _assetDeckService = assetDeckService; + _areaService = areaService; } /// @@ -190,7 +190,7 @@ [FromBody] Robot robot [HttpDelete] [Authorize(Roles = Role.Admin)] [Route("{id}")] - [ProducesResponseType(typeof(Mission), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(MissionRun), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] @@ -332,13 +332,13 @@ [FromBody] VideoStream videoStream [HttpPost] [Authorize(Roles = Role.Admin)] [Route("{robotId}/start/{missionId}")] - [ProducesResponseType(typeof(Mission), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(MissionRun), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task> StartMission( + public async Task> StartMission( [FromRoute] string robotId, [FromRoute] string missionId ) @@ -644,15 +644,14 @@ [FromRoute] string armPosition [HttpPost] [Authorize(Roles = Role.User)] [Route("start-localization")] - [ProducesResponseType(typeof(Mission), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(MissionRun), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task> StartLocalizationMission( + public async Task> StartLocalizationMission( [FromBody] ScheduleLocalizationMissionQuery scheduleLocalizationMissionQuery - ) { var robot = await _robotService.ReadById(scheduleLocalizationMissionQuery.RobotId); @@ -672,25 +671,25 @@ [FromBody] ScheduleLocalizationMissionQuery scheduleLocalizationMissionQuery return Conflict($"The Robot is not available ({robot.Status})"); } - var mission = new Mission + var area = await _areaService.ReadById(scheduleLocalizationMissionQuery.AreaId); + + if (area == null) + { + _logger.LogWarning("Could not find area with id={id}", scheduleLocalizationMissionQuery.AreaId); + return NotFound("Area not found"); + } + + var mission = new MissionRun { Name = "Localization Mission", Robot = robot, - AssetCode = "NA", + Area = area, Status = MissionStatus.Pending, DesiredStartTime = DateTimeOffset.UtcNow, Tasks = new List(), MapMetadata = new MapMetadata() }; - var deck = await _assetDeckService.ReadById(scheduleLocalizationMissionQuery.DeckId); - - if (deck == null) - { - _logger.LogWarning("Could not find deck with id={id}", scheduleLocalizationMissionQuery.DeckId); - return NotFound("Deck not found"); - } - IsarMission isarMission; try { @@ -722,7 +721,7 @@ [FromBody] ScheduleLocalizationMissionQuery scheduleLocalizationMissionQuery robot.Status = RobotStatus.Busy; robot.CurrentMissionId = mission.Id; - robot.CurrentAssetDeck = deck; + robot.CurrentArea = area; await _robotService.Update(robot); return Ok(mission); } @@ -734,18 +733,18 @@ [FromBody] ScheduleLocalizationMissionQuery scheduleLocalizationMissionQuery /// This query starts a localization for a given robot /// [HttpPost] - [Route("{robotId}/{asset}/{deck}/go-to-safe-position")] + [Route("{robotId}/{asset}/{areaName}/go-to-safe-position")] [Authorize(Roles = Role.User)] - [ProducesResponseType(typeof(Mission), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(MissionRun), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task> SafePosition( + public async Task> SafePosition( [FromRoute] string robotId, [FromRoute] string asset, - [FromRoute] string deck + [FromRoute] string areaName ) { var robot = await _robotService.ReadById(robotId); @@ -755,7 +754,7 @@ [FromRoute] string deck return NotFound("Robot not found"); } - var assets = await _assetDeckService.ReadByAsset(asset); + var assets = await _areaService.ReadByAsset(asset); if (!assets.Any()) { @@ -763,16 +762,16 @@ [FromRoute] string deck return NotFound("No asset found"); } - var assetDeck = await _assetDeckService.ReadByAssetAndDeck(asset, deck); - if (assetDeck is null) + var area = await _areaService.ReadByAssetAndName(asset, areaName); + if (area is null) { - _logger.LogWarning("Could not find deck={deck}", deck); - return NotFound("No deck found"); + _logger.LogWarning("Could not find area={areaName}", areaName); + return NotFound("No area found"); } - if (assetDeck.SafePositions.Count < 1) + if (area.SafePositions.Count < 1) { - _logger.LogWarning("No safe position for asset={asset}, deck={deck}", asset, deck); + _logger.LogWarning("No safe position for asset={asset}, area={areaName}", asset, areaName); return NotFound("No safe positions found"); } @@ -797,7 +796,7 @@ [FromRoute] string deck return StatusCode(StatusCodes.Status502BadGateway, message); } - var closestSafePosition = ClosestSafePosition(robot.Pose, assetDeck.SafePositions); + var closestSafePosition = ClosestSafePosition(robot.Pose, area.SafePositions); // Cloning to avoid tracking same object var clonedPose = ObjectCopier.Clone(closestSafePosition); var customTaskQuery = new CustomTaskQuery @@ -807,11 +806,11 @@ [FromRoute] string deck InspectionTarget = new Position(), TaskOrder = 0 }; - var mission = new Mission + var mission = new MissionRun { Name = "Drive to Safe Position", Robot = robot, - AssetCode = assetDeck.AssetCode, + Area = area, Status = MissionStatus.Pending, DesiredStartTime = DateTimeOffset.UtcNow, Tasks = new List(new[] { new MissionTask(customTaskQuery) }), @@ -853,7 +852,6 @@ [FromRoute] string deck return Ok(mission); } - private async void OnIsarUnavailable(Robot robot) { robot.Enabled = false; diff --git a/backend/api/Database/Context/FlotillaDbContext.cs b/backend/api/Database/Context/FlotillaDbContext.cs index 073daaaf0..f5c5644a0 100644 --- a/backend/api/Database/Context/FlotillaDbContext.cs +++ b/backend/api/Database/Context/FlotillaDbContext.cs @@ -9,8 +9,12 @@ public class FlotillaDbContext : DbContext { public DbSet Robots => Set(); public DbSet RobotModels => Set(); - public DbSet Missions => Set(); - public DbSet AssetDecks => Set(); + public DbSet MissionRuns => Set(); + public DbSet MissionDefinitions => Set(); + public DbSet Installations => Set(); + public DbSet Assets => Set(); + public DbSet Decks => Set(); + public DbSet Areas => Set(); public DbSet SafePositions => Set(); public FlotillaDbContext(DbContextOptions options) : base(options) { } @@ -21,7 +25,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) // https://docs.microsoft.com/en-us/ef/core/modeling/owned-entities // https://docs.microsoft.com/en-us/ef/core/modeling/owned-entities#collections-of-owned-types - modelBuilder.Entity( + modelBuilder.Entity( missionEntity => { if (isSqlLite) @@ -54,16 +58,20 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) } ); - modelBuilder.Entity().OwnsOne(m => m.MapMetadata).OwnsOne(t => t.TransformationMatrices); - modelBuilder.Entity().OwnsOne(m => m.MapMetadata).OwnsOne(b => b.Boundary); + modelBuilder.Entity().OwnsOne(m => m.MapMetadata).OwnsOne(t => t.TransformationMatrices); + modelBuilder.Entity().OwnsOne(m => m.MapMetadata).OwnsOne(b => b.Boundary); modelBuilder.Entity().OwnsOne(r => r.Pose).OwnsOne(p => p.Orientation); modelBuilder.Entity().OwnsOne(r => r.Pose).OwnsOne(p => p.Position); modelBuilder.Entity().OwnsMany(r => r.VideoStreams); - modelBuilder.Entity().OwnsOne(a => a.DefaultLocalizationPose, poseBuilder => + modelBuilder.Entity().OwnsOne(a => a.DefaultLocalizationPose, poseBuilder => { poseBuilder.OwnsOne(pose => pose.Position); poseBuilder.OwnsOne(pose => pose.Orientation); }); + modelBuilder.Entity().HasOne(a => a.Deck); + modelBuilder.Entity().HasOne(a => a.Installation); + modelBuilder.Entity().HasOne(a => a.Asset); + modelBuilder.Entity().OwnsOne(s => s.Pose, poseBuilder => { poseBuilder.OwnsOne(pose => pose.Position); @@ -73,8 +81,9 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) // There can only be one robot model per robot type modelBuilder.Entity().HasIndex(model => model.Type).IsUnique(); - // There can only be one asset deck - modelBuilder.Entity().HasIndex(a => new { a.AssetCode, a.DeckName }).IsUnique(); + // There can only be one unique asset and installation shortname + modelBuilder.Entity().HasIndex(a => new { a.ShortName }).IsUnique(); + modelBuilder.Entity().HasIndex(a => new { a.ShortName }).IsUnique(); } // SQLite does not have proper support for DateTimeOffset via Entity Framework Core, see the limitations diff --git a/backend/api/Database/Context/InitDb.cs b/backend/api/Database/Context/InitDb.cs index f4b9e2613..f8954d41d 100644 --- a/backend/api/Database/Context/InitDb.cs +++ b/backend/api/Database/Context/InitDb.cs @@ -5,8 +5,13 @@ namespace Api.Database.Context; public static class InitDb { private static readonly List robots = GetRobots(); - private static readonly List missions = GetMissions(); - private static readonly List assetDecks = GetAssetDecks(); + private static readonly List assets = GetAssets(); + private static readonly List installations = GetInstallations(); + private static readonly List decks = GetDecks(); + private static readonly List areas = GetAreas(); + private static readonly List sources = GetSources(); + private static readonly List missionDefinitions = GetMissionDefinitions(); + private static readonly List missionRuns = GetMissionRuns(); private static VideoStream VideoStream => new() @@ -73,57 +78,177 @@ private static List GetRobots() return new List(new Robot[] { robot1, robot2, robot3 }); } - private static List GetMissions() + private static List GetAssets() { - var mission1 = new Mission + var asset1 = new Asset + { + Id = Guid.NewGuid().ToString(), + Name = "Johan Sverdrup", + ShortName = "JSV" + }; + + return new List(new Asset[] { asset1 }); + } + + private static List GetInstallations() + { + var installation1 = new Installation + { + Id = Guid.NewGuid().ToString(), + Asset = assets[0], + Name = "Johan Sverdrup - P1", + ShortName = "P1" + }; + + return new List(new Installation[] { installation1 }); + } + + private static List GetDecks() + { + var deck1 = new Deck + { + Id = Guid.NewGuid().ToString(), + Installation = installations[0], + Name = "TestDeck" + }; + + return new List(new Deck[] { deck1 }); + } + + private static List GetAreas() + { + var area1 = new Area + { + Id = Guid.NewGuid().ToString(), + Deck = decks[0], + Name = "AP320", + Map = new MapMetadata(), + DefaultLocalizationPose = new Pose {}, + SafePositions = new List() + }; + + var area2 = new Area + { + Id = Guid.NewGuid().ToString(), + Deck = decks[0], + Name = "AP330", + Map = new MapMetadata(), + DefaultLocalizationPose = new Pose {}, + SafePositions = new List() + }; + + var area3 = new Area + { + Id = "TestId", + Deck = decks[0], + Name = "testArea", + Map = new MapMetadata(), + DefaultLocalizationPose = new Pose { }, + SafePositions = new List() + }; + + return new List(new Area[] { area1, area2, area3 }); + } + + private static List GetSources() + { + var source1 = new Source + { + Id = Guid.NewGuid().ToString(), + URL = "https://google.com/", + Type = MissionSourceType.Echo + }; + + var source2 = new Source + { + Id = Guid.NewGuid().ToString(), + URL = "https://google.com/", + Type = MissionSourceType.Custom + }; + + return new List(new Source[] { source1, source2 }); + } + + private static List GetMissionDefinitions() + { + var mission1 = new MissionDefinition + { + Name = "Placeholder Mission 1", + Id = Guid.NewGuid().ToString(), + AssetCode = areas[0].Deck.Installation.Asset.ShortName, + Area = areas[0], + Source = sources[0], + Comment = "Interesting comment", + InspectionFrequency = TimeSpan.Parse("14:00:0:0"), + LastRun = null + }; + + var mission2 = new MissionDefinition + { + Name = "Placeholder Mission 2", + Id = Guid.NewGuid().ToString(), + AssetCode = areas[1].Deck.Installation.Asset.ShortName, + Area = areas[1], + Source = sources[1], + InspectionFrequency = TimeSpan.Parse("7:00:0:0"), + LastRun = null + }; + + var mission3 = new MissionDefinition + { + Name = "Placeholder Mission 3", + Id = Guid.NewGuid().ToString(), + AssetCode = areas[1].Deck.Installation.Asset.ShortName, + Area = areas[1], + Source = sources[1], + LastRun = null + }; + + return new List(new[] { mission1, mission2, mission3 }); + } + + private static List GetMissionRuns() + { + var mission1 = new MissionRun { Name = "Placeholder Mission 1", Robot = robots[0], - AssetCode = "test", - EchoMissionId = 95, + AssetCode = areas[0].Deck.Installation.Asset.ShortName, + Area = areas[0], + MissionId = missionDefinitions[0].Id, Status = MissionStatus.Successful, DesiredStartTime = DateTimeOffset.UtcNow, Tasks = new List(), MapMetadata = new MapMetadata() }; - var mission2 = new Mission + var mission2 = new MissionRun { Name = "Placeholder Mission 2", Robot = robots[1], - AssetCode = "test", - EchoMissionId = 95, + AssetCode = areas[1].Deck.Installation.Asset.ShortName, + Area = areas[1], + MissionId = missionDefinitions[0].Id, Status = MissionStatus.Successful, DesiredStartTime = DateTimeOffset.UtcNow, Tasks = new List(), MapMetadata = new MapMetadata() }; - var mission3 = new Mission + var mission3 = new MissionRun { Name = "Placeholder Mission 3", Robot = robots[2], - AssetCode = "kaa", + AssetCode = areas[1].Deck.Installation.Asset.ShortName, + Area = areas[1], + MissionId = missionDefinitions[1].Id, Status = MissionStatus.Successful, DesiredStartTime = DateTimeOffset.UtcNow, Tasks = new List(), MapMetadata = new MapMetadata() }; - return new List(new[] { mission1, mission2, mission3 }); - } - - private static List GetAssetDecks() - { - var assetDeck1 = new AssetDeck - { - Id = "TestId", - DeckName = "Placeholder deck 1", - AssetCode = "Placeholder asset", - DefaultLocalizationPose = new Pose() - }; - - return new List(new[] { assetDeck1 }); + return new List(new[] { mission1, mission2, mission3 }); } public static void PopulateDb(FlotillaDbContext context) @@ -159,7 +284,7 @@ public static void PopulateDb(FlotillaDbContext context) robots[1].Model = models.Find(model => model.Type == RobotType.ExR2)!; robots[2].Model = models.Find(model => model.Type == RobotType.AnymalX)!; - foreach (var mission in missions) + foreach (var mission in missionRuns) { var task = ExampleTask; task.Inspections.Add(Inspection); @@ -168,8 +293,9 @@ public static void PopulateDb(FlotillaDbContext context) mission.Tasks = tasks; } context.AddRange(robots); - context.AddRange(missions); - context.AddRange(assetDecks); + context.AddRange(missionDefinitions); + context.AddRange(missionRuns); + context.AddRange(areas); context.SaveChanges(); } } diff --git a/backend/api/Database/Models/Area.cs b/backend/api/Database/Models/Area.cs new file mode 100644 index 000000000..863a031c5 --- /dev/null +++ b/backend/api/Database/Models/Area.cs @@ -0,0 +1,48 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +#pragma warning disable CS8618 +namespace Api.Database.Models +{ + public class Area + { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public string Id { get; set; } + + [Required] + [MaxLength(200)] + public virtual Deck Deck { get; set; } + + [Required] + [MaxLength(200)] + public string Name { get; set; } + + [Required] + public MapMetadata Map { get; set; } + + [Required] + public Pose DefaultLocalizationPose { get; set; } + + public IList SafePositions { get; set; } + } + + public class SafePosition + { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public string Id { get; set; } + + public Pose Pose { get; set; } + + public SafePosition() + { + Pose = new Pose(); + } + + public SafePosition(Pose pose) + { + Pose = pose; + } + } +} diff --git a/backend/api/Database/Models/Asset.cs b/backend/api/Database/Models/Asset.cs new file mode 100644 index 000000000..e5e832c44 --- /dev/null +++ b/backend/api/Database/Models/Asset.cs @@ -0,0 +1,21 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +#pragma warning disable CS8618 +namespace Api.Database.Models +{ + public class Asset + { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public string Id { get; set; } + + [Required] + [MaxLength(200)] + public string Name { get; set; } + + [Required] + [MaxLength(10)] + public string ShortName { get; set; } + } +} diff --git a/backend/api/Database/Models/AssetDeck.cs b/backend/api/Database/Models/AssetDeck.cs index d1fa63e2f..01fd60564 100644 --- a/backend/api/Database/Models/AssetDeck.cs +++ b/backend/api/Database/Models/AssetDeck.cs @@ -1,4 +1,4 @@ -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; #pragma warning disable CS8618 @@ -23,23 +23,4 @@ public class AssetDeck public IList SafePositions { get; set; } } - - public class SafePosition - { - [Key] - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public string Id { get; set; } - - public Pose Pose { get; set; } - - public SafePosition() - { - Pose = new Pose(); - } - - public SafePosition(Pose pose) - { - Pose = pose; - } - } } diff --git a/backend/api/Database/Models/Deck.cs b/backend/api/Database/Models/Deck.cs new file mode 100644 index 000000000..1e2372d4c --- /dev/null +++ b/backend/api/Database/Models/Deck.cs @@ -0,0 +1,21 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +#pragma warning disable CS8618 +namespace Api.Database.Models +{ + public class Deck + { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public string Id { get; set; } + + [Required] + [MaxLength(200)] + public Installation Installation { get; set; } + + [Required] + [MaxLength(200)] + public string Name { get; set; } + } +} diff --git a/backend/api/Database/Models/Installation.cs b/backend/api/Database/Models/Installation.cs new file mode 100644 index 000000000..236ec3af1 --- /dev/null +++ b/backend/api/Database/Models/Installation.cs @@ -0,0 +1,24 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +#pragma warning disable CS8618 +namespace Api.Database.Models +{ + public class Installation + { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public string Id { get; set; } + + [Required] + public Asset Asset { get; set; } + + [Required] + [MaxLength(200)] + public string Name { get; set; } + + [Required] + [MaxLength(10)] + public string ShortName { get; set; } + } +} diff --git a/backend/api/Database/Models/MissionDefinition.cs b/backend/api/Database/Models/MissionDefinition.cs new file mode 100644 index 000000000..d2de32cda --- /dev/null +++ b/backend/api/Database/Models/MissionDefinition.cs @@ -0,0 +1,32 @@ +using System.ComponentModel.DataAnnotations; + +#pragma warning disable CS8618 +namespace Api.Database.Models +{ + public class MissionDefinition + { + [Key] + [Required] + [MaxLength(200)] + public string Id { get; set; } + + [Required] + public Source Source { get; set; } + + [Required] + [MaxLength(200)] + public string Name { get; set; } + + [MaxLength(1000)] + public string? Comment { get; set; } + + public TimeSpan? InspectionFrequency { get; set; } + + public MissionRun? LastRun { get; set; } + + [Required] + public string AssetCode { get; set; } + + public Area? Area { get; set; } + } +} diff --git a/backend/api/Database/Models/Mission.cs b/backend/api/Database/Models/MissionRun.cs similarity index 98% rename from backend/api/Database/Models/Mission.cs rename to backend/api/Database/Models/MissionRun.cs index edfe1c7e8..079805751 100644 --- a/backend/api/Database/Models/Mission.cs +++ b/backend/api/Database/Models/MissionRun.cs @@ -5,14 +5,14 @@ #pragma warning disable CS8618 namespace Api.Database.Models { - public class Mission + public class MissionRun { [Key] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public string Id { get; set; } [MaxLength(200)] - public int? EchoMissionId { get; set; } + public string MissionId { get; set; } [MaxLength(200)] public string? IsarMissionId { get; set; } @@ -31,9 +31,10 @@ public class Mission public string? Comment { get; set; } [Required] - [MaxLength(200)] public string AssetCode { get; set; } + public Area? Area { get; set; } + [Required] public virtual Robot Robot { get; set; } diff --git a/backend/api/Database/Models/Robot.cs b/backend/api/Database/Models/Robot.cs index a9002a045..2f125c259 100644 --- a/backend/api/Database/Models/Robot.cs +++ b/backend/api/Database/Models/Robot.cs @@ -28,7 +28,7 @@ public class Robot public string CurrentAsset { get; set; } - public AssetDeck? CurrentAssetDeck { get; set; } + public Area? CurrentArea { get; set; } public float BatteryLevel { get; set; } @@ -99,7 +99,7 @@ public Robot(CreateRobotQuery createQuery) Name = createQuery.Name; SerialNumber = createQuery.SerialNumber; CurrentAsset = createQuery.CurrentAsset; - CurrentAssetDeck = createQuery.CurrentAssetDeck; + CurrentArea = createQuery.CurrentAssetDeck; VideoStreams = videoStreams; Host = createQuery.Host; Port = createQuery.Port; diff --git a/backend/api/Database/Models/RobotModel.cs b/backend/api/Database/Models/RobotModel.cs index 38f4b897c..54629e24b 100644 --- a/backend/api/Database/Models/RobotModel.cs +++ b/backend/api/Database/Models/RobotModel.cs @@ -79,7 +79,7 @@ public void Update(UpdateRobotModelQuery updateQuery) /// Updates the based on the data in the provided /// /// - public void UpdateAverageDurationPerTag(List recentMissionsForModelType) + public void UpdateAverageDurationPerTag(List recentMissionsForModelType) { if (recentMissionsForModelType.Any(mission => mission.Robot.Model.Type != Type)) throw new ArgumentException( diff --git a/backend/api/Database/Models/Source.cs b/backend/api/Database/Models/Source.cs new file mode 100644 index 000000000..08ea025d4 --- /dev/null +++ b/backend/api/Database/Models/Source.cs @@ -0,0 +1,24 @@ +using System.ComponentModel.DataAnnotations; + +#pragma warning disable CS8618 +namespace Api.Database.Models +{ + public class Source + { + [Key] + [Required] + [MaxLength(200)] + public string Id { get; set; } + + [Required] + public string URL { get; set; } + + [Required] + public MissionSourceType Type { get; set; } + } + + public enum MissionSourceType + { + Echo, Custom + } +} diff --git a/backend/api/EventHandlers/IsarConnectionEventHandler.cs b/backend/api/EventHandlers/IsarConnectionEventHandler.cs index 3c601a055..36165b9ff 100644 --- a/backend/api/EventHandlers/IsarConnectionEventHandler.cs +++ b/backend/api/EventHandlers/IsarConnectionEventHandler.cs @@ -18,8 +18,8 @@ public class IsarConnectionEventHandler : EventHandlerBase private IRobotService RobotService => _scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); - private IMissionService MissionService => - _scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); + private IMissionRunService MissionService => + _scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); private readonly Dictionary _isarConnectionTimers = new(); diff --git a/backend/api/EventHandlers/MissionScheduler.cs b/backend/api/EventHandlers/MissionScheduler.cs index a5f8b74bb..e2d0488a5 100644 --- a/backend/api/EventHandlers/MissionScheduler.cs +++ b/backend/api/EventHandlers/MissionScheduler.cs @@ -13,10 +13,10 @@ public class MissionScheduler : BackgroundService private readonly int _timeDelay; private readonly IServiceScopeFactory _scopeFactory; - private IList MissionQueue => + private IList MissionQueue => MissionService .ReadAll( - new MissionQueryStringParameters + new MissionRunQueryStringParameters { Statuses = new List { MissionStatus.Pending }, OrderBy = "DesiredStartTime", @@ -25,8 +25,8 @@ public class MissionScheduler : BackgroundService ) .Result; - private IMissionService MissionService => - _scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); + private IMissionRunService MissionService => + _scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); private RobotController RobotController => _scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); @@ -80,7 +80,7 @@ freshMission.Robot.Status is not RobotStatus.Available } } - private async Task StartMission(Mission queuedMission) + private async Task StartMission(MissionRun queuedMission) { var result = await RobotController.StartMission( queuedMission.Robot.Id, diff --git a/backend/api/EventHandlers/MqttEventHandler.cs b/backend/api/EventHandlers/MqttEventHandler.cs index dd32fb7e5..4b41e4318 100644 --- a/backend/api/EventHandlers/MqttEventHandler.cs +++ b/backend/api/EventHandlers/MqttEventHandler.cs @@ -248,7 +248,7 @@ private static void UpdateCurrentAssetIfChanged(string newCurrentAsset, ref Robo private async void OnMissionUpdate(object? sender, MqttReceivedArgs mqttArgs) { var provider = GetServiceProvider(); - var missionService = provider.GetRequiredService(); + var missionService = provider.GetRequiredService(); var robotService = provider.GetRequiredService(); var robotModelService = provider.GetRequiredService(); @@ -256,7 +256,7 @@ private async void OnMissionUpdate(object? sender, MqttReceivedArgs mqttArgs) MissionStatus status; try { - status = Mission.MissionStatusFromString(isarMission.Status); + status = MissionRun.MissionStatusFromString(isarMission.Status); } catch (ArgumentException e) { @@ -326,7 +326,7 @@ private async void OnMissionUpdate(object? sender, MqttReceivedArgs mqttArgs) .AddDays(-timeRangeInDays) .ToUnixTimeSeconds(); var missionsForEstimation = await missionService.ReadAll( - new MissionQueryStringParameters + new MissionRunQueryStringParameters { MinDesiredStartTime = minEpochTime, RobotModelType = robot.Model.Type, @@ -349,7 +349,7 @@ private async void OnMissionUpdate(object? sender, MqttReceivedArgs mqttArgs) private async void OnTaskUpdate(object? sender, MqttReceivedArgs mqttArgs) { var provider = GetServiceProvider(); - var missionService = provider.GetRequiredService(); + var missionService = provider.GetRequiredService(); var task = (IsarTaskMessage)mqttArgs.Message; IsarTaskStatus status; try @@ -387,7 +387,7 @@ private async void OnTaskUpdate(object? sender, MqttReceivedArgs mqttArgs) private async void OnStepUpdate(object? sender, MqttReceivedArgs mqttArgs) { var provider = GetServiceProvider(); - var missionService = provider.GetRequiredService(); + var missionService = provider.GetRequiredService(); var step = (IsarStepMessage)mqttArgs.Message; diff --git a/backend/api/Options/StorageOptions.cs b/backend/api/Options/StorageOptions.cs new file mode 100644 index 000000000..69657226e --- /dev/null +++ b/backend/api/Options/StorageOptions.cs @@ -0,0 +1,8 @@ +namespace Api.Options +{ + public class StorageOptions + { + public string CustomMissionContainerName { get; set; } = ""; + public string ConnectionString { get; set; } = ""; + } +} diff --git a/backend/api/Program.cs b/backend/api/Program.cs index 43534c1ab..771af4de4 100644 --- a/backend/api/Program.cs +++ b/backend/api/Program.cs @@ -40,15 +40,17 @@ builder.Services.AddApplicationInsightsTelemetry(); builder.Services.AddScoped(); -builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); -builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddHostedService(); builder.Services.AddHostedService(); @@ -57,6 +59,7 @@ builder.Services.Configure(builder.Configuration.GetSection("AzureAd")); builder.Services.Configure(builder.Configuration.GetSection("Maps")); +builder.Services.Configure(builder.Configuration.GetSection("Blob")); builder.Services .AddControllers() diff --git a/backend/api/Services/AreaService.cs b/backend/api/Services/AreaService.cs new file mode 100644 index 000000000..f62992d10 --- /dev/null +++ b/backend/api/Services/AreaService.cs @@ -0,0 +1,233 @@ +using Api.Controllers.Models; +using Api.Database.Context; +using Api.Database.Models; +using Microsoft.EntityFrameworkCore; + +namespace Api.Services +{ + public interface IAreaService + { + public abstract Task> ReadAll(); + + public abstract Task ReadById(string id); + + public abstract Task> ReadByAsset(string asset); + + public abstract Task ReadByAssetAndName(string asset, string name); + + public abstract Task Create(CreateAreaQuery newArea); + + public abstract Task Create(CreateAreaQuery newArea, List safePositions); + + public abstract Task Update(Area Area); + + public abstract Task AddSafePosition(string asset, string name, SafePosition safePosition); + + public abstract Task Delete(string id); + + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage( + "Globalization", + "CA1309:Use ordinal StringComparison", + Justification = "EF Core refrains from translating string comparison overloads to SQL" + )] + [System.Diagnostics.CodeAnalysis.SuppressMessage( + "Globalization", + "CA1304:Specify CultureInfo", + Justification = "Entity framework does not support translating culture info to SQL calls" + )] + public class AreaService : IAreaService + { + private readonly FlotillaDbContext _context; + + public AreaService(FlotillaDbContext context) + { + _context = context; + } + + public async Task> ReadAll() + { + return await GetAreas().ToListAsync(); + } + + private IQueryable GetAreas() + { + return _context.Areas.Include(a => a.SafePositions) + .Include(a => a.Deck).ThenInclude(d => d.Installation).ThenInclude(i => i.Asset); + } + + public async Task ReadById(string id) + { + return await GetAreas() + .FirstOrDefaultAsync(Area => Area.Id.Equals(id)); + } + + public async Task ReadByAssetAndName(string name) + { + return await _context.Areas.Where(a => + a.Name.ToLower().Equals(name.ToLower()) + ).Include(a => a.SafePositions) + .Include(a => a.Deck).ThenInclude(d => d.Installation).ThenInclude(i => i.Asset).FirstOrDefaultAsync(); + } + + public async Task> ReadByAsset(string asset) + { + return await _context.Areas.Where(a => + a.Deck.Installation.Asset.ShortName.Equals(asset.ToLower())).Include(a => a.SafePositions) + .Include(a => a.Deck).ThenInclude(d => d.Installation).ThenInclude(i => i.Asset).ToListAsync(); + } + + public async Task ReadByAssetAndName(string asset, string name) + { + // TODO: can we assume that this combination will be unique? Are area names specific enough? + return await _context.Areas.Where(a => + a.Deck.Installation.Asset.ShortName.ToLower().Equals(asset.ToLower()) && + a.Name.ToLower().Equals(name.ToLower()) + ).Include(a => a.Deck).ThenInclude(d => d.Installation).ThenInclude(i => i.Asset) + .Include(a => a.SafePositions).FirstOrDefaultAsync(); + } + + public async Task ReadAreaByAssetAndInstallationAndDeckAndName(string asset, string installation, string deck, string name) + { + return await _context.Areas.Where(a => + a.Deck.Installation.Asset.ShortName.ToLower().Equals(asset.ToLower()) && + a.Deck.Installation.ShortName.ToLower().Equals(installation.ToLower()) && + a.Deck.Name.ToLower().Equals(deck.ToLower()) && + a.Name.ToLower().Equals(name.ToLower()) + ).Include(a => a.Deck).ThenInclude(d => d.Installation).ThenInclude(i => i.Asset) + .Include(a => a.SafePositions).FirstOrDefaultAsync(); + } + + public async Task ReadDeckByAssetAndInstallationAndName(string asset, string installation, string name) + { + return await _context.Decks.Where(d => + d.Installation.Asset.ShortName.ToLower().Equals(asset.ToLower()) && + d.Installation.ShortName.ToLower().Equals(installation.ToLower()) && + d.Name.ToLower().Equals(name.ToLower()) + ).Include(a => a.Installation).ThenInclude(i => i.Asset).FirstOrDefaultAsync(); + } + + public async Task ReadInstallationByAssetAndName(string asset, string name) + { + return await _context.Installations.Where(i => + i.Asset.ShortName.ToLower().Equals(asset.ToLower()) && + i.Name.ToLower().Equals(name.ToLower()) + ).Include(i => i.Asset).FirstOrDefaultAsync(); + } + + public async Task ReadAssetByName(string asset) + { + return await _context.Assets.Where(a => + a.ShortName.ToLower().Equals(asset.ToLower()) + ).FirstOrDefaultAsync(); + } + + public async Task Create(CreateAreaQuery newAreaQuery, List safePositions) + { + var sp = new List(); + foreach (var p in safePositions) + { + sp.Add(new SafePosition(p)); + } + + var existingArea = ReadAreaByAssetAndInstallationAndDeckAndName( + newAreaQuery.AssetCode, + newAreaQuery.InstallationName, + newAreaQuery.DeckName, + newAreaQuery.AreaName); + if (existingArea != null) + { + // TODO: maybe just append safe positions, or return an error + } + + var deck = await ReadDeckByAssetAndInstallationAndName(newAreaQuery.AssetCode, newAreaQuery.InstallationName, newAreaQuery.DeckName); + if (deck == null) + { + var installation = await ReadInstallationByAssetAndName(newAreaQuery.AssetCode, newAreaQuery.InstallationName); + if (installation == null) + { + var asset = await ReadAssetByName(newAreaQuery.AssetCode); + if (asset == null) + { + asset = new Asset + { + Name = "", // TODO: + ShortName = newAreaQuery.AssetCode + }; + await _context.Assets.AddAsync(asset); + await _context.SaveChangesAsync(); + } + installation = new Installation + { + Asset = asset, + Name = "", // TODO: + ShortName = newAreaQuery.InstallationName + }; + await _context.Installations.AddAsync(installation); + await _context.SaveChangesAsync(); + } + deck = new Deck + { + Installation = installation, + Name = newAreaQuery.DeckName + }; + await _context.Decks.AddAsync(deck); + await _context.SaveChangesAsync(); + } + + var newArea = new Area + { + Name = newAreaQuery.AreaName, + DefaultLocalizationPose = newAreaQuery.DefaultLocalizationPose, + SafePositions = sp, + Map = new MapMetadata(), + Deck = deck + }; + await _context.Areas.AddAsync(newArea); + await _context.SaveChangesAsync(); + return newArea; + } + + public async Task Create(CreateAreaQuery newArea) + { + var area = await Create(newArea, new List()); + return area; + } + + public async Task AddSafePosition(string asset, string name, SafePosition safePosition) + { + var area = await ReadByAssetAndName(asset, name); + if (area is null) + { + return null; + } + area.SafePositions.Add(safePosition); + _context.Areas.Update(area); + await _context.SaveChangesAsync(); + return area; + } + + public async Task Update(Area area) + { + var entry = _context.Update(area); + await _context.SaveChangesAsync(); + return entry.Entity; + } + + public async Task Delete(string id) + { + var area = await GetAreas() + .FirstOrDefaultAsync(ev => ev.Id.Equals(id)); + if (area is null) + { + return null; + } + + _context.Areas.Remove(area); + await _context.SaveChangesAsync(); + + return area; + } + } +} diff --git a/backend/api/Services/AssetDeckService.cs b/backend/api/Services/AssetDeckService.cs deleted file mode 100644 index 9a5980bda..000000000 --- a/backend/api/Services/AssetDeckService.cs +++ /dev/null @@ -1,140 +0,0 @@ -using Api.Controllers.Models; -using Api.Database.Context; -using Api.Database.Models; -using Microsoft.EntityFrameworkCore; - -namespace Api.Services -{ - public interface IAssetDeckService - { - public abstract Task> ReadAll(); - - public abstract Task ReadById(string id); - - public abstract Task> ReadByAsset(string asset); - - public abstract Task ReadByAssetAndDeck(string asset, string deck); - - public abstract Task Create(CreateAssetDeckQuery newAssetDeck); - - public abstract Task Create(CreateAssetDeckQuery newAssetDeck, List safePositions); - - public abstract Task Update(AssetDeck assetDeck); - - public abstract Task AddSafePosition(string asset, string deck, SafePosition safePosition); - - public abstract Task Delete(string id); - - } - - [System.Diagnostics.CodeAnalysis.SuppressMessage( - "Globalization", - "CA1309:Use ordinal StringComparison", - Justification = "EF Core refrains from translating string comparison overloads to SQL" - )] - [System.Diagnostics.CodeAnalysis.SuppressMessage( - "Globalization", - "CA1304:Specify CultureInfo", - Justification = "Entity framework does not support translating culture info to SQL calls" - )] - public class AssetDeckService : IAssetDeckService - { - private readonly FlotillaDbContext _context; - - public AssetDeckService(FlotillaDbContext context) - { - _context = context; - } - - public async Task> ReadAll() - { - return await GetAssetDecks().ToListAsync(); - } - - private IQueryable GetAssetDecks() - { - return _context.AssetDecks.Include(a => a.SafePositions); - } - - public async Task ReadById(string id) - { - return await GetAssetDecks() - .FirstOrDefaultAsync(assetDeck => assetDeck.Id.Equals(id)); - } - - public async Task> ReadByAsset(string asset) - { - - return await _context.AssetDecks.Where(a => - a.AssetCode.ToLower().Equals(asset.ToLower())).Include(a => a.SafePositions).ToListAsync(); - } - - public async Task ReadByAssetAndDeck(string asset, string deck) - { - return await _context.AssetDecks.Where(a => - a.AssetCode.ToLower().Equals(asset.ToLower()) && - a.DeckName.ToLower().Equals(deck.ToLower()) - ).Include(a => a.SafePositions).FirstOrDefaultAsync(); - } - - public async Task Create(CreateAssetDeckQuery newAssetDeck, List safePositions) - { - var sp = new List(); - foreach (var p in safePositions) - { - sp.Add(new SafePosition(p)); - } - var assetDeck = new AssetDeck - { - AssetCode = newAssetDeck.AssetCode, - DeckName = newAssetDeck.DeckName, - DefaultLocalizationPose = newAssetDeck.DefaultLocalizationPose, - SafePositions = sp - }; - await _context.AssetDecks.AddAsync(assetDeck); - await _context.SaveChangesAsync(); - return assetDeck; - } - - public async Task Create(CreateAssetDeckQuery newAssetDeck) - { - var assetDeck = await Create(newAssetDeck, new List()); - return assetDeck; - } - - public async Task AddSafePosition(string asset, string deck, SafePosition safePosition) - { - var assetDeck = await ReadByAssetAndDeck(asset, deck); - if (assetDeck is null) - { - return null; - } - assetDeck.SafePositions.Add(safePosition); - _context.AssetDecks.Update(assetDeck); - await _context.SaveChangesAsync(); - return assetDeck; - } - - public async Task Update(AssetDeck assetDeck) - { - var entry = _context.Update(assetDeck); - await _context.SaveChangesAsync(); - return entry.Entity; - } - - public async Task Delete(string id) - { - var assetDeck = await GetAssetDecks() - .FirstOrDefaultAsync(ev => ev.Id.Equals(id)); - if (assetDeck is null) - { - return null; - } - - _context.AssetDecks.Remove(assetDeck); - await _context.SaveChangesAsync(); - - return assetDeck; - } - } -} diff --git a/backend/api/Services/EchoService.cs b/backend/api/Services/EchoService.cs index f4ed55622..5648be8c7 100644 --- a/backend/api/Services/EchoService.cs +++ b/backend/api/Services/EchoService.cs @@ -10,7 +10,7 @@ namespace Api.Services { public interface IEchoService { - public abstract Task> GetAvailableMissions(string? installationCode); + public abstract Task> GetAvailableMissions(string? installationCode); public abstract Task GetMissionById(int missionId); @@ -31,7 +31,7 @@ public EchoService(IDownstreamWebApi downstreamWebApi, ILogger logg _logger = logger; } - public async Task> GetAvailableMissions(string? installationCode) + public async Task> GetAvailableMissions(string? installationCode) { string relativePath = string.IsNullOrEmpty(installationCode) ? $"robots/robot-plan?Status=Ready" @@ -88,6 +88,33 @@ public async Task GetMissionById(int missionId) return mission; } + public async Task GetMissionByPath(string relativePath) + { + var response = await _echoApi.CallWebApiForAppAsync( + ServiceName, + options => + { + options.HttpMethod = HttpMethod.Get; + options.RelativePath = relativePath; + } + ); + + response.EnsureSuccessStatusCode(); + + var echoMission = await response.Content.ReadFromJsonAsync(); + + if (echoMission is null) + throw new JsonException("Failed to deserialize mission from Echo"); + + var mission = ProcessEchoMission(echoMission); + if (mission == null) + { + throw new InvalidDataException($"EchoMission with relative path: {relativePath} is invalid."); + } + + return mission; + } + public async Task> GetEchoPlantInfos() { string relativePath = "plantinfo"; @@ -170,9 +197,9 @@ private List ProcessPlanItems(List planItems, string installa return tags; } - private List ProcessAvailableEchoMission(List echoMissions) + private List ProcessAvailableEchoMission(List echoMissions) { - var availableMissions = new List(); + var availableMissions = new List(); foreach (var echoMission in echoMissions) { @@ -180,7 +207,7 @@ private List ProcessAvailableEchoMission(List StartMission(Robot robot, Mission mission); + public abstract Task StartMission(Robot robot, MissionRun mission); public abstract Task StopMission(Robot robot); @@ -68,7 +68,7 @@ private async Task CallApi( return response; } - public async Task StartMission(Robot robot, Mission mission) + public async Task StartMission(Robot robot, MissionRun mission) { var response = await CallApi( HttpMethod.Post, diff --git a/backend/api/Services/MapService.cs b/backend/api/Services/MapService.cs index 6aee8aa7f..0977dba10 100644 --- a/backend/api/Services/MapService.cs +++ b/backend/api/Services/MapService.cs @@ -9,7 +9,7 @@ public interface IMapService { public Task FetchMapImage(string mapName, string assetCode); public Task ChooseMapFromPositions(IList positions, string assetCode); - public Task AssignMapToMission(Mission mission); + public Task AssignMapToMission(MissionRun mission); } public class MapService : IMapService @@ -70,7 +70,7 @@ public async Task FetchMapImage(string mapName, string assetCode) return map; } - public async Task AssignMapToMission(Mission mission) + public async Task AssignMapToMission(MissionRun mission) { MapMetadata? mapMetadata; var positions = new List(); diff --git a/backend/api/Services/MissionDefinitionService.cs b/backend/api/Services/MissionDefinitionService.cs new file mode 100644 index 000000000..0bb75554b --- /dev/null +++ b/backend/api/Services/MissionDefinitionService.cs @@ -0,0 +1,226 @@ +using System.Linq.Dynamic.Core; +using System.Linq.Expressions; +using System.Reflection; +using System.Text; +using Api.Controllers.Models; +using Api.Database.Context; +using Api.Database.Models; +using Api.Utilities; +using Microsoft.EntityFrameworkCore; + +namespace Api.Services +{ + public interface IMissionDefinitionService + { + public abstract Task Create(MissionDefinition mission); + + public abstract Task ReadById(string id); + + public abstract Task> ReadAll(MissionDefinitionQueryStringParameters parameters); + + public abstract Task Update(MissionDefinition mission); + + public abstract Task Delete(string id); + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage( + "Globalization", + "CA1309:Use ordinal StringComparison", + Justification = "EF Core refrains from translating string comparison overloads to SQL" + )] + [System.Diagnostics.CodeAnalysis.SuppressMessage( + "Globalization", + "CA1304:Specify CultureInfo", + Justification = "Entity framework does not support translating culture info to SQL calls" + )] + [System.Diagnostics.CodeAnalysis.SuppressMessage( + "Globalization", + "CA1307:Specify CultureInfo", + Justification = "Entity framework does not support translating culture info to SQL calls" + )] + public class MissionDefinitionService : IMissionDefinitionService + { + private readonly FlotillaDbContext _context; + private readonly ILogger _logger; + + public MissionDefinitionService(FlotillaDbContext context, ILogger logger) + { + _context = context; + _logger = logger; + } + + public async Task Create(MissionDefinition mission) + { + await _context.MissionDefinitions.AddAsync(mission); + await _context.SaveChangesAsync(); + + return mission; + } + + private IQueryable GetMissionsWithSubModels() + { + return _context.MissionDefinitions + .Include(mission => mission.Area) + .ThenInclude(robot => robot.Deck) + .ThenInclude(robot => robot.Installation) + .ThenInclude(robot => robot.Asset) + .Include(mission => mission.Source) + .Include(mission => mission.LastRun) + .ThenInclude(planTask => planTask == null ? null : planTask.StartTime); + } + + public async Task ReadById(string id) + { + return await GetMissionsWithSubModels() + .FirstOrDefaultAsync(mission => mission.Id.Equals(id)); + } + + public async Task> ReadAll(MissionDefinitionQueryStringParameters parameters) + { + var query = GetMissionsWithSubModels(); + var filter = ConstructFilter(parameters); + + query = query.Where(filter); + + SearchByName(ref query, parameters.NameSearch); + + ApplySort(ref query, parameters.OrderBy); + + return await PagedList.ToPagedListAsync( + query, + parameters.PageNumber, + parameters.PageSize + ); + } + + public async Task Update(MissionDefinition mission) + { + var entry = _context.Update(mission); + await _context.SaveChangesAsync(); + return entry.Entity; + } + + public async Task Delete(string id) + { + // We do not delete the source here as more than one mission definition may be using it + var mission = await ReadById(id); + if (mission is null) + { + return null; + } + + _context.MissionDefinitions.Remove(mission); + await _context.SaveChangesAsync(); + + return mission; + } + + private static void SearchByName(ref IQueryable missions, string? name) + { + if (!missions.Any() || string.IsNullOrWhiteSpace(name)) + return; + + missions = missions.Where( + mission => + mission.Name != null && mission.Name.ToLower().Contains(name.Trim().ToLower()) + ); + } + + /// + /// Filters by and + /// + /// Uses LINQ Expression trees (see ) + /// + /// The variable containing the filter params + private static Expression> ConstructFilter( + MissionDefinitionQueryStringParameters parameters + ) + { + Expression> areaFilter = parameters.Area is null + ? mission => true + : mission => + mission.Area.Name.ToLower().Equals(parameters.Area.Trim().ToLower()); + + Expression> assetFilter = parameters.AssetCode is null + ? mission => true + : mission => + mission.AssetCode.ToLower().Equals(parameters.AssetCode.Trim().ToLower()); + + Expression> missionTypeFilter = parameters.SourceType is null + ? mission => true + : mission => + mission.Source.Type.Equals(parameters.SourceType); + + // The parameter of the filter expression + var mission = Expression.Parameter(typeof(MissionRun)); + + // Combining the body of the filters to create the combined filter, using invoke to force parameter substitution + Expression body = Expression.AndAlso( + Expression.Invoke(assetFilter, mission), + Expression.AndAlso( + Expression.Invoke(areaFilter, mission), + Expression.Invoke(missionTypeFilter, mission) + ) + ); + + // Constructing the resulting lambda expression by combining parameter and body + return Expression.Lambda>(body, mission); + } + + private static void ApplySort(ref IQueryable missions, string orderByQueryString) + { + if (!missions.Any()) + return; + + if (string.IsNullOrWhiteSpace(orderByQueryString)) + { + missions = missions.OrderBy(x => x.Name); + return; + } + + string[] orderParams = orderByQueryString + .Trim() + .Split(',') + .Select(parameterString => parameterString.Trim()) + .ToArray(); + + var propertyInfos = typeof(MissionDefinition).GetProperties( + BindingFlags.Public | BindingFlags.Instance + ); + var orderQueryBuilder = new StringBuilder(); + + foreach (string param in orderParams) + { + if (string.IsNullOrWhiteSpace(param)) + continue; + + string propertyFromQueryName = param.Split(" ")[0]; + var objectProperty = propertyInfos.FirstOrDefault( + pi => + pi.Name.Equals( + propertyFromQueryName, + StringComparison.InvariantCultureIgnoreCase + ) + ); + + if (objectProperty == null) + throw new InvalidDataException( + $"Mission has no property '{propertyFromQueryName}' for ordering" + ); + + string sortingOrder = param.EndsWith(" desc", StringComparison.OrdinalIgnoreCase) + ? "descending" + : "ascending"; + + string sortParameter = $"{objectProperty.Name} {sortingOrder}, "; + orderQueryBuilder.Append(sortParameter); + } + + string orderQuery = orderQueryBuilder.ToString().TrimEnd(',', ' '); + + missions = string.IsNullOrWhiteSpace(orderQuery) + ? missions.OrderBy(mission => mission.Name) + : missions.OrderBy(orderQuery); + } + } +} diff --git a/backend/api/Services/MissionService.cs b/backend/api/Services/MissionRunService.cs similarity index 80% rename from backend/api/Services/MissionService.cs rename to backend/api/Services/MissionRunService.cs index d656d2284..a5efe1b54 100644 --- a/backend/api/Services/MissionService.cs +++ b/backend/api/Services/MissionRunService.cs @@ -12,17 +12,17 @@ namespace Api.Services { - public interface IMissionService + public interface IMissionRunService { - public abstract Task Create(Mission mission); + public abstract Task Create(MissionRun mission); - public abstract Task> ReadAll(MissionQueryStringParameters parameters); + public abstract Task> ReadAll(MissionRunQueryStringParameters parameters); - public abstract Task ReadById(string id); + public abstract Task ReadById(string id); - public abstract Task Update(Mission mission); + public abstract Task Update(MissionRun mission); - public abstract Task UpdateMissionStatusByIsarMissionId( + public abstract Task UpdateMissionStatusByIsarMissionId( string isarMissionId, MissionStatus missionStatus ); @@ -40,7 +40,7 @@ public abstract Task UpdateStepStatusByIsarStepId( IsarStepStatus stepStatus ); - public abstract Task Delete(string id); + public abstract Task Delete(string id); } [System.Diagnostics.CodeAnalysis.SuppressMessage( @@ -58,20 +58,24 @@ IsarStepStatus stepStatus "CA1307:Specify CultureInfo", Justification = "Entity framework does not support translating culture info to SQL calls" )] - public class MissionService : IMissionService + public class MissionRunService : IMissionRunService { private readonly FlotillaDbContext _context; - private readonly ILogger _logger; + private readonly ILogger _logger; - public MissionService(FlotillaDbContext context, ILogger logger) + public MissionRunService(FlotillaDbContext context, ILogger logger) { _context = context; _logger = logger; } - private IQueryable GetMissionsWithSubModels() + private IQueryable GetMissionsWithSubModels() { - return _context.Missions + return _context.MissionRuns + .Include(mission => mission.Area) + .ThenInclude(a => a.Deck) + .ThenInclude(d => d.Installation) + .ThenInclude(i => i.Asset) .Include(mission => mission.Robot) .ThenInclude(robot => robot.VideoStreams) .Include(mission => mission.Robot) @@ -82,15 +86,15 @@ private IQueryable GetMissionsWithSubModels() .ThenInclude(task => task.Inspections); } - public async Task Create(Mission mission) + public async Task Create(MissionRun mission) { - await _context.Missions.AddAsync(mission); + await _context.MissionRuns.AddAsync(mission); await _context.SaveChangesAsync(); return mission; } - public async Task> ReadAll(MissionQueryStringParameters parameters) + public async Task> ReadAll(MissionRunQueryStringParameters parameters) { var query = GetMissionsWithSubModels(); var filter = ConstructFilter(parameters); @@ -103,27 +107,27 @@ public async Task> ReadAll(MissionQueryStringParameters param ApplySort(ref query, parameters.OrderBy); - return await PagedList.ToPagedListAsync( + return await PagedList.ToPagedListAsync( query, parameters.PageNumber, parameters.PageSize ); } - public async Task ReadById(string id) + public async Task ReadById(string id) { return await GetMissionsWithSubModels() .FirstOrDefaultAsync(mission => mission.Id.Equals(id)); } - public async Task Update(Mission mission) + public async Task Update(MissionRun mission) { var entry = _context.Update(mission); await _context.SaveChangesAsync(); return entry.Entity; } - public async Task Delete(string id) + public async Task Delete(string id) { var mission = await GetMissionsWithSubModels() .FirstOrDefaultAsync(ev => ev.Id.Equals(id)); @@ -132,7 +136,7 @@ public async Task Update(Mission mission) return null; } - _context.Missions.Remove(mission); + _context.MissionRuns.Remove(mission); await _context.SaveChangesAsync(); return mission; @@ -140,7 +144,7 @@ public async Task Update(Mission mission) #region ISAR Specific methods - private async Task ReadByIsarMissionId(string isarMissionId) + private async Task ReadByIsarMissionId(string isarMissionId) { return await GetMissionsWithSubModels() .FirstOrDefaultAsync( @@ -149,7 +153,7 @@ public async Task Update(Mission mission) ); } - public async Task UpdateMissionStatusByIsarMissionId( + public async Task UpdateMissionStatusByIsarMissionId( string isarMissionId, MissionStatus missionStatus ) @@ -270,7 +274,7 @@ IsarStepStatus stepStatus #endregion ISAR Specific methods - private static void SearchByName(ref IQueryable missions, string? name) + private static void SearchByName(ref IQueryable missions, string? name) { if (!missions.Any() || string.IsNullOrWhiteSpace(name)) return; @@ -281,7 +285,7 @@ private static void SearchByName(ref IQueryable missions, string? name) ); } - private static void SearchByRobotName(ref IQueryable missions, string? robotName) + private static void SearchByRobotName(ref IQueryable missions, string? robotName) { if (!missions.Any() || string.IsNullOrWhiteSpace(robotName)) return; @@ -291,7 +295,7 @@ private static void SearchByRobotName(ref IQueryable missions, string? ); } - private static void SearchByTag(ref IQueryable missions, string? tag) + private static void SearchByTag(ref IQueryable missions, string? tag) { if (!missions.Any() || string.IsNullOrWhiteSpace(tag)) return; @@ -307,33 +311,38 @@ private static void SearchByTag(ref IQueryable missions, string? tag) } /// - /// Filters by and + /// Filters by and /// /// Uses LINQ Expression trees (see ) /// /// The variable containing the filter params - private static Expression> ConstructFilter( - MissionQueryStringParameters parameters + private static Expression> ConstructFilter( + MissionRunQueryStringParameters parameters ) { - Expression> assetFilter = parameters.AssetCode is null + Expression> areaFilter = parameters.Area is null + ? mission => true + : mission => + mission.Area.Name.ToLower().Equals(parameters.Area.Trim().ToLower()); + + Expression> assetFilter = parameters.AssetCode is null ? mission => true : mission => mission.AssetCode.ToLower().Equals(parameters.AssetCode.Trim().ToLower()); - Expression> statusFilter = parameters.Statuses is null + Expression> statusFilter = parameters.Statuses is null ? mission => true : mission => parameters.Statuses.Contains(mission.Status); - Expression> robotTypeFilter = parameters.RobotModelType is null + Expression> robotTypeFilter = parameters.RobotModelType is null ? mission => true : mission => mission.Robot.Model.Type.Equals(parameters.RobotModelType); - Expression> robotIdFilter = parameters.RobotId is null + Expression> robotIdFilter = parameters.RobotId is null ? mission => true : mission => mission.Robot.Id.Equals(parameters.RobotId); - Expression> inspectionTypeFilter = parameters.InspectionTypes is null + Expression> inspectionTypeFilter = parameters.InspectionTypes is null ? mission => true : mission => mission.Tasks.Any( task => @@ -344,7 +353,7 @@ MissionQueryStringParameters parameters var minStartTime = DateTimeOffset.FromUnixTimeSeconds(parameters.MinStartTime); var maxStartTime = DateTimeOffset.FromUnixTimeSeconds(parameters.MaxStartTime); - Expression> startTimeFilter = mission => + Expression> startTimeFilter = mission => mission.StartTime == null || ( DateTimeOffset.Compare(mission.StartTime.Value, minStartTime) >= 0 @@ -353,7 +362,7 @@ MissionQueryStringParameters parameters var minEndTime = DateTimeOffset.FromUnixTimeSeconds(parameters.MinEndTime); var maxEndTime = DateTimeOffset.FromUnixTimeSeconds(parameters.MaxEndTime); - Expression> endTimeFilter = mission => + Expression> endTimeFilter = mission => mission.EndTime == null || ( DateTimeOffset.Compare(mission.EndTime.Value, minEndTime) >= 0 @@ -366,12 +375,12 @@ MissionQueryStringParameters parameters var maxDesiredStartTime = DateTimeOffset.FromUnixTimeSeconds( parameters.MaxDesiredStartTime ); - Expression> desiredStartTimeFilter = mission => + Expression> desiredStartTimeFilter = mission => DateTimeOffset.Compare(mission.DesiredStartTime, minDesiredStartTime) >= 0 && DateTimeOffset.Compare(mission.DesiredStartTime, maxDesiredStartTime) <= 0; // The parameter of the filter expression - var mission = Expression.Parameter(typeof(Mission)); + var mission = Expression.Parameter(typeof(MissionRun)); // Combining the body of the filters to create the combined filter, using invoke to force parameter substitution Expression body = Expression.AndAlso( @@ -398,10 +407,10 @@ MissionQueryStringParameters parameters ); // Constructing the resulting lambda expression by combining parameter and body - return Expression.Lambda>(body, mission); + return Expression.Lambda>(body, mission); } - private static void ApplySort(ref IQueryable missions, string orderByQueryString) + private static void ApplySort(ref IQueryable missions, string orderByQueryString) { if (!missions.Any()) return; @@ -418,7 +427,7 @@ private static void ApplySort(ref IQueryable missions, string orderByQu .Select(parameterString => parameterString.Trim()) .ToArray(); - var propertyInfos = typeof(Mission).GetProperties( + var propertyInfos = typeof(MissionRun).GetProperties( BindingFlags.Public | BindingFlags.Instance ); var orderQueryBuilder = new StringBuilder(); diff --git a/backend/api/Services/Models/IsarMissionDefinition.cs b/backend/api/Services/Models/IsarMissionDefinition.cs index 8144386b7..d230f72e7 100644 --- a/backend/api/Services/Models/IsarMissionDefinition.cs +++ b/backend/api/Services/Models/IsarMissionDefinition.cs @@ -25,7 +25,7 @@ public IsarMissionDefinition(List tasks) Tasks = tasks; } - public IsarMissionDefinition(Mission mission) + public IsarMissionDefinition(MissionRun mission) { Id = mission.IsarMissionId; Name = mission.Name; @@ -47,7 +47,7 @@ public struct IsarTaskDefinition [JsonPropertyName("inspections")] public List Inspections { get; set; } - public IsarTaskDefinition(MissionTask missionTask, Mission mission) + public IsarTaskDefinition(MissionTask missionTask, MissionRun mission) { Id = missionTask.IsarTaskId; Pose = new IsarPose(missionTask.RobotPose); @@ -81,7 +81,7 @@ public struct IsarInspectionDefinition [JsonPropertyName("metadata")] public Dictionary? Metadata { get; set; } - public IsarInspectionDefinition(Inspection inspection, MissionTask task, Mission mission) + public IsarInspectionDefinition(Inspection inspection, MissionTask task, MissionRun mission) { Id = inspection.IsarStepId; Type = inspection.InspectionType.ToString(); diff --git a/backend/api/Services/RobotService.cs b/backend/api/Services/RobotService.cs index 6721be469..a668c6ed9 100644 --- a/backend/api/Services/RobotService.cs +++ b/backend/api/Services/RobotService.cs @@ -30,7 +30,7 @@ public RobotService(FlotillaDbContext context) private IQueryable GetRobotsWithSubModels() { - return _context.Robots.Include(r => r.VideoStreams).Include(r => r.Model).Include(r => r.CurrentAssetDeck); + return _context.Robots.Include(r => r.VideoStreams).Include(r => r.Model).Include(r => r.CurrentArea); } public async Task Create(Robot newRobot) diff --git a/backend/api/Services/SourceService.cs b/backend/api/Services/SourceService.cs new file mode 100644 index 000000000..a98059971 --- /dev/null +++ b/backend/api/Services/SourceService.cs @@ -0,0 +1,100 @@ + + +using System.Text.Json; +using System.Text.Json.Serialization; +using Api.Database.Models; +using Api.Options; +using Azure.Storage; +using Azure.Storage.Blobs; +using Microsoft.Extensions.Options; + +namespace Api.Services +{ + + public interface ISourceService + { + Task UploadSource(string id, List tasks); + List? GetMissionTasksFromMissionId(string id); + List? GetMissionTasksFromURL(string url); + } + + public class SourceService : ISourceService + { + private readonly IOptions _storageOptions; + private readonly ILogger _logger; + + public SourceService(IOptions storageOptions, ILogger logger) + { + _storageOptions = storageOptions; + _logger = logger; + } + + public async Task CreateContainer(string containerName) + { + var blobServiceClient = new BlobServiceClient(_storageOptions.Value.ConnectionString); + var containerClient = blobServiceClient.GetBlobContainerClient(containerName); + await containerClient.CreateIfNotExistsAsync(); + + return true; + } + + public async Task UploadFile(string fileName, Stream fileStream) + { + var blobServiceClient = new BlobServiceClient(_storageOptions.Value.ConnectionString); + var containerClient = blobServiceClient.GetBlobContainerClient(_storageOptions.Value.CustomMissionContainerName); + containerClient.CreateIfNotExists(); + + var blobClient = containerClient.GetBlobClient(fileName); + _ = await blobClient.UploadAsync(fileStream, true); + //var hash = $"0x{BitConverter.ToString(blobProperties.Value.ContentHash).Replace("-", string.Empty)}"; + return blobClient.Uri; + } + + public Task UploadSource(string id, List tasks) + { + var memoryStream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(JsonSerializer.Serialize(tasks))); + + var taskUri = UploadFile(id, memoryStream); + + return taskUri; + } + + public List? GetMissionTasksFromMissionId(string id) + { + var blobServiceClient = new BlobServiceClient(_storageOptions.Value.ConnectionString); + var containerClient = blobServiceClient.GetBlobContainerClient(_storageOptions.Value.CustomMissionContainerName); + containerClient.CreateIfNotExists(); + + var blobClient = containerClient.GetBlobClient(id); + + List? content = null; + try + { + content = blobClient.DownloadContent().Value.Content.ToObjectFromJson>(); + } + catch (System.Exception) + { + return null; + } + + return content; + } + + public List? GetMissionTasksFromURL(string url) + { + var blobClient = new BlobClient(new Uri(url)); + + List? content = null; + try + { + content = blobClient.DownloadContent().Value.Content.ToObjectFromJson>(); + } + catch (System.Exception) + { + return null; + } + + return content; + } + } +} diff --git a/backend/api/appsettings.Development.json b/backend/api/appsettings.Development.json index 7a5c89e15..47f239155 100644 --- a/backend/api/appsettings.Development.json +++ b/backend/api/appsettings.Development.json @@ -35,6 +35,10 @@ "MaxRetryAttempts": 5, "ShouldFailOnMaxRetries": false }, + "Blob": { + "CustomMissionContainerName": "custommission", + "ConnectionString": "" + }, "Database": { "UseInMemoryDatabase": true } diff --git a/backend/api/appsettings.Production.json b/backend/api/appsettings.Production.json index eca23c9a6..18688b217 100644 --- a/backend/api/appsettings.Production.json +++ b/backend/api/appsettings.Production.json @@ -16,6 +16,10 @@ "AllowedOrigins": [ "https://*.equinor.com/" ], + "Blob": { + "CustomMissionContainerName": "", + "AccountName": "" + }, "Mqtt": { "Host": "localhost", "Port": 1883, diff --git a/backend/api/appsettings.Staging.json b/backend/api/appsettings.Staging.json index 65c24f9e6..cf8e20187 100644 --- a/backend/api/appsettings.Staging.json +++ b/backend/api/appsettings.Staging.json @@ -18,6 +18,10 @@ "http://localhost:3001", "https://localhost:3001" ], + "Blob": { + "CustomMissionContainerName": "", + "AccountName": "" + }, "Mqtt": { "Host": "localhost", "Port": 1883, diff --git a/backend/api/appsettings.Test.json b/backend/api/appsettings.Test.json index 16ca42855..1fb3dab01 100644 --- a/backend/api/appsettings.Test.json +++ b/backend/api/appsettings.Test.json @@ -11,6 +11,10 @@ "http://localhost:3001", "https://localhost:3001" ], + "Blob": { + "CustomMissionContainerName": "", + "AccountName": "" + }, "Mqtt": { "Host": "localhost", "Port": 1883, diff --git a/flotilla.code-workspace b/flotilla.code-workspace new file mode 100644 index 000000000..0097397f4 --- /dev/null +++ b/flotilla.code-workspace @@ -0,0 +1,28 @@ +{ + "folders": [ + { + "name": "Frontend", + "path": "frontend/" + }, + { + "name": "Backend", + "path": "backend" + }, + { + "name": "Root", + "path": "." + } + ], + "launch": { + "version": "0.2.0", + "compounds": [ + { + "name": "Launch flotilla", + "configurations": [ + "Launch backend", + "Launch frontend" + ] + } + ] + } +} \ No newline at end of file diff --git a/frontend/src/api/ApiCaller.tsx b/frontend/src/api/ApiCaller.tsx index e9921c81d..330c5bc1d 100644 --- a/frontend/src/api/ApiCaller.tsx +++ b/frontend/src/api/ApiCaller.tsx @@ -4,7 +4,8 @@ import { Mission } from 'models/Mission' import { Robot } from 'models/Robot' import { VideoStream } from 'models/VideoStream' import { filterRobots } from 'utils/filtersAndSorts' -import { MissionQueryParameters } from 'models/MissionQueryParameters' +import { MissionRunQueryParameters } from 'models/MissionRunQueryParameters' +import { MissionDefinitionQueryParameters, SourceType } from 'models/MissionDefinitionQueryParameters' import { PaginatedResponse, PaginationHeader, PaginationHeaderName } from 'models/PaginatedResponse' import { Pose } from 'models/Pose' import { AssetDeck } from 'models/AssetDeck' @@ -14,6 +15,7 @@ import { TaskStatus } from 'models/Task' import { CreateCustomMission, CustomMissionQuery } from 'models/CustomMission' import { MapMetadata } from 'models/MapMetadata' import { MissionDefinition } from 'models/MissionDefinition' +import { EchoMission } from 'models/EchoMission' /** Implements the request sent to the backend api. */ export class BackendAPICaller { @@ -125,8 +127,17 @@ export class BackendAPICaller { return result.content } - static async getMissions(parameters: MissionQueryParameters): Promise> { - let path: string = 'missions?' + static async getAllEchoMissions(): Promise { + const path: string = 'echo/missions' + const result = await BackendAPICaller.GET(path).catch((e) => { + console.error(`Failed to GET /${path}: ` + e) + throw e + }) + return result.content + } + + static async getMissionRuns(parameters: MissionRunQueryParameters): Promise> { + let path: string = 'missions/runs?' // Always filter by currently selected asset const assetCode: string | null = BackendAPICaller.assetCode @@ -143,6 +154,7 @@ export class BackendAPICaller { }) } + if (parameters.area) path = path + 'Area=' + parameters.area + '&' if (parameters.pageNumber) path = path + 'PageNumber=' + parameters.pageNumber + '&' if (parameters.pageSize) path = path + 'PageSize=' + parameters.pageSize + '&' if (parameters.orderBy) path = path + 'OrderBy=' + parameters.orderBy + '&' @@ -177,6 +189,43 @@ export class BackendAPICaller { return result.content } + static async getMissionDefinitions( + parameters: MissionDefinitionQueryParameters + ): Promise> { + let path: string = 'missions/definitions?' + + // Always filter by currently selected asset + const assetCode: string | null = BackendAPICaller.assetCode + if (assetCode) path = path + 'AssetCode=' + assetCode + '&' + + if (parameters.area) path = path + 'Area=' + parameters.area + '&' + if (parameters.sourceType) path = path + 'SourceType=' + parameters.sourceType + '&' + if (parameters.pageNumber) path = path + 'PageNumber=' + parameters.pageNumber + '&' + if (parameters.pageSize) path = path + 'PageSize=' + parameters.pageSize + '&' + if (parameters.orderBy) path = path + 'OrderBy=' + parameters.orderBy + '&' + if (parameters.nameSearch) path = path + 'NameSearch=' + parameters.nameSearch + '&' + if (parameters.sourceType) path = path + 'SourceType=' + parameters.sourceType + '&' + + const result = await BackendAPICaller.GET(path).catch((e) => { + console.error(`Failed to GET /${path}: ` + e) + throw e + }) + if (!result.headers.has(PaginationHeaderName)) { + console.error('No Pagination header received ("' + PaginationHeaderName + '")') + } + const pagination: PaginationHeader = JSON.parse(result.headers.get(PaginationHeaderName)!) + return { pagination: pagination, content: result.content } + } + + static async getEchoMissions(installationCode: string = ''): Promise { + const path: string = 'echo/missions?installationCode=' + installationCode + const result = await BackendAPICaller.GET(path).catch((e) => { + console.error(`Failed to GET /${path}: ` + e) + throw e + }) + return result.content + } + static async getMissionById(missionId: string): Promise { const path: string = 'missions/' + missionId const result = await BackendAPICaller.GET(path).catch((e) => { @@ -212,6 +261,7 @@ export class BackendAPICaller { echoMissionId: echoMissionId, desiredStartTime: new Date(), assetCode: assetCode, + areaName: '', } const result = await BackendAPICaller.POST(path, body).catch((e) => { console.error(`Failed to POST /${path}: ` + e) @@ -312,6 +362,8 @@ export class BackendAPICaller { static async reRunMission(missionId: string, failedTasksOnly: boolean = false): Promise { let mission = await this.getMissionById(missionId) + // TODO: utilise reschedule endpoint instead of copying + if (failedTasksOnly) { mission.tasks = mission.tasks.filter( (task) => task.status !== TaskStatus.PartiallySuccessful && task.status !== TaskStatus.Successful diff --git a/frontend/src/components/Pages/FrontPage/MissionOverview/FailedMissionAlertView.tsx b/frontend/src/components/Pages/FrontPage/MissionOverview/FailedMissionAlertView.tsx index 84a570726..4bae2a873 100644 --- a/frontend/src/components/Pages/FrontPage/MissionOverview/FailedMissionAlertView.tsx +++ b/frontend/src/components/Pages/FrontPage/MissionOverview/FailedMissionAlertView.tsx @@ -106,7 +106,7 @@ export function FailedMissionAlertView({ refreshInterval }: RefreshProps) { const updateRecentFailedMissions = useCallback(() => { const lastDismissTime: Date = getLastDismissalTime() - BackendAPICaller.getMissions({ statuses: [MissionStatus.Failed], pageSize: PageSize }).then((missions) => { + BackendAPICaller.getMissionRuns({ statuses: [MissionStatus.Failed], pageSize: PageSize }).then((missions) => { const newRecentFailedMissions = missions.content.filter((m) => new Date(m.endTime!) > lastDismissTime) setRecentFailedMissions(newRecentFailedMissions) }) diff --git a/frontend/src/components/Pages/FrontPage/MissionOverview/MissionQueueView.tsx b/frontend/src/components/Pages/FrontPage/MissionOverview/MissionQueueView.tsx index b237f70bf..8cc0af54f 100644 --- a/frontend/src/components/Pages/FrontPage/MissionOverview/MissionQueueView.tsx +++ b/frontend/src/components/Pages/FrontPage/MissionOverview/MissionQueueView.tsx @@ -109,7 +109,7 @@ export function MissionQueueView({ refreshInterval }: RefreshProps) { useEffect(() => { const id = setInterval(() => { - BackendAPICaller.getMissions({ + BackendAPICaller.getMissionRuns({ statuses: [MissionStatus.Pending], pageSize: missionPageSize, orderBy: 'DesiredStartTime', diff --git a/frontend/src/components/Pages/FrontPage/MissionOverview/OngoingMissionView.tsx b/frontend/src/components/Pages/FrontPage/MissionOverview/OngoingMissionView.tsx index f5695dd6f..b45542097 100644 --- a/frontend/src/components/Pages/FrontPage/MissionOverview/OngoingMissionView.tsx +++ b/frontend/src/components/Pages/FrontPage/MissionOverview/OngoingMissionView.tsx @@ -31,7 +31,7 @@ export function OngoingMissionView({ refreshInterval }: RefreshProps) { const [missions, setMissions] = useState([]) const updateOngoingAndPausedMissions = useCallback(() => { - BackendAPICaller.getMissions({ + BackendAPICaller.getMissionRuns({ statuses: [MissionStatus.Ongoing, MissionStatus.Paused], pageSize: missionPageSize, orderBy: 'StartTime desc', diff --git a/frontend/src/components/Pages/MissionHistoryPage/MissionHistoryView.tsx b/frontend/src/components/Pages/MissionHistoryPage/MissionHistoryView.tsx index 501f0a0bb..b638afe82 100644 --- a/frontend/src/components/Pages/MissionHistoryPage/MissionHistoryView.tsx +++ b/frontend/src/components/Pages/MissionHistoryPage/MissionHistoryView.tsx @@ -47,7 +47,7 @@ export function MissionHistoryView({ refreshInterval }: RefreshProps) { } = useMissionFilterContext() const updateFilteredMissions = useCallback(() => { - BackendAPICaller.getMissions({ + BackendAPICaller.getMissionRuns({ statuses: statuses, nameSearch: missionName, robotNameSearch: robotName, diff --git a/frontend/src/models/MissionDefinitionQueryParameters.ts b/frontend/src/models/MissionDefinitionQueryParameters.ts new file mode 100644 index 000000000..8008442a3 --- /dev/null +++ b/frontend/src/models/MissionDefinitionQueryParameters.ts @@ -0,0 +1,14 @@ +export enum SourceType { + Echo, + Custom, +} + +export interface MissionDefinitionQueryParameters { + nameSearch?: string + robotNameSearch?: string + area?: string + sourceType?: SourceType + pageNumber?: number + pageSize?: number + orderBy?: string +} diff --git a/frontend/src/models/MissionQueryParameters.ts b/frontend/src/models/MissionRunQueryParameters.ts similarity index 88% rename from frontend/src/models/MissionQueryParameters.ts rename to frontend/src/models/MissionRunQueryParameters.ts index e6325e473..c65bc304b 100644 --- a/frontend/src/models/MissionQueryParameters.ts +++ b/frontend/src/models/MissionRunQueryParameters.ts @@ -1,13 +1,14 @@ import { InspectionType } from './Inspection' import { MissionStatus } from './Mission' -export interface MissionQueryParameters { +export interface MissionRunQueryParameters { statuses?: MissionStatus[] robotId?: string nameSearch?: string robotNameSearch?: string tagSearch?: string inspectionTypes?: InspectionType[] + area?: string minStartTime?: number maxStartTime?: number minEndTime?: number From f23452d385ef0010256204d54e44b15654e5be7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20Chirico=20Indreb=C3=B8?= Date: Tue, 20 Jun 2023 14:58:55 +0200 Subject: [PATCH 02/10] Flatten areas nested objects, fix endpoint tests --- .../EventHandlers/TestMissionScheduler.cs | 34 ++++- backend/api.test/Services/MissionService.cs | 30 +++- backend/api/Controllers/AreaController.cs | 21 ++- backend/api/Controllers/MissionController.cs | 3 +- backend/api/Controllers/RobotController.cs | 1 + backend/api/Database/Context/InitDb.cs | 17 ++- backend/api/Database/Models/Area.cs | 12 +- backend/api/Database/Models/Deck.cs | 4 +- backend/api/Database/Models/MissionRun.cs | 2 +- backend/api/Services/AreaService.cs | 136 ++++++++++-------- .../api/Services/MissionDefinitionService.cs | 4 +- backend/api/Services/SourceService.cs | 14 +- 12 files changed, 184 insertions(+), 94 deletions(-) diff --git a/backend/api.test/EventHandlers/TestMissionScheduler.cs b/backend/api.test/EventHandlers/TestMissionScheduler.cs index cadf4a15a..2f5b95812 100644 --- a/backend/api.test/EventHandlers/TestMissionScheduler.cs +++ b/backend/api.test/EventHandlers/TestMissionScheduler.cs @@ -22,6 +22,18 @@ namespace Api.Test.EventHandlers [Collection("Database collection")] public class TestMissionScheduler : IDisposable { + private static readonly Asset testAsset = new() + { + ShortName = "test", + Name = "test test" + }; + private static readonly Installation testInstallation = new() + { + ShortName = "test", + Name = "test test", + Asset = testAsset + }; + private static MissionRun ScheduledMission => new() { @@ -29,13 +41,33 @@ public class TestMissionScheduler : IDisposable MissionId = Guid.NewGuid().ToString(), Status = MissionStatus.Pending, DesiredStartTime = DateTimeOffset.Now, + Area = new Area + { + Deck = new Deck + { + Installation = testInstallation, + Asset = testAsset, + Name = "testDeck" + }, + Asset = testAsset, + Installation = testInstallation, + Name = "testArea", + MapMetadata = new MapMetadata() + { + MapName = "TestMap", + Boundary = new(), + TransformationMatrices = new() + }, + DefaultLocalizationPose = new Pose(), + SafePositions = new List() + }, MapMetadata = new MapMetadata() { MapName = "TestMap", Boundary = new(), TransformationMatrices = new() }, - Area = new Area() + AssetCode = "testAsset" }; private readonly MissionScheduler _scheduledMissionEventHandler; diff --git a/backend/api.test/Services/MissionService.cs b/backend/api.test/Services/MissionService.cs index 1872dec6e..78016ddf8 100644 --- a/backend/api.test/Services/MissionService.cs +++ b/backend/api.test/Services/MissionService.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Api.Controllers.Models; @@ -46,13 +47,40 @@ public async Task Create() .ReadAll(new MissionRunQueryStringParameters()) .Result.Count; + var testAsset = new Asset + { + ShortName = "test", + Name = "test test" + }; + var testInstallation = new Installation + { + ShortName = "test", + Name = "test test", + Asset = testAsset + }; + MissionRun mission = new() { Name = "testMission", Robot = robot, MapMetadata = new MapMetadata() { MapName = "testMap" }, - Area = new Area(), + Area = new Area + { + Deck = new Deck + { + Installation = testInstallation, + Asset = testAsset, + Name = "testDeck" + }, + Asset = testAsset, + Installation = testInstallation, + Name = "testArea", + MapMetadata = new MapMetadata() { MapName = "testMap" }, + DefaultLocalizationPose = new Pose(), + SafePositions = new List() + }, + AssetCode = "testAsset", DesiredStartTime = DateTime.Now }; diff --git a/backend/api/Controllers/AreaController.cs b/backend/api/Controllers/AreaController.cs index 9db513838..e0bd63951 100644 --- a/backend/api/Controllers/AreaController.cs +++ b/backend/api/Controllers/AreaController.cs @@ -68,10 +68,19 @@ public async Task>> GetAreas() [ProducesResponseType(StatusCodes.Status500InternalServerError)] public async Task> GetAreaById([FromRoute] string id) { - var area = await _areaService.ReadById(id); - if (area == null) - return NotFound($"Could not find area with id {id}"); - return Ok(area); + try + { + var area = await _areaService.ReadById(id); + if (area == null) + return NotFound($"Could not find area with id {id}"); + return Ok(area); + } + catch (Exception e) + { + _logger.LogError(e, "Error during GET of areas from database"); + throw; + } + } /// @@ -81,7 +90,7 @@ public async Task> GetAreaById([FromRoute] string id) /// This query adds a new area to the database /// [HttpPost] - [Authorize(Roles = Role.User)] + [Authorize(Roles = Role.Admin)] [ProducesResponseType(typeof(Area), StatusCodes.Status201Created)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] @@ -95,7 +104,7 @@ public async Task> Create([FromBody] CreateAreaQuery area) var existingArea = await _areaService.ReadByAssetAndName(area.AssetCode, area.AreaName); if (existingArea != null) { - _logger.LogInformation("An ara for given name and asset already exists"); + _logger.LogInformation("An area for given name and asset already exists"); return BadRequest($"Area already exists"); } diff --git a/backend/api/Controllers/MissionController.cs b/backend/api/Controllers/MissionController.cs index 9c40a8767..7c88f6456 100644 --- a/backend/api/Controllers/MissionController.cs +++ b/backend/api/Controllers/MissionController.cs @@ -460,8 +460,7 @@ [FromBody] CustomMissionQuery customMissionQuery if (area == null) return NotFound($"Could not find area with name {customMissionQuery.AreaName} in asset {customMissionQuery.AssetCode}"); - // TODO: upload file to blobstorage and then pass the URL to MissionDefinition Source - var customMissionId = Guid.NewGuid().ToString(); + string customMissionId = Guid.NewGuid().ToString(); var sourceURL = await _sourceService.UploadSource(customMissionId, missionTasks); var customMissionDefinition = new MissionDefinition diff --git a/backend/api/Controllers/RobotController.cs b/backend/api/Controllers/RobotController.cs index 3103649d8..33f8a8c64 100644 --- a/backend/api/Controllers/RobotController.cs +++ b/backend/api/Controllers/RobotController.cs @@ -810,6 +810,7 @@ [FromRoute] string areaName { Name = "Drive to Safe Position", Robot = robot, + AssetCode = asset, Area = area, Status = MissionStatus.Pending, DesiredStartTime = DateTimeOffset.UtcNow, diff --git a/backend/api/Database/Context/InitDb.cs b/backend/api/Database/Context/InitDb.cs index f8954d41d..5b6d048f7 100644 --- a/backend/api/Database/Context/InitDb.cs +++ b/backend/api/Database/Context/InitDb.cs @@ -109,6 +109,7 @@ private static List GetDecks() { Id = Guid.NewGuid().ToString(), Installation = installations[0], + Asset = installations[0].Asset, Name = "TestDeck" }; @@ -121,8 +122,10 @@ private static List GetAreas() { Id = Guid.NewGuid().ToString(), Deck = decks[0], + Installation = decks[0].Installation, + Asset = decks[0].Installation.Asset, Name = "AP320", - Map = new MapMetadata(), + MapMetadata = new MapMetadata(), DefaultLocalizationPose = new Pose {}, SafePositions = new List() }; @@ -131,8 +134,10 @@ private static List GetAreas() { Id = Guid.NewGuid().ToString(), Deck = decks[0], + Installation = decks[0].Installation, + Asset = decks[0].Installation.Asset, Name = "AP330", - Map = new MapMetadata(), + MapMetadata = new MapMetadata(), DefaultLocalizationPose = new Pose {}, SafePositions = new List() }; @@ -141,8 +146,10 @@ private static List GetAreas() { Id = "TestId", Deck = decks[0], + Installation = decks[0].Installation, + Asset = decks[0].Installation.Asset, Name = "testArea", - Map = new MapMetadata(), + MapMetadata = new MapMetadata(), DefaultLocalizationPose = new Pose { }, SafePositions = new List() }; @@ -179,7 +186,7 @@ private static List GetMissionDefinitions() Area = areas[0], Source = sources[0], Comment = "Interesting comment", - InspectionFrequency = TimeSpan.Parse("14:00:0:0"), + InspectionFrequency = new DateTime().AddDays(12) - new DateTime(), LastRun = null }; @@ -190,7 +197,7 @@ private static List GetMissionDefinitions() AssetCode = areas[1].Deck.Installation.Asset.ShortName, Area = areas[1], Source = sources[1], - InspectionFrequency = TimeSpan.Parse("7:00:0:0"), + InspectionFrequency = new DateTime().AddDays(7) - new DateTime(), LastRun = null }; diff --git a/backend/api/Database/Models/Area.cs b/backend/api/Database/Models/Area.cs index 863a031c5..13eeb00c6 100644 --- a/backend/api/Database/Models/Area.cs +++ b/backend/api/Database/Models/Area.cs @@ -11,15 +11,19 @@ public class Area public string Id { get; set; } [Required] - [MaxLength(200)] - public virtual Deck Deck { get; set; } + public Deck Deck { get; set; } + + [Required] + public Installation Installation { get; set; } + + [Required] + public Asset Asset { get; set; } [Required] - [MaxLength(200)] public string Name { get; set; } [Required] - public MapMetadata Map { get; set; } + public MapMetadata MapMetadata { get; set; } [Required] public Pose DefaultLocalizationPose { get; set; } diff --git a/backend/api/Database/Models/Deck.cs b/backend/api/Database/Models/Deck.cs index 1e2372d4c..eb05d5d85 100644 --- a/backend/api/Database/Models/Deck.cs +++ b/backend/api/Database/Models/Deck.cs @@ -11,9 +11,11 @@ public class Deck public string Id { get; set; } [Required] - [MaxLength(200)] public Installation Installation { get; set; } + [Required] + public Asset Asset { get; set; } + [Required] [MaxLength(200)] public string Name { get; set; } diff --git a/backend/api/Database/Models/MissionRun.cs b/backend/api/Database/Models/MissionRun.cs index 079805751..c02ef058b 100644 --- a/backend/api/Database/Models/MissionRun.cs +++ b/backend/api/Database/Models/MissionRun.cs @@ -12,7 +12,7 @@ public class MissionRun public string Id { get; set; } [MaxLength(200)] - public string MissionId { get; set; } + public string? MissionId { get; set; } [MaxLength(200)] public string? IsarMissionId { get; set; } diff --git a/backend/api/Services/AreaService.cs b/backend/api/Services/AreaService.cs index f62992d10..61a0d09b7 100644 --- a/backend/api/Services/AreaService.cs +++ b/backend/api/Services/AreaService.cs @@ -13,13 +13,13 @@ public interface IAreaService public abstract Task> ReadByAsset(string asset); - public abstract Task ReadByAssetAndName(string asset, string name); + public abstract Task ReadByAssetAndName(string assetName, string name); public abstract Task Create(CreateAreaQuery newArea); public abstract Task Create(CreateAreaQuery newArea, List safePositions); - public abstract Task Update(Area Area); + public abstract Task Update(Area area); public abstract Task AddSafePosition(string asset, string name, SafePosition safePosition); @@ -60,59 +60,71 @@ private IQueryable GetAreas() public async Task ReadById(string id) { return await GetAreas() - .FirstOrDefaultAsync(Area => Area.Id.Equals(id)); + .FirstOrDefaultAsync(a => a.Id.Equals(id)); } - public async Task ReadByAssetAndName(string name) + public async Task ReadByAssetAndName(Asset? asset, string name) { + if (asset == null) + return null; return await _context.Areas.Where(a => - a.Name.ToLower().Equals(name.ToLower()) - ).Include(a => a.SafePositions) - .Include(a => a.Deck).ThenInclude(d => d.Installation).ThenInclude(i => i.Asset).FirstOrDefaultAsync(); + a.Name.ToLower().Equals(name.ToLower()) && + a.Asset.Equals(asset) + ).Include(a => a.SafePositions).FirstOrDefaultAsync(); } - public async Task> ReadByAsset(string asset) + public async Task> ReadByAsset(string assetName) { + var asset = await ReadAssetByName(assetName); + if (asset == null) + return new List(); return await _context.Areas.Where(a => - a.Deck.Installation.Asset.ShortName.Equals(asset.ToLower())).Include(a => a.SafePositions) - .Include(a => a.Deck).ThenInclude(d => d.Installation).ThenInclude(i => i.Asset).ToListAsync(); + a.Asset.Equals(asset)).Include(a => a.SafePositions).ToListAsync(); } - public async Task ReadByAssetAndName(string asset, string name) + public async Task ReadByAssetAndName(string assetName, string name) { // TODO: can we assume that this combination will be unique? Are area names specific enough? + var asset = await ReadAssetByName(assetName); + if (asset == null) + return null; return await _context.Areas.Where(a => - a.Deck.Installation.Asset.ShortName.ToLower().Equals(asset.ToLower()) && + a.Asset.Equals(asset) && a.Name.ToLower().Equals(name.ToLower()) - ).Include(a => a.Deck).ThenInclude(d => d.Installation).ThenInclude(i => i.Asset) - .Include(a => a.SafePositions).FirstOrDefaultAsync(); + ).Include(a => a.SafePositions).FirstOrDefaultAsync(); } - public async Task ReadAreaByAssetAndInstallationAndDeckAndName(string asset, string installation, string deck, string name) + public async Task ReadAreaByAssetAndInstallationAndDeckAndName(Asset? asset, Installation? installation, Deck? deck, string name) { + if (asset == null || installation == null || deck == null) + return null; return await _context.Areas.Where(a => - a.Deck.Installation.Asset.ShortName.ToLower().Equals(asset.ToLower()) && - a.Deck.Installation.ShortName.ToLower().Equals(installation.ToLower()) && - a.Deck.Name.ToLower().Equals(deck.ToLower()) && + a.Deck.Equals(deck) && + a.Installation.Equals(installation) && + a.Asset.Equals(asset) && a.Name.ToLower().Equals(name.ToLower()) ).Include(a => a.Deck).ThenInclude(d => d.Installation).ThenInclude(i => i.Asset) .Include(a => a.SafePositions).FirstOrDefaultAsync(); } - public async Task ReadDeckByAssetAndInstallationAndName(string asset, string installation, string name) + public async Task ReadDeckByAssetAndInstallationAndName(Asset? asset, Installation? installation, string name) { - return await _context.Decks.Where(d => - d.Installation.Asset.ShortName.ToLower().Equals(asset.ToLower()) && - d.Installation.ShortName.ToLower().Equals(installation.ToLower()) && - d.Name.ToLower().Equals(name.ToLower()) - ).Include(a => a.Installation).ThenInclude(i => i.Asset).FirstOrDefaultAsync(); + if (asset == null || installation == null) + return null; + return await _context.Decks.Where(a => + a.Installation.Equals(installation) && + a.Asset.Equals(asset) && + a.Name.ToLower().Equals(name.ToLower()) + ).Include(d => d.Installation).Include(i => i.Asset).FirstOrDefaultAsync(); } - public async Task ReadInstallationByAssetAndName(string asset, string name) + public async Task ReadInstallationByAssetAndName(Asset? asset, string name) { - return await _context.Installations.Where(i => - i.Asset.ShortName.ToLower().Equals(asset.ToLower()) && - i.Name.ToLower().Equals(name.ToLower()) + if (asset == null) + return null; + return await _context.Installations.Where(a => + a.Asset.Equals(asset) && + a.Name.ToLower().Equals(name.ToLower()) ).Include(i => i.Asset).FirstOrDefaultAsync(); } @@ -131,58 +143,60 @@ public async Task Create(CreateAreaQuery newAreaQuery, List safePosi sp.Add(new SafePosition(p)); } - var existingArea = ReadAreaByAssetAndInstallationAndDeckAndName( - newAreaQuery.AssetCode, - newAreaQuery.InstallationName, - newAreaQuery.DeckName, - newAreaQuery.AreaName); - if (existingArea != null) + var asset = await ReadAssetByName(newAreaQuery.AssetCode); + if (asset == null) { - // TODO: maybe just append safe positions, or return an error + asset = new Asset + { + Name = "", // TODO: + ShortName = newAreaQuery.AssetCode + }; + await _context.Assets.AddAsync(asset); + await _context.SaveChangesAsync(); } - var deck = await ReadDeckByAssetAndInstallationAndName(newAreaQuery.AssetCode, newAreaQuery.InstallationName, newAreaQuery.DeckName); - if (deck == null) + var installation = await ReadInstallationByAssetAndName(asset, newAreaQuery.InstallationName); + if (installation == null) { - var installation = await ReadInstallationByAssetAndName(newAreaQuery.AssetCode, newAreaQuery.InstallationName); - if (installation == null) + installation = new Installation { - var asset = await ReadAssetByName(newAreaQuery.AssetCode); - if (asset == null) - { - asset = new Asset - { - Name = "", // TODO: - ShortName = newAreaQuery.AssetCode - }; - await _context.Assets.AddAsync(asset); - await _context.SaveChangesAsync(); - } - installation = new Installation - { - Asset = asset, - Name = "", // TODO: - ShortName = newAreaQuery.InstallationName - }; - await _context.Installations.AddAsync(installation); - await _context.SaveChangesAsync(); - } + Asset = asset, + Name = "", // TODO: + ShortName = newAreaQuery.InstallationName + }; + await _context.Installations.AddAsync(installation); + await _context.SaveChangesAsync(); + } + + var deck = await ReadDeckByAssetAndInstallationAndName(asset, installation, newAreaQuery.DeckName); + if (deck == null) + { deck = new Deck { Installation = installation, + Asset = asset, Name = newAreaQuery.DeckName }; await _context.Decks.AddAsync(deck); await _context.SaveChangesAsync(); } + var existingArea = await ReadAreaByAssetAndInstallationAndDeckAndName( + asset, installation, deck, newAreaQuery.AreaName); + if (existingArea != null) + { + // TODO: maybe just append safe positions, or return an error + } + var newArea = new Area { Name = newAreaQuery.AreaName, DefaultLocalizationPose = newAreaQuery.DefaultLocalizationPose, SafePositions = sp, - Map = new MapMetadata(), - Deck = deck + MapMetadata = new MapMetadata(), + Deck = deck, + Installation = installation, + Asset = asset }; await _context.Areas.AddAsync(newArea); await _context.SaveChangesAsync(); diff --git a/backend/api/Services/MissionDefinitionService.cs b/backend/api/Services/MissionDefinitionService.cs index 0bb75554b..b05804c0e 100644 --- a/backend/api/Services/MissionDefinitionService.cs +++ b/backend/api/Services/MissionDefinitionService.cs @@ -41,12 +41,10 @@ public interface IMissionDefinitionService public class MissionDefinitionService : IMissionDefinitionService { private readonly FlotillaDbContext _context; - private readonly ILogger _logger; - public MissionDefinitionService(FlotillaDbContext context, ILogger logger) + public MissionDefinitionService(FlotillaDbContext context) { _context = context; - _logger = logger; } public async Task Create(MissionDefinition mission) diff --git a/backend/api/Services/SourceService.cs b/backend/api/Services/SourceService.cs index a98059971..ef158c85b 100644 --- a/backend/api/Services/SourceService.cs +++ b/backend/api/Services/SourceService.cs @@ -1,10 +1,8 @@  using System.Text.Json; -using System.Text.Json.Serialization; using Api.Database.Models; using Api.Options; -using Azure.Storage; using Azure.Storage.Blobs; using Microsoft.Extensions.Options; @@ -21,12 +19,10 @@ public interface ISourceService public class SourceService : ISourceService { private readonly IOptions _storageOptions; - private readonly ILogger _logger; - public SourceService(IOptions storageOptions, ILogger logger) + public SourceService(IOptions storageOptions) { _storageOptions = storageOptions; - _logger = logger; } public async Task CreateContainer(string containerName) @@ -67,12 +63,12 @@ public Task UploadSource(string id, List tasks) var blobClient = containerClient.GetBlobClient(id); - List? content = null; + List? content; try { content = blobClient.DownloadContent().Value.Content.ToObjectFromJson>(); } - catch (System.Exception) + catch (Exception) { return null; } @@ -84,12 +80,12 @@ public Task UploadSource(string id, List tasks) { var blobClient = new BlobClient(new Uri(url)); - List? content = null; + List? content; try { content = blobClient.DownloadContent().Value.Content.ToObjectFromJson>(); } - catch (System.Exception) + catch (Exception) { return null; } From 7dba54e038ce7cae054ccfc5d463aeb1ec5314e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20Chirico=20Indreb=C3=B8?= Date: Wed, 5 Jul 2023 11:47:04 +0200 Subject: [PATCH 03/10] Implement requests, clarify mission names --- .gitignore | 1 + backend/README.md | 5 +- backend/api.test/EndpointTest.cs | 182 +++++++++++-- .../EventHandlers/TestMissionScheduler.cs | 48 ++-- backend/api.test/Services/MissionService.cs | 21 +- backend/api/Controllers/AreaController.cs | 254 +++++++++++------- backend/api/Controllers/AssetController.cs | 147 ++++++++++ backend/api/Controllers/DeckController.cs | 163 +++++++++++ .../api/Controllers/InstallationController.cs | 155 +++++++++++ backend/api/Controllers/MissionController.cs | 186 ++++++------- .../api/Controllers/Models/AreaResponse.cs | 22 ++ .../api/Controllers/Models/CreateAreaQuery.cs | 2 +- .../Models/CreateAssetDeckQuery.cs | 13 - .../Controllers/Models/CreateAssetQuery.cs | 8 + .../api/Controllers/Models/CreateDeckQuery.cs | 9 + .../Models/CreateInstallationQuery.cs | 9 + .../Controllers/Models/CreateRobotQuery.cs | 2 +- .../Models/RescheduleMissionQuery.cs | 12 - .../Models/ScheduleMissionQuery.cs | 9 + backend/api/Controllers/RobotController.cs | 83 +++--- .../api/Database/Context/FlotillaDbContext.cs | 14 +- backend/api/Database/Context/InitDb.cs | 55 ++-- backend/api/Database/Models/Area.cs | 1 + backend/api/Database/Models/Asset.cs | 2 +- backend/api/Database/Models/AssetDeck.cs | 26 -- backend/api/Database/Models/Installation.cs | 8 +- .../api/Database/Models/MissionDefinition.cs | 14 +- backend/api/Database/Models/MissionRun.cs | 69 ++--- backend/api/Database/Models/Robot.cs | 2 +- backend/api/Database/Models/RobotModel.cs | 18 +- backend/api/Database/Models/SortableRecord.cs | 8 + backend/api/Database/Models/Source.cs | 6 +- .../IsarConnectionEventHandler.cs | 14 +- backend/api/EventHandlers/MissionScheduler.cs | 38 +-- backend/api/EventHandlers/MqttEventHandler.cs | 26 +- backend/api/Options/StorageOptions.cs | 2 +- backend/api/Program.cs | 5 +- backend/api/Services/AreaService.cs | 146 ++++------ backend/api/Services/AssetService.cs | 107 ++++++++ backend/api/Services/CustomMissionService.cs | 59 ++++ backend/api/Services/DeckService.cs | 144 ++++++++++ backend/api/Services/EchoService.cs | 38 +-- backend/api/Services/InstallationService.cs | 137 ++++++++++ backend/api/Services/IsarService.cs | 6 +- backend/api/Services/MapService.cs | 13 +- .../api/Services/MissionDefinitionService.cs | 93 ++++--- backend/api/Services/MissionRunService.cs | 186 ++++++------- .../Services/Models/IsarMissionDefinition.cs | 26 +- backend/api/Services/SortingService.cs | 65 +++++ backend/api/Services/SourceService.cs | 96 ------- backend/api/Utilities/Exceptions.cs | 25 ++ backend/api/appsettings.Development.json | 2 +- backend/api/appsettings.Production.json | 2 +- backend/api/appsettings.Staging.json | 2 +- backend/api/appsettings.Test.json | 2 +- frontend/src/api/ApiCaller.tsx | 20 +- .../MissionOverview/MissionQueueView.tsx | 1 + .../Pages/MissionPage/MissionPage.tsx | 4 +- .../{AssetDeckMapView.tsx => AreaMapView.tsx} | 18 +- .../Pages/RobotPage/LocalizationDialog.tsx | 47 ++-- frontend/src/models/{AssetDeck.ts => Area.ts} | 4 +- frontend/src/models/Robot.ts | 4 +- 62 files changed, 1968 insertions(+), 918 deletions(-) create mode 100644 backend/api/Controllers/AssetController.cs create mode 100644 backend/api/Controllers/DeckController.cs create mode 100644 backend/api/Controllers/InstallationController.cs create mode 100644 backend/api/Controllers/Models/AreaResponse.cs delete mode 100644 backend/api/Controllers/Models/CreateAssetDeckQuery.cs create mode 100644 backend/api/Controllers/Models/CreateAssetQuery.cs create mode 100644 backend/api/Controllers/Models/CreateDeckQuery.cs create mode 100644 backend/api/Controllers/Models/CreateInstallationQuery.cs delete mode 100644 backend/api/Controllers/Models/RescheduleMissionQuery.cs create mode 100644 backend/api/Controllers/Models/ScheduleMissionQuery.cs delete mode 100644 backend/api/Database/Models/AssetDeck.cs create mode 100644 backend/api/Database/Models/SortableRecord.cs create mode 100644 backend/api/Services/AssetService.cs create mode 100644 backend/api/Services/CustomMissionService.cs create mode 100644 backend/api/Services/DeckService.cs create mode 100644 backend/api/Services/InstallationService.cs create mode 100644 backend/api/Services/SortingService.cs delete mode 100644 backend/api/Services/SourceService.cs rename frontend/src/components/Pages/RobotPage/{AssetDeckMapView.tsx => AreaMapView.tsx} (92%) rename frontend/src/models/{AssetDeck.ts => Area.ts} (62%) diff --git a/.gitignore b/.gitignore index 2a9fa9b22..362b36ce5 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ frontend/build/ .idea *.log *.vs +*.code-workspace # Environment variables *.env diff --git a/backend/README.md b/backend/README.md index 6e6390dfe..1543a36c4 100644 --- a/backend/README.md +++ b/backend/README.md @@ -246,7 +246,10 @@ The access matrix looks like this: | | **Read Only** | **User** | **Admin** | | -------------------------- | ------------- | -------- | --------- | -| Asset Decks | Read | Read | CRUD | +| Area | Read | Read | CRUD | +| Deck | Read | Read | CRUD | +| Installation | Read | Read | CRUD | +| Asset | Read | Read | CRUD | | Echo | Read | Read | CRUD | | Missions | Read | Read | CRUD | | Robots | Read | Read | CRUD | diff --git a/backend/api.test/EndpointTest.cs b/backend/api.test/EndpointTest.cs index 253b92d6f..19ecaf521 100644 --- a/backend/api.test/EndpointTest.cs +++ b/backend/api.test/EndpointTest.cs @@ -102,17 +102,104 @@ public void Dispose() GC.SuppressFinalize(this); } + private async Task PopulateAreaDb(string assetName, string installationName, string deckName, string areaName) + { + string assetUrl = $"/assets"; + string installationUrl = $"/installations"; + string deckUrl = $"/decks"; + string areaUrl = $"/areas"; + var testPose = new Pose + { + Position = new Position + { + X = 1, + Y = 2, + Z = 2 + }, + Orientation = new Orientation + { + X = 0, + Y = 0, + Z = 0, + W = 1 + } + }; + + var assetQuery = new CreateAssetQuery + { + AssetCode = assetName, + Name = assetName + }; + + var installationQuery = new CreateInstallationQuery + { + AssetCode = assetName, + InstallationCode = installationName, + Name = installationName + }; + + var deckQuery = new CreateDeckQuery + { + AssetCode = assetName, + InstallationCode = installationName, + Name = deckName + }; + + var areaQuery = new CreateAreaQuery + { + AssetCode = assetName, + InstallationCode = installationName, + DeckName = deckName, + AreaName = areaName, + DefaultLocalizationPose = testPose + }; + + var assetContent = new StringContent( + JsonSerializer.Serialize(assetQuery), + null, + "application/json" + ); + + var installationContent = new StringContent( + JsonSerializer.Serialize(installationQuery), + null, + "application/json" + ); + + var deckContent = new StringContent( + JsonSerializer.Serialize(deckQuery), + null, + "application/json" + ); + + var areaContent = new StringContent( + JsonSerializer.Serialize(areaQuery), + null, + "application/json" + ); + + // Act + var assetResponse = await _client.PostAsync(assetUrl, assetContent); + Assert.NotNull(assetResponse); + var installationResponse = await _client.PostAsync(installationUrl, installationContent); + Assert.NotNull(installationResponse); + var deckResponse = await _client.PostAsync(deckUrl, deckContent); + Assert.NotNull(deckResponse); + var areaResponse = await _client.PostAsync(areaUrl, areaContent); + Assert.NotNull(areaResponse); + } + #region MissionsController [Fact] public async Task MissionsTest() { string url = "/missions/runs"; var response = await _client.GetAsync(url); - var missions = await response.Content.ReadFromJsonAsync>( + var missionRuns = await response.Content.ReadFromJsonAsync>( _serializerOptions ); Assert.True(response.IsSuccessStatusCode); - Assert.True(missions != null && missions.Count == 3); + Assert.True(missionRuns != null && missionRuns.Count == 3); } [Fact] @@ -177,22 +264,25 @@ public async Task GetRobotById_ShouldReturnRobot() public async Task StartMissionTest() { // Arrange - string url = "/robots"; - var response = await _client.GetAsync(url); + string robotUrl = "/robots"; + string missionsUrl = "/missions"; + var response = await _client.GetAsync(robotUrl); Assert.True(response.IsSuccessStatusCode); var robots = await response.Content.ReadFromJsonAsync>(_serializerOptions); Assert.True(robots != null); var robot = robots[0]; string robotId = robot.Id; + string testAsset = "TestAsset"; + string testArea = "testArea"; + int echoMissionId = 95; // Act - url = "/missions"; var query = new ScheduledMissionQuery { RobotId = robotId, - AssetCode = "JSV", - AreaName = "testArea", - EchoMissionId = 95, + AssetCode = testAsset, + AreaName = testArea, + EchoMissionId = echoMissionId, DesiredStartTime = DateTimeOffset.UtcNow }; var content = new StringContent( @@ -200,14 +290,14 @@ public async Task StartMissionTest() null, "application/json" ); - response = await _client.PostAsync(url, content); + response = await _client.PostAsync(missionsUrl, content); // Assert Assert.True(response.IsSuccessStatusCode); - var mission = await response.Content.ReadFromJsonAsync(_serializerOptions); - Assert.True(mission != null); - Assert.True(mission.Id != null); - Assert.True(mission.Status == MissionStatus.Pending); + var missionRun = await response.Content.ReadFromJsonAsync(_serializerOptions); + Assert.True(missionRun != null); + Assert.True(missionRun.Id != null); + Assert.True(missionRun.Status == MissionStatus.Pending); } [Fact] @@ -218,6 +308,9 @@ public async Task AreaTest() string testInstallation = "TestInstallation"; string testDeck = "testDeck2"; string testArea = "testArea"; + string assetUrl = $"/assets"; + string installationUrl = $"/installations"; + string deckUrl = $"/decks"; string areaUrl = $"/areas"; var testPose = new Pose { @@ -236,25 +329,69 @@ public async Task AreaTest() } }; - var query = new CreateAreaQuery + var assetQuery = new CreateAssetQuery + { + AssetCode = testAsset, + Name = testAsset + }; + + var installationQuery = new CreateInstallationQuery + { + AssetCode = testAsset, + InstallationCode = testInstallation, + Name = testInstallation + }; + + var deckQuery = new CreateDeckQuery + { + AssetCode = testAsset, + InstallationCode = testInstallation, + Name = testDeck + }; + + var areaQuery = new CreateAreaQuery { AssetCode = testAsset, - InstallationName = testInstallation, + InstallationCode = testInstallation, DeckName = testDeck, AreaName = testArea, DefaultLocalizationPose = testPose }; - var content = new StringContent( - JsonSerializer.Serialize(query), + var assetContent = new StringContent( + JsonSerializer.Serialize(assetQuery), + null, + "application/json" + ); + + var installationContent = new StringContent( + JsonSerializer.Serialize(installationQuery), + null, + "application/json" + ); + + var deckContent = new StringContent( + JsonSerializer.Serialize(deckQuery), + null, + "application/json" + ); + + var areaContent = new StringContent( + JsonSerializer.Serialize(areaQuery), null, "application/json" ); // Act - var areaResponse = await _client.PostAsync(areaUrl, content); + var assetResponse = await _client.PostAsync(assetUrl, assetContent); + var installationResponse = await _client.PostAsync(installationUrl, installationContent); + var deckResponse = await _client.PostAsync(deckUrl, deckContent); + var areaResponse = await _client.PostAsync(areaUrl, areaContent); // Assert + Assert.True(assetResponse.IsSuccessStatusCode); + Assert.True(installationResponse.IsSuccessStatusCode); + Assert.True(deckResponse.IsSuccessStatusCode); Assert.True(areaResponse.IsSuccessStatusCode); var area = await areaResponse.Content.ReadFromJsonAsync(_serializerOptions); Assert.True(area != null); @@ -291,6 +428,9 @@ public async Task SafePositionTest() null, "application/json" ); + + await PopulateAreaDb("testAsset", "testInstallation", "testDeck", "testArea"); + var areaResponse = await _client.PostAsync(addSafePositionUrl, content); Assert.True(areaResponse.IsSuccessStatusCode); var area = await areaResponse.Content.ReadFromJsonAsync(_serializerOptions); @@ -311,10 +451,10 @@ public async Task SafePositionTest() // Assert Assert.True(missionResponse.IsSuccessStatusCode); - var mission = await missionResponse.Content.ReadFromJsonAsync(_serializerOptions); - Assert.True(mission != null); + var missionRun = await missionResponse.Content.ReadFromJsonAsync(_serializerOptions); + Assert.True(missionRun != null); Assert.True( - JsonSerializer.Serialize(mission.Tasks[0].RobotPose.Position) == + JsonSerializer.Serialize(missionRun.Tasks[0].RobotPose.Position) == JsonSerializer.Serialize(testPosition) ); } diff --git a/backend/api.test/EventHandlers/TestMissionScheduler.cs b/backend/api.test/EventHandlers/TestMissionScheduler.cs index 2f5b95812..c0290b132 100644 --- a/backend/api.test/EventHandlers/TestMissionScheduler.cs +++ b/backend/api.test/EventHandlers/TestMissionScheduler.cs @@ -24,12 +24,12 @@ public class TestMissionScheduler : IDisposable { private static readonly Asset testAsset = new() { - ShortName = "test", + AssetCode = "test", Name = "test test" }; private static readonly Installation testInstallation = new() { - ShortName = "test", + InstallationCode = "test", Name = "test test", Asset = testAsset }; @@ -71,7 +71,7 @@ public class TestMissionScheduler : IDisposable }; private readonly MissionScheduler _scheduledMissionEventHandler; - private readonly IMissionRunService _missionService; + private readonly IMissionRunService _missionRunService; private readonly IRobotService _robotService; private readonly RobotControllerMock _robotControllerMock; private readonly FlotillaDbContext _context; @@ -85,7 +85,7 @@ public TestMissionScheduler(DatabaseFixture fixture) // Mock ScheduledMissionService: _context = fixture.NewContext; - _missionService = new MissionRunService(_context, missionLogger); + _missionRunService = new MissionRunService(_context, missionLogger); _robotService = new RobotService(_context); _robotControllerMock = new RobotControllerMock(); @@ -94,7 +94,7 @@ public TestMissionScheduler(DatabaseFixture fixture) // Mock injection of MissionService: mockServiceProvider .Setup(p => p.GetService(typeof(IMissionRunService))) - .Returns(_missionService); + .Returns(_missionRunService); // Mock injection of RobotService: mockServiceProvider .Setup(p => p.GetService(typeof(IRobotService))) @@ -137,7 +137,7 @@ public void Dispose() private async void AssertExpectedStatusChange( MissionStatus preStatus, MissionStatus postStatus, - MissionRun mission + MissionRun missionRun ) { // ARRANGE @@ -145,18 +145,18 @@ MissionRun mission var cts = new CancellationTokenSource(); // Add Scheduled mission - await _missionService.Create(mission); + await _missionRunService.Create(missionRun); _robotControllerMock.RobotServiceMock - .Setup(service => service.ReadById(mission.Robot.Id)) - .Returns(async () => mission.Robot); + .Setup(service => service.ReadById(missionRun.Robot.Id)) + .Returns(async () => missionRun.Robot); _robotControllerMock.MissionServiceMock - .Setup(service => service.ReadById(mission.Id)) - .Returns(async () => mission); + .Setup(service => service.ReadById(missionRun.Id)) + .Returns(async () => missionRun); // Assert start conditions - var preMission = await _missionService.ReadById(mission.Id); + var preMission = await _missionRunService.ReadById(missionRun.Id); Assert.NotNull(preMission); Assert.Equal(preStatus, preMission!.Status); @@ -170,7 +170,7 @@ MissionRun mission // ASSERT // Verify status change - var postMission = await _missionService.ReadById(mission.Id); + var postMission = await _missionRunService.ReadById(missionRun.Id); Assert.NotNull(postMission); Assert.Equal(postStatus, postMission!.Status); } @@ -179,33 +179,33 @@ MissionRun mission // Test that if robot is busy, mission awaits available robot public async void ScheduledMissionPendingIfRobotBusy() { - var mission = ScheduledMission; + var missionRun = ScheduledMission; // Get real robot to avoid error on robot model var robot = (await _robotService.ReadAll()).First( r => r is { Status: RobotStatus.Busy, Enabled: true } ); - mission.Robot = robot; + missionRun.Robot = robot; // Expect failed because robot does not exist - AssertExpectedStatusChange(MissionStatus.Pending, MissionStatus.Pending, mission); + AssertExpectedStatusChange(MissionStatus.Pending, MissionStatus.Pending, missionRun); } [Fact] // Test that if robot is available, mission is started public async void ScheduledMissionStartedIfRobotAvailable() { - var mission = ScheduledMission; + var missionRun = ScheduledMission; // Get real robot to avoid error on robot model var robot = (await _robotService.ReadAll()).First( r => r is { Status: RobotStatus.Available, Enabled: true } ); - mission.Robot = robot; + missionRun.Robot = robot; // Mock successful Start Mission: _robotControllerMock.IsarServiceMock - .Setup(isar => isar.StartMission(robot, mission)) + .Setup(isar => isar.StartMission(robot, missionRun)) .Returns( async () => new IsarMission( @@ -218,29 +218,29 @@ public async void ScheduledMissionStartedIfRobotAvailable() ); // Expect failed because robot does not exist - AssertExpectedStatusChange(MissionStatus.Pending, MissionStatus.Ongoing, mission); + AssertExpectedStatusChange(MissionStatus.Pending, MissionStatus.Ongoing, missionRun); } [Fact] // Test that if ISAR fails, mission is set to failed public async void ScheduledMissionFailedIfIsarUnavailable() { - var mission = ScheduledMission; + var missionRun = ScheduledMission; // Get real robot to avoid error on robot model var robot = (await _robotService.ReadAll()).First(); robot.Enabled = true; robot.Status = RobotStatus.Available; await _robotService.Update(robot); - mission.Robot = robot; + missionRun.Robot = robot; // Mock failing ISAR: _robotControllerMock.IsarServiceMock - .Setup(isar => isar.StartMission(robot, mission)) + .Setup(isar => isar.StartMission(robot, missionRun)) .Throws(new MissionException("ISAR Failed test message")); // Expect failed because robot does not exist - AssertExpectedStatusChange(MissionStatus.Pending, MissionStatus.Failed, mission); + AssertExpectedStatusChange(MissionStatus.Pending, MissionStatus.Failed, missionRun); } } } diff --git a/backend/api.test/Services/MissionService.cs b/backend/api.test/Services/MissionService.cs index 78016ddf8..010887420 100644 --- a/backend/api.test/Services/MissionService.cs +++ b/backend/api.test/Services/MissionService.cs @@ -17,13 +17,13 @@ public class MissionServiceTest : IDisposable { private readonly FlotillaDbContext _context; private readonly ILogger _logger; - private readonly MissionRunService _missionService; + private readonly MissionRunService _missionRunService; public MissionServiceTest(DatabaseFixture fixture) { _context = fixture.NewContext; _logger = new Mock>().Object; - _missionService = new MissionRunService(_context, _logger); + _missionRunService = new MissionRunService(_context, _logger); } public void Dispose() @@ -35,35 +35,36 @@ public void Dispose() [Fact] public async Task ReadIdDoesNotExist() { - var mission = await _missionService.ReadById("some_id_that_does_not_exist"); - Assert.Null(mission); + var missionRun = await _missionRunService.ReadById("some_id_that_does_not_exist"); + Assert.Null(missionRun); } [Fact] public async Task Create() { var robot = _context.Robots.First(); - int nReportsBefore = _missionService + int nReportsBefore = _missionRunService .ReadAll(new MissionRunQueryStringParameters()) .Result.Count; var testAsset = new Asset { - ShortName = "test", + AssetCode = "test", Name = "test test" }; var testInstallation = new Installation { - ShortName = "test", + InstallationCode = "test", Name = "test test", Asset = testAsset }; - MissionRun mission = + MissionRun missionRun = new() { Name = "testMission", Robot = robot, + MissionId = Guid.NewGuid().ToString(), MapMetadata = new MapMetadata() { MapName = "testMap" }, Area = new Area { @@ -84,8 +85,8 @@ public async Task Create() DesiredStartTime = DateTime.Now }; - await _missionService.Create(mission); - int nReportsAfter = _missionService + await _missionRunService.Create(missionRun); + int nReportsAfter = _missionRunService .ReadAll(new MissionRunQueryStringParameters()) .Result.Count; diff --git a/backend/api/Controllers/AreaController.cs b/backend/api/Controllers/AreaController.cs index e0bd63951..42f4e579d 100644 --- a/backend/api/Controllers/AreaController.cs +++ b/backend/api/Controllers/AreaController.cs @@ -1,7 +1,7 @@ using Api.Controllers.Models; using Api.Database.Models; using Api.Services; -using Api.Utilities; +using Azure; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -28,61 +28,6 @@ IAreaService areaService _areaService = areaService; } - /// - /// List all asset areas in the Flotilla database - /// - /// - /// This query gets all asset areas - /// - [HttpGet] - [Authorize(Roles = Role.Any)] - [ProducesResponseType(typeof(IList), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task>> GetAreas() - { - try - { - var areas = await _areaService.ReadAll(); - return Ok(areas); - } - catch (Exception e) - { - _logger.LogError(e, "Error during GET of areas from database"); - throw; - } - } - - /// - /// Lookup area by specified id. - /// - [HttpGet] - [Authorize(Roles = Role.Any)] - [Route("{id}")] - [ProducesResponseType(typeof(Area), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task> GetAreaById([FromRoute] string id) - { - try - { - var area = await _areaService.ReadById(id); - if (area == null) - return NotFound($"Could not find area with id {id}"); - return Ok(area); - } - catch (Exception e) - { - _logger.LogError(e, "Error during GET of areas from database"); - throw; - } - - } - /// /// Add a new area /// @@ -91,12 +36,13 @@ public async Task> GetAreaById([FromRoute] string id) /// [HttpPost] [Authorize(Roles = Role.Admin)] - [ProducesResponseType(typeof(Area), StatusCodes.Status201Created)] + [ProducesResponseType(typeof(AreaResponse), StatusCodes.Status201Created)] [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status409Conflict)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task> Create([FromBody] CreateAreaQuery area) + public async Task> Create([FromBody] CreateAreaQuery area) { _logger.LogInformation("Creating new area"); try @@ -104,8 +50,8 @@ public async Task> Create([FromBody] CreateAreaQuery area) var existingArea = await _areaService.ReadByAssetAndName(area.AssetCode, area.AreaName); if (existingArea != null) { - _logger.LogInformation("An area for given name and asset already exists"); - return BadRequest($"Area already exists"); + _logger.LogWarning("An area for given name and asset already exists"); + return Conflict($"Area already exists"); } var newArea = await _areaService.Create(area); @@ -113,10 +59,21 @@ public async Task> Create([FromBody] CreateAreaQuery area) "Succesfully created new area with id '{areaId}'", newArea.Id ); + var response = new AreaResponse + { + Id = newArea.Id, + DeckName = newArea.Deck.Name, + InstallationCode = newArea.Installation.InstallationCode, + AssetCode = newArea.Asset.AssetCode, + AreaName = newArea.Name, + MapMetadata = newArea.MapMetadata, + DefaultLocalizationPose = newArea.DefaultLocalizationPose, + SafePositions = newArea.SafePositions + }; return CreatedAtAction( nameof(GetAreaById), new { id = newArea.Id }, - newArea + response ); } catch (Exception e) @@ -126,55 +83,56 @@ public async Task> Create([FromBody] CreateAreaQuery area) } } - /// - /// Add a safe position to a area + /// Add safe position to an area /// /// /// This query adds a new safe position to the database /// [HttpPost] [Authorize(Roles = Role.Admin)] - [Route("{asset}/{installationName}/{deckName}/{areaName}/safe-position")] - [ProducesResponseType(typeof(Area), StatusCodes.Status201Created)] + [Route("{assetCode}/{installationCode}/{deckName}/{areaName}/safe-position")] + [ProducesResponseType(typeof(AreaResponse), StatusCodes.Status201Created)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task> AddSafePosition( - [FromRoute] string asset, - [FromRoute] string installationName, + public async Task> AddSafePosition( + [FromRoute] string assetCode, + [FromRoute] string installationCode, [FromRoute] string deckName, [FromRoute] string areaName, [FromBody] Pose safePosition ) { - _logger.LogInformation("Adding new safe position"); + _logger.LogInformation(@"Adding new safe position to {Asset}, {Installation}, + {Deck}, {Area}", assetCode, installationCode, deckName, areaName); try { - var area = await _areaService.AddSafePosition(asset, areaName, new SafePosition(safePosition)); + var area = await _areaService.AddSafePosition(assetCode, areaName, new SafePosition(safePosition)); if (area != null) { - _logger.LogInformation("Succesfully added new safe position for asset '{assetId}' and name '{name}'", asset, areaName); - return CreatedAtAction(nameof(GetAreaById), new { id = area.Id }, area); ; + _logger.LogInformation(@"Successfully added new safe position for asset '{assetId}' + and name '{name}'", assetCode, areaName); + var response = new AreaResponse + { + Id = area.Id, + DeckName = area.Deck.Name, + InstallationCode = area.Installation.InstallationCode, + AssetCode = area.Asset.AssetCode, + AreaName = area.Name, + MapMetadata = area.MapMetadata, + DefaultLocalizationPose = area.DefaultLocalizationPose, + SafePositions = area.SafePositions + }; + return CreatedAtAction(nameof(GetAreaById), new { id = area.Id }, response); ; } else { - _logger.LogInformation("Creating Area for asset '{assetId}' and name '{name}'", asset, areaName); - // Cloning to avoid tracking same object - var tempPose = ObjectCopier.Clone(safePosition); - area = await _areaService.Create( - new CreateAreaQuery - { - AssetCode = asset, - AreaName = areaName, - InstallationName = installationName, - DeckName = deckName, - DefaultLocalizationPose = new Pose() - }, - new List { tempPose } - ); - return CreatedAtAction(nameof(GetAreaById), new { id = area.Id }, area); + _logger.LogInformation(@"No area with asset {assetCode}, installation {installationCode}, + deck {deckName} and name {areaName} could be found.", assetCode, installationCode, deckName, areaName); + return NotFound(@$"No area with asset {assetCode}, installation {installationCode}, + deck {deckName} and name {areaName} could be found."); } } catch (Exception e) @@ -190,17 +148,105 @@ [FromBody] Pose safePosition [HttpDelete] [Authorize(Roles = Role.Admin)] [Route("{id}")] - [ProducesResponseType(typeof(Area), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(AreaResponse), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task> DeleteArea([FromRoute] string id) + public async Task> DeleteArea([FromRoute] string id) { var area = await _areaService.Delete(id); if (area is null) return NotFound($"Area with id {id} not found"); - return Ok(area); + var response = new AreaResponse + { + Id = area.Id, + DeckName = area.Deck.Name, + InstallationCode = area.Installation.InstallationCode, + AssetCode = area.Asset.AssetCode, + AreaName = area.Name, + MapMetadata = area.MapMetadata, + DefaultLocalizationPose = area.DefaultLocalizationPose, + SafePositions = area.SafePositions + }; + return Ok(response); + } + + /// + /// List all asset areas in the Flotilla database + /// + /// + /// This query gets all asset areas + /// + [HttpGet] + [Authorize(Roles = Role.Any)] + [ProducesResponseType(typeof(IList), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task>> GetAreas() + { + try + { + var areas = await _areaService.ReadAll(); + var response = areas.Select(area => new AreaResponse + { + Id = area.Id, + DeckName = area.Deck.Name, + InstallationCode = area.Installation.InstallationCode, + AssetCode = area.Asset.AssetCode, + AreaName = area.Name, + MapMetadata = area.MapMetadata, + DefaultLocalizationPose = area.DefaultLocalizationPose, + SafePositions = area.SafePositions + }); + return Ok(response); + } + catch (Exception e) + { + _logger.LogError(e, "Error during GET of areas from database"); + throw; + } + } + + /// + /// Lookup area by specified id. + /// + [HttpGet] + [Authorize(Roles = Role.Any)] + [Route("{id}")] + [ProducesResponseType(typeof(AreaResponse), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task> GetAreaById([FromRoute] string id) + { + try + { + var area = await _areaService.ReadById(id); + if (area == null) + return NotFound($"Could not find area with id {id}"); + var response = new AreaResponse + { + Id = area.Id, + DeckName = area.Deck.Name, + InstallationCode = area.Installation.InstallationCode, + AssetCode = area.Asset.AssetCode, + AreaName = area.Name, + MapMetadata = area.MapMetadata, + DefaultLocalizationPose = area.DefaultLocalizationPose, + SafePositions = area.SafePositions + }; + return Ok(response); + } + catch (Exception e) + { + _logger.LogError(e, "Error during GET of areas from database"); + throw; + } + } /// @@ -219,30 +265,38 @@ public async Task> GetMapMetadata([FromRoute] string i var area = await _areaService.ReadById(id); if (area is null) { - _logger.LogError("Area not found for area ID {areaId}", id); - return NotFound("Could not find this area"); + string errorMessage = $"Area not found for area with ID {id}"; + _logger.LogError("{ErrorMessage}", errorMessage); + return NotFound(errorMessage); } - MapMetadata? map; + MapMetadata? mapMetadata; var positions = new List { area.DefaultLocalizationPose.Position }; try { - map = await _mapService.ChooseMapFromPositions(positions, area.Deck.Installation.Asset.ShortName); + mapMetadata = await _mapService.ChooseMapFromPositions(positions, area.Deck.Installation.Asset.AssetCode); + } + catch (RequestFailedException e) + { + string errorMessage = $"An error occurred while retrieving the map for area {area.Id}"; + _logger.LogError(e, "{ErrorMessage}", errorMessage); + return StatusCode(StatusCodes.Status502BadGateway, errorMessage); } - catch (ArgumentOutOfRangeException) + catch (ArgumentOutOfRangeException e) { - _logger.LogWarning("Unable to find a map for area '{areaId}'", area.Id); - return NotFound("Could not find map suited for the positions in this area"); + string errorMessage = $"Could not find a suitable map for area {area.Id}"; + _logger.LogError(e, "{ErrorMessage}", errorMessage); + return NotFound(errorMessage); } - if (map == null) + if (mapMetadata == null) { - return NotFound("Could not find map for this area"); + return NotFound("A map which contained at least half of the points in this mission could not be found"); } - return Ok(map); + return Ok(mapMetadata); } } } diff --git a/backend/api/Controllers/AssetController.cs b/backend/api/Controllers/AssetController.cs new file mode 100644 index 000000000..b6025d4f1 --- /dev/null +++ b/backend/api/Controllers/AssetController.cs @@ -0,0 +1,147 @@ +using Api.Controllers.Models; +using Api.Database.Models; +using Api.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Api.Controllers +{ + [ApiController] + [Route("assets")] + public class AssetController : ControllerBase + { + private readonly IAssetService _assetService; + + private readonly IMapService _mapService; + + private readonly ILogger _logger; + + public AssetController( + ILogger logger, + IMapService mapService, + IAssetService assetService + ) + { + _logger = logger; + _mapService = mapService; + _assetService = assetService; + } + + /// + /// List all assets in the Flotilla database + /// + /// + /// This query gets all assets + /// + [HttpGet] + [Authorize(Roles = Role.Any)] + [ProducesResponseType(typeof(IList), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task>> GetAssets() + { + try + { + var assets = await _assetService.ReadAll(); + return Ok(assets); + } + catch (Exception e) + { + _logger.LogError(e, "Error during GET of assets from database"); + throw; + } + } + + /// + /// Lookup asset by specified id. + /// + [HttpGet] + [Authorize(Roles = Role.Any)] + [Route("{id}")] + [ProducesResponseType(typeof(Asset), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task> GetAssetById([FromRoute] string id) + { + try + { + var asset = await _assetService.ReadById(id); + if (asset == null) + return NotFound($"Could not find asset with id {id}"); + return Ok(asset); + } + catch (Exception e) + { + _logger.LogError(e, "Error during GET of asset from database"); + throw; + } + + } + + /// + /// Add a new asset + /// + /// + /// This query adds a new asset to the database + /// + [HttpPost] + [Authorize(Roles = Role.Admin)] + [ProducesResponseType(typeof(Asset), StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task> Create([FromBody] CreateAssetQuery asset) + { + _logger.LogInformation("Creating new asset"); + try + { + var existingAsset = await _assetService.ReadByName(asset.AssetCode); + if (existingAsset != null) + { + _logger.LogInformation("An asset for given name and asset already exists"); + return BadRequest($"Asset already exists"); + } + + var newAsset = await _assetService.Create(asset); + _logger.LogInformation( + "Succesfully created new asset with id '{assetId}'", + newAsset.Id + ); + return CreatedAtAction( + nameof(GetAssetById), + new { id = newAsset.Id }, + newAsset + ); + } + catch (Exception e) + { + _logger.LogError(e, "Error while creating new asset"); + throw; + } + } + + /// + /// Deletes the asset with the specified id from the database. + /// + [HttpDelete] + [Authorize(Roles = Role.Admin)] + [Route("{id}")] + [ProducesResponseType(typeof(Asset), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task> DeleteAsset([FromRoute] string id) + { + var asset = await _assetService.Delete(id); + if (asset is null) + return NotFound($"Asset with id {id} not found"); + return Ok(asset); + } + } +} diff --git a/backend/api/Controllers/DeckController.cs b/backend/api/Controllers/DeckController.cs new file mode 100644 index 000000000..f17089d85 --- /dev/null +++ b/backend/api/Controllers/DeckController.cs @@ -0,0 +1,163 @@ +using Api.Controllers.Models; +using Api.Database.Models; +using Api.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Api.Controllers +{ + [ApiController] + [Route("decks")] + public class DeckController : ControllerBase + { + private readonly IDeckService _deckService; + private readonly IAssetService _assetService; + private readonly IInstallationService _installationService; + + private readonly IMapService _mapService; + + private readonly ILogger _logger; + + public DeckController( + ILogger logger, + IMapService mapService, + IDeckService deckService, + IAssetService assetService, + IInstallationService installationService + ) + { + _logger = logger; + _mapService = mapService; + _deckService = deckService; + _assetService = assetService; + _installationService = installationService; + } + + /// + /// List all decks in the Flotilla database + /// + /// + /// This query gets all decks + /// + [HttpGet] + [Authorize(Roles = Role.Any)] + [ProducesResponseType(typeof(IList), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task>> GetDecks() + { + try + { + var decks = await _deckService.ReadAll(); + return Ok(decks); + } + catch (Exception e) + { + _logger.LogError(e, "Error during GET of decks from database"); + throw; + } + } + + /// + /// Lookup deck by specified id. + /// + [HttpGet] + [Authorize(Roles = Role.Any)] + [Route("{id}")] + [ProducesResponseType(typeof(Deck), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task> GetDeckById([FromRoute] string id) + { + try + { + var deck = await _deckService.ReadById(id); + if (deck == null) + return NotFound($"Could not find deck with id {id}"); + return Ok(deck); + } + catch (Exception e) + { + _logger.LogError(e, "Error during GET of deck from database"); + throw; + } + + } + + /// + /// Add a new deck + /// + /// + /// This query adds a new deck to the database + /// + [HttpPost] + [Authorize(Roles = Role.Admin)] + [ProducesResponseType(typeof(Deck), StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task> Create([FromBody] CreateDeckQuery deck) + { + _logger.LogInformation("Creating new deck"); + try + { + var existingAsset = await _assetService.ReadByName(deck.AssetCode); + if (existingAsset == null) + { + return NotFound($"Could not find asset with name {deck.AssetCode}"); + } + var existingInstallation = await _installationService.ReadByAssetAndName(existingAsset, deck.InstallationCode); + if (existingInstallation == null) + { + return NotFound($"Could not find installation with name {deck.InstallationCode}"); + } + var existingDeck = await _deckService.ReadByAssetAndInstallationAndName(existingAsset, existingInstallation, deck.Name); + if (existingDeck != null) + { + _logger.LogInformation("An deck for given name and deck already exists"); + return BadRequest($"Deck already exists"); + } + + var newDeck = await _deckService.Create(deck); + _logger.LogInformation( + "Succesfully created new deck with id '{deckId}'", + newDeck.Id + ); + return CreatedAtAction( + nameof(GetDeckById), + new { id = newDeck.Id }, + newDeck + ); + } + catch (Exception e) + { + _logger.LogError(e, "Error while creating new deck"); + throw; + } + } + + /// + /// Deletes the deck with the specified id from the database. + /// + [HttpDelete] + [Authorize(Roles = Role.Admin)] + [Route("{id}")] + [ProducesResponseType(typeof(Deck), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task> DeleteDeck([FromRoute] string id) + { + var deck = await _deckService.Delete(id); + if (deck is null) + return NotFound($"Deck with id {id} not found"); + return Ok(deck); + } + } +} diff --git a/backend/api/Controllers/InstallationController.cs b/backend/api/Controllers/InstallationController.cs new file mode 100644 index 000000000..b44853f4c --- /dev/null +++ b/backend/api/Controllers/InstallationController.cs @@ -0,0 +1,155 @@ +using Api.Controllers.Models; +using Api.Database.Models; +using Api.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Api.Controllers +{ + [ApiController] + [Route("installations")] + public class InstallationController : ControllerBase + { + private readonly IInstallationService _installationService; + private readonly IAssetService _assetService; + + private readonly IMapService _mapService; + + private readonly ILogger _logger; + + public InstallationController( + ILogger logger, + IMapService mapService, + IInstallationService installationService, + IAssetService assetService + ) + { + _logger = logger; + _mapService = mapService; + _installationService = installationService; + _assetService = assetService; + } + + /// + /// List all installations in the Flotilla database + /// + /// + /// This query gets all installations + /// + [HttpGet] + [Authorize(Roles = Role.Any)] + [ProducesResponseType(typeof(IList), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task>> GetInstallations() + { + try + { + var installations = await _installationService.ReadAll(); + return Ok(installations); + } + catch (Exception e) + { + _logger.LogError(e, "Error during GET of installations from database"); + throw; + } + } + + /// + /// Lookup installation by specified id. + /// + [HttpGet] + [Authorize(Roles = Role.Any)] + [Route("{id}")] + [ProducesResponseType(typeof(Installation), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task> GetInstallationById([FromRoute] string id) + { + try + { + var installation = await _installationService.ReadById(id); + if (installation == null) + return NotFound($"Could not find installation with id {id}"); + return Ok(installation); + } + catch (Exception e) + { + _logger.LogError(e, "Error during GET of installation from database"); + throw; + } + + } + + /// + /// Add a new installation + /// + /// + /// This query adds a new installation to the database + /// + [HttpPost] + [Authorize(Roles = Role.Admin)] + [ProducesResponseType(typeof(Installation), StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task> Create([FromBody] CreateInstallationQuery installation) + { + _logger.LogInformation("Creating new installation"); + try + { + var existingAsset = await _assetService.ReadByName(installation.AssetCode); + if (existingAsset == null) + { + return NotFound($"Asset with asset code {installation.AssetCode} not found"); + } + var existingInstallation = await _installationService.ReadByAssetAndName(existingAsset, installation.InstallationCode); + if (existingInstallation != null) + { + _logger.LogInformation("An installation for given name and installation already exists"); + return BadRequest($"Installation already exists"); + } + + var newInstallation = await _installationService.Create(installation); + _logger.LogInformation( + "Succesfully created new installation with id '{installationId}'", + newInstallation.Id + ); + return CreatedAtAction( + nameof(GetInstallationById), + new { id = newInstallation.Id }, + newInstallation + ); + } + catch (Exception e) + { + _logger.LogError(e, "Error while creating new installation"); + throw; + } + } + + /// + /// Deletes the installation with the specified id from the database. + /// + [HttpDelete] + [Authorize(Roles = Role.Admin)] + [Route("{id}")] + [ProducesResponseType(typeof(Installation), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task> DeleteInstallation([FromRoute] string id) + { + var installation = await _installationService.Delete(id); + if (installation is null) + return NotFound($"Installation with id {id} not found"); + return Ok(installation); + } + } +} diff --git a/backend/api/Controllers/MissionController.cs b/backend/api/Controllers/MissionController.cs index 7c88f6456..e77fc9397 100644 --- a/backend/api/Controllers/MissionController.cs +++ b/backend/api/Controllers/MissionController.cs @@ -1,4 +1,5 @@ using System.Text.Json; +using System.Globalization; using Api.Controllers.Models; using Api.Database.Models; using Api.Services; @@ -17,7 +18,7 @@ public class MissionController : ControllerBase private readonly IAreaService _areaService; private readonly IRobotService _robotService; private readonly IEchoService _echoService; - private readonly ISourceService _sourceService; + private readonly ICustomMissionService _customMissionService; private readonly ILogger _logger; private readonly IStidService _stidService; private readonly IMapService _mapService; @@ -28,7 +29,7 @@ public MissionController( IAreaService areaService, IRobotService robotService, IEchoService echoService, - ISourceService sourceService, + ICustomMissionService customMissionService, ILogger logger, IMapService mapService, IStidService stidService @@ -39,7 +40,7 @@ IStidService stidService _areaService = areaService; _robotService = robotService; _echoService = echoService; - _sourceService = sourceService; + _customMissionService = customMissionService; _mapService = mapService; _stidService = stidService; _logger = logger; @@ -49,7 +50,7 @@ IStidService stidService /// List all mission runs in the Flotilla database /// /// - /// This query gets all missions + /// This query gets all mission runs /// [HttpGet("runs")] [Authorize(Roles = Role.Any)] @@ -75,10 +76,10 @@ [FromQuery] MissionRunQueryStringParameters parameters return BadRequest("Max EndTime cannot be less than min EndTime"); } - PagedList missions; + PagedList missionRuns; try { - missions = await _missionRunService.ReadAll(parameters); + missionRuns = await _missionRunService.ReadAll(parameters); } catch (InvalidDataException e) { @@ -88,12 +89,12 @@ [FromQuery] MissionRunQueryStringParameters parameters var metadata = new { - missions.TotalCount, - missions.PageSize, - missions.CurrentPage, - missions.TotalPages, - missions.HasNext, - missions.HasPrevious + missionRuns.TotalCount, + missionRuns.PageSize, + missionRuns.CurrentPage, + missionRuns.TotalPages, + missionRuns.HasNext, + missionRuns.HasPrevious }; Response.Headers.Add( @@ -101,14 +102,14 @@ [FromQuery] MissionRunQueryStringParameters parameters JsonSerializer.Serialize(metadata) ); - return Ok(missions); + return Ok(missionRuns); } /// /// List all mission definitions in the Flotilla database /// /// - /// This query gets all missions + /// This query gets all mission definitions /// [HttpGet("definitions")] [Authorize(Roles = Role.Any)] @@ -121,11 +122,10 @@ public async Task>> GetMissionDefinitions( [FromQuery] MissionDefinitionQueryStringParameters parameters ) { - // TODO: define new parameters using primarily area and source type - PagedList missions; + PagedList missionDefinitions; try { - missions = await _missionDefinitionService.ReadAll(parameters); + missionDefinitions = await _missionDefinitionService.ReadAll(parameters); } catch (InvalidDataException e) { @@ -135,12 +135,12 @@ [FromQuery] MissionDefinitionQueryStringParameters parameters var metadata = new { - missions.TotalCount, - missions.PageSize, - missions.CurrentPage, - missions.TotalPages, - missions.HasNext, - missions.HasPrevious + missionDefinitions.TotalCount, + missionDefinitions.PageSize, + missionDefinitions.CurrentPage, + missionDefinitions.TotalPages, + missionDefinitions.HasNext, + missionDefinitions.HasPrevious }; Response.Headers.Add( @@ -148,7 +148,7 @@ [FromQuery] MissionDefinitionQueryStringParameters parameters JsonSerializer.Serialize(metadata) ); - return Ok(missions); + return Ok(missionDefinitions); } /// @@ -164,10 +164,10 @@ [FromQuery] MissionDefinitionQueryStringParameters parameters [ProducesResponseType(StatusCodes.Status500InternalServerError)] public async Task> GetMissionRunById([FromRoute] string id) { - var mission = await _missionRunService.ReadById(id); - if (mission == null) - return NotFound($"Could not find mission with id {id}"); - return Ok(mission); + var missioRun = await _missionRunService.ReadById(id); + if (missioRun == null) + return NotFound($"Could not find mission run with id {id}"); + return Ok(missioRun); } /// @@ -183,10 +183,10 @@ public async Task> GetMissionRunById([FromRoute] string [ProducesResponseType(StatusCodes.Status500InternalServerError)] public async Task> GetMissionDefinitionById([FromRoute] string id) { - var mission = await _missionDefinitionService.ReadById(id); - if (mission == null) - return NotFound($"Could not find mission with id {id}"); - return Ok(mission); + var missionDefinition = await _missionDefinitionService.ReadById(id); + if (missionDefinition == null) + return NotFound($"Could not find mission definition with id {id}"); + return Ok(missionDefinition); } /// @@ -216,12 +216,12 @@ public async Task> GetMap([FromRoute] string assetCode, str } /// - /// Reschedule an existing mission + /// Schedule an existing mission definition /// /// - /// This query reschedules an existing mission and adds it to the database + /// This query schedules an existing mission and adds it to the database /// - [HttpPost("{missionId}/reschedule")] + [HttpPost("schedule")] [Authorize(Roles = Role.User)] [ProducesResponseType(typeof(MissionRun), StatusCodes.Status201Created)] [ProducesResponseType(StatusCodes.Status400BadRequest)] @@ -229,42 +229,26 @@ public async Task> GetMap([FromRoute] string assetCode, str [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task> Rechedule( - [FromRoute] string missionId, - [FromBody] RescheduleMissionQuery rescheduledMissionQuery + public async Task> Schedule( + [FromBody] ScheduleMissionQuery scheduledMissionQuery ) { - var robot = await _robotService.ReadById(rescheduledMissionQuery.RobotId); + var robot = await _robotService.ReadById(scheduledMissionQuery.RobotId); if (robot is null) - return NotFound($"Could not find robot with id {rescheduledMissionQuery.RobotId}"); - - MissionDefinition? missionDefinition; - try - { - missionDefinition = await _missionDefinitionService.ReadById(missionId); - if (missionDefinition == null) - return NotFound("Mission definition not found"); - } - catch (HttpRequestException e) - { - if (e.StatusCode.HasValue && (int)e.StatusCode.Value == 404) - { - _logger.LogWarning( - "Could not find mission definition with id={id}", - missionId - ); - return NotFound("Mission definition not found"); - } + return NotFound($"Could not find robot with id {scheduledMissionQuery.RobotId}"); - _logger.LogError(e, "Error getting mission database"); - return StatusCode(StatusCodes.Status502BadGateway, $"{e.Message}"); - } + var missionDefinition = await _missionDefinitionService.ReadById(scheduledMissionQuery.MissionDefinitionId); + if (missionDefinition == null) + return NotFound("Mission definition not found"); List? missionTasks; - switch (missionDefinition.Source.Type) + missionTasks = missionDefinition.Source.Type switch { - case MissionSourceType.Echo: - missionTasks = _echoService.GetMissionById(Int32.Parse(missionDefinition.Source.Id)).Result.Tags + MissionSourceType.Echo => + // CultureInfo is not important here since we are not using decimal points + missionTasks = _echoService.GetMissionById( + int.Parse(missionDefinition.Source.SourceId, new CultureInfo("en-US")) + ).Result.Tags .Select( t => { @@ -274,39 +258,37 @@ [FromBody] RescheduleMissionQuery rescheduledMissionQuery return new MissionTask(t, tagPosition); } ) - .ToList(); - break; - case MissionSourceType.Custom: - missionTasks = _sourceService.GetMissionTasksFromURL(missionDefinition.Source.URL); - break; - default: - return BadRequest("Invalid mission source type provided"); - } + .ToList(), + MissionSourceType.Custom => + missionTasks = await _customMissionService.GetMissionTasksFromMissionId(missionDefinition.Source.SourceId), + _ => + throw new MissionSourceTypeException($"Mission type {missionDefinition.Source.Type} is not accounted for") + }; if (missionTasks == null) return NotFound("No mission tasks were found for the requested mission"); - var scheduledMission = new MissionRun + var missionRun = new MissionRun { Name = missionDefinition.Name, Robot = robot, MissionId = missionDefinition.Id, Status = MissionStatus.Pending, - DesiredStartTime = rescheduledMissionQuery.DesiredStartTime, + DesiredStartTime = scheduledMissionQuery.DesiredStartTime, Tasks = missionTasks, AssetCode = missionDefinition.AssetCode, Area = missionDefinition.Area, MapMetadata = new MapMetadata() }; - await _mapService.AssignMapToMission(scheduledMission); + await _mapService.AssignMapToMission(missionRun); - if (scheduledMission.Tasks.Any()) - scheduledMission.CalculateEstimatedDuration(); + if (missionRun.Tasks.Any()) + missionRun.CalculateEstimatedDuration(); - var newMission = await _missionRunService.Create(scheduledMission); + var newMissionRun = await _missionRunService.Create(missionRun); - return CreatedAtAction(nameof(GetMissionRunById), new { id = newMission.Id }, newMission); + return CreatedAtAction(nameof(GetMissionRunById), new { id = newMissionRun.Id }, newMissionRun); } /// @@ -327,6 +309,8 @@ public async Task> Create( [FromBody] ScheduledMissionQuery scheduledMissionQuery ) { + // TODO: once we have a good way of creating mission definitions for echo missions, + // we can delete this endpoint var robot = await _robotService.ReadById(scheduledMissionQuery.RobotId); if (robot is null) return NotFound($"Could not find robot with id {scheduledMissionQuery.RobotId}"); @@ -380,6 +364,7 @@ [FromBody] ScheduledMissionQuery scheduledMissionQuery if (area == null) { + // This is disabled for now as the Area database is not yet populated //return NotFound($"Could not find area with name {scheduledMissionQuery.AreaName} in asset {scheduledMissionQuery.AssetCode}"); } @@ -390,8 +375,7 @@ [FromBody] ScheduledMissionQuery scheduledMissionQuery Id = Guid.NewGuid().ToString(), Source = new Source { - Id = Guid.NewGuid().ToString(), - URL = $"robots/robot-plan/{echoMission.Id}", // Could use echoMission.URL here, but that would necessitate new retrieval methods + SourceId = $"{echoMission.Id}", Type = MissionSourceType.Echo }, Name = echoMission.Name, @@ -400,7 +384,7 @@ [FromBody] ScheduledMissionQuery scheduledMissionQuery Area = area }; - var scheduledMission = new MissionRun + var missionRun = new MissionRun { Name = echoMission.Name, Robot = robot, @@ -413,16 +397,16 @@ [FromBody] ScheduledMissionQuery scheduledMissionQuery MapMetadata = new MapMetadata() }; - await _mapService.AssignMapToMission(scheduledMission); + await _mapService.AssignMapToMission(missionRun); - if (scheduledMission.Tasks.Any()) - scheduledMission.CalculateEstimatedDuration(); + if (missionRun.Tasks.Any()) + missionRun.CalculateEstimatedDuration(); var newMissionDefinition = await _missionDefinitionService.Create(scheduledMissionDefinition); - var newMission = await _missionRunService.Create(scheduledMission); + var newMissionRun = await _missionRunService.Create(missionRun); - return CreatedAtAction(nameof(GetMissionRunById), new { id = newMission.Id }, newMission); + return CreatedAtAction(nameof(GetMissionRunById), new { id = newMissionRun.Id }, newMissionRun); } /// @@ -444,11 +428,6 @@ public async Task> Create( [FromBody] CustomMissionQuery customMissionQuery ) { - - // TODO: only allow admins - - // TODO: create new endpoint for scheduling existing custom missions - var robot = await _robotService.ReadById(customMissionQuery.RobotId); if (robot is null) return NotFound($"Could not find robot with id {customMissionQuery.RobotId}"); @@ -460,16 +439,13 @@ [FromBody] CustomMissionQuery customMissionQuery if (area == null) return NotFound($"Could not find area with name {customMissionQuery.AreaName} in asset {customMissionQuery.AssetCode}"); - string customMissionId = Guid.NewGuid().ToString(); - var sourceURL = await _sourceService.UploadSource(customMissionId, missionTasks); + string sourceURL = await _customMissionService.UploadSource(missionTasks); var customMissionDefinition = new MissionDefinition { - Id = customMissionId, Source = new Source { - Id = Guid.NewGuid().ToString(), - URL = sourceURL.ToString(), + SourceId = sourceURL.ToString(), Type = MissionSourceType.Echo }, Name = customMissionQuery.Name, @@ -500,9 +476,9 @@ [FromBody] CustomMissionQuery customMissionQuery var newMissionDefinition = await _missionDefinitionService.Create(customMissionDefinition); - var newMission = await _missionRunService.Create(scheduledMission); + var newMissionRun = await _missionRunService.Create(scheduledMission); - return CreatedAtAction(nameof(GetMissionRunById), new { id = newMission.Id }, newMission); + return CreatedAtAction(nameof(GetMissionRunById), new { id = newMissionRun.Id }, newMissionRun); } /// @@ -518,10 +494,10 @@ [FromBody] CustomMissionQuery customMissionQuery [ProducesResponseType(StatusCodes.Status500InternalServerError)] public async Task> DeleteMissionDefinition([FromRoute] string id) { - var mission = await _missionDefinitionService.Delete(id); - if (mission is null) + var missionDefinition = await _missionDefinitionService.Delete(id); + if (missionDefinition is null) return NotFound($"Mission definition with id {id} not found"); - return Ok(mission); + return Ok(missionDefinition); } /// @@ -537,9 +513,9 @@ public async Task> DeleteMissionDefinition([From [ProducesResponseType(StatusCodes.Status500InternalServerError)] public async Task> DeleteMissionRun([FromRoute] string id) { - var mission = await _missionRunService.Delete(id); - if (mission is null) + var missionRun = await _missionRunService.Delete(id); + if (missionRun is null) return NotFound($"Mission run with id {id} not found"); - return Ok(mission); + return Ok(missionRun); } } diff --git a/backend/api/Controllers/Models/AreaResponse.cs b/backend/api/Controllers/Models/AreaResponse.cs new file mode 100644 index 000000000..9b0a3ad3a --- /dev/null +++ b/backend/api/Controllers/Models/AreaResponse.cs @@ -0,0 +1,22 @@ +#pragma warning disable CS8618 +namespace Api.Database.Models +{ + public class AreaResponse + { + public string Id { get; set; } + + public string DeckName { get; set; } + + public string InstallationCode { get; set; } + + public string AssetCode { get; set; } + + public string AreaName { get; set; } + + public MapMetadata MapMetadata { get; set; } + + public Pose DefaultLocalizationPose { get; set; } + + public IList SafePositions { get; set; } + } +} diff --git a/backend/api/Controllers/Models/CreateAreaQuery.cs b/backend/api/Controllers/Models/CreateAreaQuery.cs index ac25914af..2abd7f051 100644 --- a/backend/api/Controllers/Models/CreateAreaQuery.cs +++ b/backend/api/Controllers/Models/CreateAreaQuery.cs @@ -5,7 +5,7 @@ namespace Api.Controllers.Models public struct CreateAreaQuery { public string AssetCode { get; set; } - public string InstallationName { get; set; } + public string InstallationCode { get; set; } public string DeckName { get; set; } public string AreaName { get; set; } diff --git a/backend/api/Controllers/Models/CreateAssetDeckQuery.cs b/backend/api/Controllers/Models/CreateAssetDeckQuery.cs deleted file mode 100644 index c42fd473f..000000000 --- a/backend/api/Controllers/Models/CreateAssetDeckQuery.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Api.Database.Models; - -namespace Api.Controllers.Models -{ - public struct CreateAssetDeckQuery - { - public string AssetCode { get; set; } - - public string DeckName { get; set; } - - public Pose DefaultLocalizationPose { get; set; } - } -} diff --git a/backend/api/Controllers/Models/CreateAssetQuery.cs b/backend/api/Controllers/Models/CreateAssetQuery.cs new file mode 100644 index 000000000..9341141b8 --- /dev/null +++ b/backend/api/Controllers/Models/CreateAssetQuery.cs @@ -0,0 +1,8 @@ +namespace Api.Controllers.Models +{ + public struct CreateAssetQuery + { + public string AssetCode { get; set; } + public string Name { get; set; } + } +} diff --git a/backend/api/Controllers/Models/CreateDeckQuery.cs b/backend/api/Controllers/Models/CreateDeckQuery.cs new file mode 100644 index 000000000..235f5cf08 --- /dev/null +++ b/backend/api/Controllers/Models/CreateDeckQuery.cs @@ -0,0 +1,9 @@ +namespace Api.Controllers.Models +{ + public struct CreateDeckQuery + { + public string AssetCode { get; set; } + public string InstallationCode { get; set; } + public string Name { get; set; } + } +} diff --git a/backend/api/Controllers/Models/CreateInstallationQuery.cs b/backend/api/Controllers/Models/CreateInstallationQuery.cs new file mode 100644 index 000000000..4bab44759 --- /dev/null +++ b/backend/api/Controllers/Models/CreateInstallationQuery.cs @@ -0,0 +1,9 @@ +namespace Api.Controllers.Models +{ + public struct CreateInstallationQuery + { + public string AssetCode { get; set; } + public string InstallationCode { get; set; } + public string Name { get; set; } + } +} diff --git a/backend/api/Controllers/Models/CreateRobotQuery.cs b/backend/api/Controllers/Models/CreateRobotQuery.cs index b165d6763..5675176e1 100644 --- a/backend/api/Controllers/Models/CreateRobotQuery.cs +++ b/backend/api/Controllers/Models/CreateRobotQuery.cs @@ -14,7 +14,7 @@ public struct CreateRobotQuery public string CurrentAsset { get; set; } - public AssetDeck CurrentAssetDeck { get; set; } + public Area CurrentArea { get; set; } public IList VideoStreams { get; set; } diff --git a/backend/api/Controllers/Models/RescheduleMissionQuery.cs b/backend/api/Controllers/Models/RescheduleMissionQuery.cs deleted file mode 100644 index de0109324..000000000 --- a/backend/api/Controllers/Models/RescheduleMissionQuery.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Api.Database.Models; - -namespace Api.Controllers.Models -{ - public class RescheduleMissionQuery - { - public MissionSourceType MissionType { get; set; } - public string RobotId { get; set; } - public DateTimeOffset DesiredStartTime { get; set; } - public TimeSpan? InspectionFrequency { get; set; } - } -} diff --git a/backend/api/Controllers/Models/ScheduleMissionQuery.cs b/backend/api/Controllers/Models/ScheduleMissionQuery.cs new file mode 100644 index 000000000..921c714cd --- /dev/null +++ b/backend/api/Controllers/Models/ScheduleMissionQuery.cs @@ -0,0 +1,9 @@ +namespace Api.Controllers.Models +{ + public class ScheduleMissionQuery + { + public string MissionDefinitionId { get; set; } + public string RobotId { get; set; } + public DateTimeOffset DesiredStartTime { get; set; } + } +} diff --git a/backend/api/Controllers/RobotController.cs b/backend/api/Controllers/RobotController.cs index 33f8a8c64..91ae652ea 100644 --- a/backend/api/Controllers/RobotController.cs +++ b/backend/api/Controllers/RobotController.cs @@ -16,7 +16,7 @@ public class RobotController : ControllerBase private readonly ILogger _logger; private readonly IRobotService _robotService; private readonly IIsarService _isarService; - private readonly IMissionRunService _missionService; + private readonly IMissionRunService _missionRunService; private readonly IRobotModelService _robotModelService; private readonly IAreaService _areaService; @@ -24,7 +24,7 @@ public RobotController( ILogger logger, IRobotService robotService, IIsarService isarService, - IMissionRunService missionService, + IMissionRunService missionRunService, IRobotModelService robotModelService, IAreaService areaService ) @@ -32,7 +32,7 @@ IAreaService areaService _logger = logger; _robotService = robotService; _isarService = isarService; - _missionService = missionService; + _missionRunService = missionRunService; _robotModelService = robotModelService; _areaService = areaService; } @@ -324,14 +324,14 @@ [FromBody] VideoStream videoStream } /// - /// Start the mission in the database with the corresponding 'missionId' for the robot with id 'robotId' + /// Start the mission in the database with the corresponding 'missionRunId' for the robot with id 'robotId' /// /// /// This query starts a mission for a given robot /// [HttpPost] [Authorize(Roles = Role.Admin)] - [Route("{robotId}/start/{missionId}")] + [Route("{robotId}/start/{missionRunId}")] [ProducesResponseType(typeof(MissionRun), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] @@ -340,7 +340,7 @@ [FromBody] VideoStream videoStream [ProducesResponseType(StatusCodes.Status500InternalServerError)] public async Task> StartMission( [FromRoute] string robotId, - [FromRoute] string missionId + [FromRoute] string missionRunId ) { var robot = await _robotService.ReadById(robotId); @@ -360,18 +360,18 @@ [FromRoute] string missionId return Conflict($"The Robot is not available ({robot.Status})"); } - var mission = await _missionService.ReadById(missionId); + var missionRun = await _missionRunService.ReadById(missionRunId); - if (mission == null) + if (missionRun == null) { - _logger.LogWarning("Could not find mission with id={id}", missionId); + _logger.LogWarning("Could not find mission with id={id}", missionRunId); return NotFound("Mission not found"); } IsarMission isarMission; try { - isarMission = await _isarService.StartMission(robot, mission); + isarMission = await _isarService.StartMission(robot, missionRun); } catch (HttpRequestException e) { @@ -399,26 +399,26 @@ [FromRoute] string missionId return StatusCode(StatusCodes.Status500InternalServerError, message); } - mission.UpdateWithIsarInfo(isarMission); - mission.Status = MissionStatus.Ongoing; + missionRun.UpdateWithIsarInfo(isarMission); + missionRun.Status = MissionStatus.Ongoing; - await _missionService.Update(mission); + await _missionRunService.Update(missionRun); if (robot.CurrentMissionId != null) { - var orphanedMission = await _missionService.ReadById(robot.CurrentMissionId); - if (orphanedMission != null) + var orphanedMissionRun = await _missionRunService.ReadById(robot.CurrentMissionId); + if (orphanedMissionRun != null) { - orphanedMission.SetToFailed(); - await _missionService.Update(orphanedMission); + orphanedMissionRun.SetToFailed(); + await _missionRunService.Update(orphanedMissionRun); } } robot.Status = RobotStatus.Busy; - robot.CurrentMissionId = mission.Id; + robot.CurrentMissionId = missionRun.Id; await _robotService.Update(robot); - return Ok(mission); + return Ok(missionRun); } /// @@ -679,11 +679,12 @@ [FromBody] ScheduleLocalizationMissionQuery scheduleLocalizationMissionQuery return NotFound("Area not found"); } - var mission = new MissionRun + var missionRun = new MissionRun { Name = "Localization Mission", Robot = robot, Area = area, + AssetCode = "NA", Status = MissionStatus.Pending, DesiredStartTime = DateTimeOffset.UtcNow, Tasks = new List(), @@ -714,16 +715,16 @@ [FromBody] ScheduleLocalizationMissionQuery scheduleLocalizationMissionQuery return StatusCode(StatusCodes.Status500InternalServerError, message); } - mission.UpdateWithIsarInfo(isarMission); - mission.Status = MissionStatus.Ongoing; - await _missionService.Create(mission); + missionRun.UpdateWithIsarInfo(isarMission); + missionRun.Status = MissionStatus.Ongoing; - robot.Status = RobotStatus.Busy; - robot.CurrentMissionId = mission.Id; + await _missionRunService.Create(missionRun); - robot.CurrentArea = area; + robot.Status = RobotStatus.Busy; + robot.CurrentMissionId = missionRun.Id; await _robotService.Update(robot); - return Ok(mission); + robot.CurrentArea = area; + return Ok(missionRun); } /// @@ -741,7 +742,7 @@ [FromBody] ScheduleLocalizationMissionQuery scheduleLocalizationMissionQuery [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task> SafePosition( + public async Task> SendRobotToSafePosition( [FromRoute] string robotId, [FromRoute] string asset, [FromRoute] string areaName @@ -806,7 +807,8 @@ [FromRoute] string areaName InspectionTarget = new Position(), TaskOrder = 0 }; - var mission = new MissionRun + // TODO: The MissionId is nullable because of this mission + var missionRun = new MissionRun { Name = "Drive to Safe Position", Robot = robot, @@ -821,7 +823,7 @@ [FromRoute] string areaName IsarMission isarMission; try { - isarMission = await _isarService.StartMission(robot, mission); + isarMission = await _isarService.StartMission(robot, missionRun); } catch (HttpRequestException e) { @@ -842,15 +844,15 @@ [FromRoute] string areaName return StatusCode(StatusCodes.Status500InternalServerError, message); } - mission.UpdateWithIsarInfo(isarMission); - mission.Status = MissionStatus.Ongoing; + missionRun.UpdateWithIsarInfo(isarMission); + missionRun.Status = MissionStatus.Ongoing; - await _missionService.Create(mission); + await _missionRunService.Create(missionRun); robot.Status = RobotStatus.Busy; - robot.CurrentMissionId = mission.Id; + robot.CurrentMissionId = missionRun.Id; await _robotService.Update(robot); - return Ok(mission); + return Ok(missionRun); } private async void OnIsarUnavailable(Robot robot) @@ -859,14 +861,14 @@ private async void OnIsarUnavailable(Robot robot) robot.Status = RobotStatus.Offline; if (robot.CurrentMissionId != null) { - var mission = await _missionService.ReadById(robot.CurrentMissionId); - if (mission != null) + var missionRun = await _missionRunService.ReadById(robot.CurrentMissionId); + if (missionRun != null) { - mission.SetToFailed(); - await _missionService.Update(mission); + missionRun.SetToFailed(); + await _missionRunService.Update(missionRun); _logger.LogWarning( "Mission '{id}' failed because ISAR could not be reached", - mission.Id + missionRun.Id ); } } @@ -902,5 +904,4 @@ private static float CalculateDistance(Pose pose1, Pose pose2) var pos2 = pose2.Position; return (float)Math.Sqrt(Math.Pow(pos1.X - pos2.X, 2) + Math.Pow(pos1.Y - pos2.Y, 2) + Math.Pow(pos1.Z - pos2.Z, 2)); } - } diff --git a/backend/api/Database/Context/FlotillaDbContext.cs b/backend/api/Database/Context/FlotillaDbContext.cs index f5c5644a0..5d72c5d07 100644 --- a/backend/api/Database/Context/FlotillaDbContext.cs +++ b/backend/api/Database/Context/FlotillaDbContext.cs @@ -26,12 +26,12 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) // https://docs.microsoft.com/en-us/ef/core/modeling/owned-entities // https://docs.microsoft.com/en-us/ef/core/modeling/owned-entities#collections-of-owned-types modelBuilder.Entity( - missionEntity => + missionRunEntity => { if (isSqlLite) - AddConverterForDateTimeOffsets(ref missionEntity); - missionEntity.OwnsMany( - mission => mission.Tasks, + AddConverterForDateTimeOffsets(ref missionRunEntity); + missionRunEntity.OwnsMany( + missionRun => missionRun.Tasks, taskEntity => { if (isSqlLite) @@ -55,11 +55,13 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) ); } ); + //missionRunEntity.HasOne(missionRun => missionRun.MissionDefinition); } ); modelBuilder.Entity().OwnsOne(m => m.MapMetadata).OwnsOne(t => t.TransformationMatrices); modelBuilder.Entity().OwnsOne(m => m.MapMetadata).OwnsOne(b => b.Boundary); + //modelBuilder.Entity().HasOne(m => m.LastRun).WithOne(m => m.MissionDefinition).HasForeignKey(m => m.Id); modelBuilder.Entity().OwnsOne(r => r.Pose).OwnsOne(p => p.Orientation); modelBuilder.Entity().OwnsOne(r => r.Pose).OwnsOne(p => p.Position); modelBuilder.Entity().OwnsMany(r => r.VideoStreams); @@ -82,8 +84,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.Entity().HasIndex(model => model.Type).IsUnique(); // There can only be one unique asset and installation shortname - modelBuilder.Entity().HasIndex(a => new { a.ShortName }).IsUnique(); - modelBuilder.Entity().HasIndex(a => new { a.ShortName }).IsUnique(); + modelBuilder.Entity().HasIndex(a => new { a.AssetCode }).IsUnique(); + modelBuilder.Entity().HasIndex(a => new { a.InstallationCode }).IsUnique(); } // SQLite does not have proper support for DateTimeOffset via Entity Framework Core, see the limitations diff --git a/backend/api/Database/Context/InitDb.cs b/backend/api/Database/Context/InitDb.cs index 5b6d048f7..42c0ed231 100644 --- a/backend/api/Database/Context/InitDb.cs +++ b/backend/api/Database/Context/InitDb.cs @@ -84,7 +84,7 @@ private static List GetAssets() { Id = Guid.NewGuid().ToString(), Name = "Johan Sverdrup", - ShortName = "JSV" + AssetCode = "JSV" }; return new List(new Asset[] { asset1 }); @@ -97,7 +97,7 @@ private static List GetInstallations() Id = Guid.NewGuid().ToString(), Asset = assets[0], Name = "Johan Sverdrup - P1", - ShortName = "P1" + InstallationCode = "P1" }; return new List(new Installation[] { installation1 }); @@ -126,7 +126,7 @@ private static List GetAreas() Asset = decks[0].Installation.Asset, Name = "AP320", MapMetadata = new MapMetadata(), - DefaultLocalizationPose = new Pose {}, + DefaultLocalizationPose = new Pose { }, SafePositions = new List() }; @@ -138,7 +138,7 @@ private static List GetAreas() Asset = decks[0].Installation.Asset, Name = "AP330", MapMetadata = new MapMetadata(), - DefaultLocalizationPose = new Pose {}, + DefaultLocalizationPose = new Pose { }, SafePositions = new List() }; @@ -161,15 +161,13 @@ private static List GetSources() { var source1 = new Source { - Id = Guid.NewGuid().ToString(), - URL = "https://google.com/", + SourceId = "https://google.com/", Type = MissionSourceType.Echo }; var source2 = new Source { - Id = Guid.NewGuid().ToString(), - URL = "https://google.com/", + SourceId = "https://google.com/", Type = MissionSourceType.Custom }; @@ -178,11 +176,11 @@ private static List GetSources() private static List GetMissionDefinitions() { - var mission1 = new MissionDefinition + var missionDefinition1 = new MissionDefinition { - Name = "Placeholder Mission 1", Id = Guid.NewGuid().ToString(), - AssetCode = areas[0].Deck.Installation.Asset.ShortName, + Name = "Placeholder Mission 1", + AssetCode = areas[0].Deck.Installation.Asset.AssetCode, Area = areas[0], Source = sources[0], Comment = "Interesting comment", @@ -190,37 +188,37 @@ private static List GetMissionDefinitions() LastRun = null }; - var mission2 = new MissionDefinition + var missionDefinition2 = new MissionDefinition { - Name = "Placeholder Mission 2", Id = Guid.NewGuid().ToString(), - AssetCode = areas[1].Deck.Installation.Asset.ShortName, + Name = "Placeholder Mission 2", + AssetCode = areas[1].Deck.Installation.Asset.AssetCode, Area = areas[1], Source = sources[1], InspectionFrequency = new DateTime().AddDays(7) - new DateTime(), LastRun = null }; - var mission3 = new MissionDefinition + var missionDefinition3 = new MissionDefinition { - Name = "Placeholder Mission 3", Id = Guid.NewGuid().ToString(), - AssetCode = areas[1].Deck.Installation.Asset.ShortName, + Name = "Placeholder Mission 3", + AssetCode = areas[1].Deck.Installation.Asset.AssetCode, Area = areas[1], Source = sources[1], LastRun = null }; - return new List(new[] { mission1, mission2, mission3 }); + return new List(new[] { missionDefinition1, missionDefinition2, missionDefinition3 }); } private static List GetMissionRuns() { - var mission1 = new MissionRun + var missionRun1 = new MissionRun { Name = "Placeholder Mission 1", Robot = robots[0], - AssetCode = areas[0].Deck.Installation.Asset.ShortName, + AssetCode = areas[0].Deck.Installation.Asset.AssetCode, Area = areas[0], MissionId = missionDefinitions[0].Id, Status = MissionStatus.Successful, @@ -229,11 +227,11 @@ private static List GetMissionRuns() MapMetadata = new MapMetadata() }; - var mission2 = new MissionRun + var missionRun2 = new MissionRun { Name = "Placeholder Mission 2", Robot = robots[1], - AssetCode = areas[1].Deck.Installation.Asset.ShortName, + AssetCode = areas[1].Deck.Installation.Asset.AssetCode, Area = areas[1], MissionId = missionDefinitions[0].Id, Status = MissionStatus.Successful, @@ -241,12 +239,13 @@ private static List GetMissionRuns() Tasks = new List(), MapMetadata = new MapMetadata() }; + missionDefinitions[0].LastRun = missionRun2; - var mission3 = new MissionRun + var missionRun3 = new MissionRun { Name = "Placeholder Mission 3", Robot = robots[2], - AssetCode = areas[1].Deck.Installation.Asset.ShortName, + AssetCode = areas[1].Deck.Installation.Asset.AssetCode, Area = areas[1], MissionId = missionDefinitions[1].Id, Status = MissionStatus.Successful, @@ -255,7 +254,9 @@ private static List GetMissionRuns() MapMetadata = new MapMetadata() }; - return new List(new[] { mission1, mission2, mission3 }); + missionDefinitions[1].LastRun = missionRun3; + + return new List(new[] { missionRun1, missionRun2, missionRun3 }); } public static void PopulateDb(FlotillaDbContext context) @@ -291,13 +292,13 @@ public static void PopulateDb(FlotillaDbContext context) robots[1].Model = models.Find(model => model.Type == RobotType.ExR2)!; robots[2].Model = models.Find(model => model.Type == RobotType.AnymalX)!; - foreach (var mission in missionRuns) + foreach (var missionRun in missionRuns) { var task = ExampleTask; task.Inspections.Add(Inspection); task.Inspections.Add(Inspection2); var tasks = new List { task }; - mission.Tasks = tasks; + missionRun.Tasks = tasks; } context.AddRange(robots); context.AddRange(missionDefinitions); diff --git a/backend/api/Database/Models/Area.cs b/backend/api/Database/Models/Area.cs index 13eeb00c6..5c20757be 100644 --- a/backend/api/Database/Models/Area.cs +++ b/backend/api/Database/Models/Area.cs @@ -20,6 +20,7 @@ public class Area public Asset Asset { get; set; } [Required] + [MaxLength(200)] public string Name { get; set; } [Required] diff --git a/backend/api/Database/Models/Asset.cs b/backend/api/Database/Models/Asset.cs index e5e832c44..08c7ceae1 100644 --- a/backend/api/Database/Models/Asset.cs +++ b/backend/api/Database/Models/Asset.cs @@ -16,6 +16,6 @@ public class Asset [Required] [MaxLength(10)] - public string ShortName { get; set; } + public string AssetCode { get; set; } } } diff --git a/backend/api/Database/Models/AssetDeck.cs b/backend/api/Database/Models/AssetDeck.cs deleted file mode 100644 index 01fd60564..000000000 --- a/backend/api/Database/Models/AssetDeck.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -#pragma warning disable CS8618 -namespace Api.Database.Models -{ - public class AssetDeck - { - [Key] - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public string Id { get; set; } - - [Required] - [MaxLength(200)] - public string AssetCode { get; set; } - - [Required] - [MaxLength(200)] - public string DeckName { get; set; } - - [Required] - public Pose DefaultLocalizationPose { get; set; } - - public IList SafePositions { get; set; } - } -} diff --git a/backend/api/Database/Models/Installation.cs b/backend/api/Database/Models/Installation.cs index 236ec3af1..1e0157d9e 100644 --- a/backend/api/Database/Models/Installation.cs +++ b/backend/api/Database/Models/Installation.cs @@ -14,11 +14,11 @@ public class Installation public Asset Asset { get; set; } [Required] - [MaxLength(200)] - public string Name { get; set; } + [MaxLength(10)] + public string InstallationCode { get; set; } [Required] - [MaxLength(10)] - public string ShortName { get; set; } + [MaxLength(200)] + public string Name { get; set; } } } diff --git a/backend/api/Database/Models/MissionDefinition.cs b/backend/api/Database/Models/MissionDefinition.cs index d2de32cda..228eca2a2 100644 --- a/backend/api/Database/Models/MissionDefinition.cs +++ b/backend/api/Database/Models/MissionDefinition.cs @@ -1,4 +1,5 @@ using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; #pragma warning disable CS8618 namespace Api.Database.Models @@ -6,8 +7,7 @@ namespace Api.Database.Models public class MissionDefinition { [Key] - [Required] - [MaxLength(200)] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public string Id { get; set; } [Required] @@ -17,16 +17,18 @@ public class MissionDefinition [MaxLength(200)] public string Name { get; set; } + [Required] + public string AssetCode { get; set; } + [MaxLength(1000)] public string? Comment { get; set; } public TimeSpan? InspectionFrequency { get; set; } - public MissionRun? LastRun { get; set; } - - [Required] - public string AssetCode { get; set; } + public virtual MissionRun? LastRun { get; set; } public Area? Area { get; set; } + + public bool Deprecated { get; set; } } } diff --git a/backend/api/Database/Models/MissionRun.cs b/backend/api/Database/Models/MissionRun.cs index c02ef058b..c8a7c4b61 100644 --- a/backend/api/Database/Models/MissionRun.cs +++ b/backend/api/Database/Models/MissionRun.cs @@ -11,35 +11,13 @@ public class MissionRun [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public string Id { get; set; } - [MaxLength(200)] + //[Required] // See "Drive to Safe Position" mission in RobotController.cs public string? MissionId { get; set; } - [MaxLength(200)] - public string? IsarMissionId { get; set; } - [Required] [MaxLength(200)] public string Name { get; set; } - [MaxLength(450)] - public string? Description { get; set; } - - [MaxLength(450)] - public string? StatusReason { get; set; } - - [MaxLength(1000)] - public string? Comment { get; set; } - - [Required] - public string AssetCode { get; set; } - - public Area? Area { get; set; } - - [Required] - public virtual Robot Robot { get; set; } - - private MissionStatus _status; - [Required] public MissionStatus Status { @@ -55,6 +33,40 @@ public MissionStatus Status } } + [Required] + [MaxLength(200)] + public string AssetCode { get; set; } + + [Required] + public DateTimeOffset DesiredStartTime { get; set; } + + [Required] + public virtual Robot Robot { get; set; } + + // The tasks are always returned ordered by their order field + [Required] + public IList Tasks + { + get { return _tasks.OrderBy(t => t.TaskOrder).ToList(); } + set { _tasks = value; } + } + + [MaxLength(200)] + public string? IsarMissionId { get; set; } + + [MaxLength(450)] + public string? Description { get; set; } + + [MaxLength(450)] + public string? StatusReason { get; set; } + + [MaxLength(1000)] + public string? Comment { get; set; } + + public Area? Area { get; set; } + + private MissionStatus _status; + public bool IsCompleted => _status is MissionStatus.Aborted @@ -65,9 +77,6 @@ or MissionStatus.PartiallySuccessful public MapMetadata? MapMetadata { get; set; } - [Required] - public DateTimeOffset DesiredStartTime { get; set; } - public DateTimeOffset? StartTime { get; private set; } public DateTimeOffset? EndTime { get; private set; } @@ -79,14 +88,6 @@ or MissionStatus.PartiallySuccessful private IList _tasks; - // The tasks are always returned ordered by their order field - [Required] - public IList Tasks - { - get { return _tasks.OrderBy(t => t.TaskOrder).ToList(); } - set { _tasks = value; } - } - public void UpdateWithIsarInfo(IsarMission isarMission) { IsarMissionId = isarMission.IsarMissionId; diff --git a/backend/api/Database/Models/Robot.cs b/backend/api/Database/Models/Robot.cs index 2f125c259..5ebe4cb21 100644 --- a/backend/api/Database/Models/Robot.cs +++ b/backend/api/Database/Models/Robot.cs @@ -99,7 +99,7 @@ public Robot(CreateRobotQuery createQuery) Name = createQuery.Name; SerialNumber = createQuery.SerialNumber; CurrentAsset = createQuery.CurrentAsset; - CurrentArea = createQuery.CurrentAssetDeck; + CurrentArea = createQuery.CurrentArea; VideoStreams = videoStreams; Host = createQuery.Host; Port = createQuery.Port; diff --git a/backend/api/Database/Models/RobotModel.cs b/backend/api/Database/Models/RobotModel.cs index 54629e24b..cd4e9e444 100644 --- a/backend/api/Database/Models/RobotModel.cs +++ b/backend/api/Database/Models/RobotModel.cs @@ -76,27 +76,27 @@ public void Update(UpdateRobotModelQuery updateQuery) } /// - /// Updates the based on the data in the provided + /// Updates the based on the data in the provided /// - /// - public void UpdateAverageDurationPerTag(List recentMissionsForModelType) + /// + public void UpdateAverageDurationPerTag(List recentMissionRunsForModelType) { - if (recentMissionsForModelType.Any(mission => mission.Robot.Model.Type != Type)) + if (recentMissionRunsForModelType.Any(missionRun => missionRun.Robot.Model.Type != Type)) throw new ArgumentException( string.Format( CultureInfo.CurrentCulture, "{0} should only include missions for this model type ('{1}')", - nameof(recentMissionsForModelType), + nameof(recentMissionRunsForModelType), Type ), - nameof(recentMissionsForModelType) + nameof(recentMissionRunsForModelType) ); // The time spent on each tasks, not including the duration of video/audio recordings - var timeSpentPerTask = recentMissionsForModelType + var timeSpentPerTask = recentMissionRunsForModelType .SelectMany( - mission => - mission.Tasks + missionRun => + missionRun.Tasks .Where(task => task.EndTime is not null && task.StartTime is not null) .Select( task => diff --git a/backend/api/Database/Models/SortableRecord.cs b/backend/api/Database/Models/SortableRecord.cs new file mode 100644 index 000000000..33127dc6c --- /dev/null +++ b/backend/api/Database/Models/SortableRecord.cs @@ -0,0 +1,8 @@ +#pragma warning disable CS8618 +namespace Api.Database.Models +{ + public interface SortableRecord + { + string Name { get; set; } + } +} diff --git a/backend/api/Database/Models/Source.cs b/backend/api/Database/Models/Source.cs index 08ea025d4..0168c2623 100644 --- a/backend/api/Database/Models/Source.cs +++ b/backend/api/Database/Models/Source.cs @@ -1,4 +1,5 @@ using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; #pragma warning disable CS8618 namespace Api.Database.Models @@ -6,12 +7,11 @@ namespace Api.Database.Models public class Source { [Key] - [Required] - [MaxLength(200)] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public string Id { get; set; } [Required] - public string URL { get; set; } + public string SourceId { get; set; } [Required] public MissionSourceType Type { get; set; } diff --git a/backend/api/EventHandlers/IsarConnectionEventHandler.cs b/backend/api/EventHandlers/IsarConnectionEventHandler.cs index 36165b9ff..a3cd45eea 100644 --- a/backend/api/EventHandlers/IsarConnectionEventHandler.cs +++ b/backend/api/EventHandlers/IsarConnectionEventHandler.cs @@ -18,7 +18,7 @@ public class IsarConnectionEventHandler : EventHandlerBase private IRobotService RobotService => _scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); - private IMissionRunService MissionService => + private IMissionRunService MissionRunService => _scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); private readonly Dictionary _isarConnectionTimers = new(); @@ -119,16 +119,16 @@ private async void OnTimeoutEvent(IsarRobotStatusMessage robotStatusMessage) robot.Status = RobotStatus.Offline; if (robot.CurrentMissionId != null) { - var mission = await MissionService.ReadById(robot.CurrentMissionId); - if (mission != null) + var missionRun = await MissionRunService.ReadById(robot.CurrentMissionId); + if (missionRun != null) { _logger.LogError( "Mission '{missionId}' ('{missionName}') failed due to ISAR timeout", - mission.Id, - mission.Name + missionRun.Id, + missionRun.Name ); - mission.SetToFailed(); - await MissionService.Update(mission); + missionRun.SetToFailed(); + await MissionRunService.Update(missionRun); } } robot.CurrentMissionId = null; diff --git a/backend/api/EventHandlers/MissionScheduler.cs b/backend/api/EventHandlers/MissionScheduler.cs index e2d0488a5..bea7190e1 100644 --- a/backend/api/EventHandlers/MissionScheduler.cs +++ b/backend/api/EventHandlers/MissionScheduler.cs @@ -13,8 +13,8 @@ public class MissionScheduler : BackgroundService private readonly int _timeDelay; private readonly IServiceScopeFactory _scopeFactory; - private IList MissionQueue => - MissionService + private IList MissionRunQueue => + MissionRunService .ReadAll( new MissionRunQueryStringParameters { @@ -25,7 +25,7 @@ public class MissionScheduler : BackgroundService ) .Result; - private IMissionRunService MissionService => + private IMissionRunService MissionRunService => _scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); private RobotController RobotController => @@ -42,17 +42,17 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { - foreach (var queuedMission in MissionQueue) + foreach (var queuedMissionRun in MissionRunQueue) { - var freshMission = MissionService.ReadById(queuedMission.Id).Result; - if (freshMission == null) + var freshMissionRun = MissionRunService.ReadById(queuedMissionRun.Id).Result; + if (freshMissionRun == null) { continue; } if ( - freshMission.Robot.Status is not RobotStatus.Available - || !freshMission.Robot.Enabled - || freshMission.DesiredStartTime > DateTimeOffset.UtcNow + freshMissionRun.Robot.Status is not RobotStatus.Available + || !freshMissionRun.Robot.Enabled + || freshMissionRun.DesiredStartTime > DateTimeOffset.UtcNow ) { continue; @@ -60,31 +60,31 @@ freshMission.Robot.Status is not RobotStatus.Available try { - await StartMission(queuedMission); + await StartMissionRun(queuedMissionRun); } catch (MissionException e) { const MissionStatus NewStatus = MissionStatus.Failed; _logger.LogWarning( - "Mission {id} was not started successfully. Status updated to '{status}'.\nReason: {failReason}", - queuedMission.Id, + "Mission run {id} was not started successfully. Status updated to '{status}'.\nReason: {failReason}", + queuedMissionRun.Id, NewStatus, e.Message ); - queuedMission.Status = NewStatus; - queuedMission.StatusReason = $"Failed to start: '{e.Message}'"; - await MissionService.Update(queuedMission); + queuedMissionRun.Status = NewStatus; + queuedMissionRun.StatusReason = $"Failed to start: '{e.Message}'"; + await MissionRunService.Update(queuedMissionRun); } } await Task.Delay(_timeDelay, stoppingToken); } } - private async Task StartMission(MissionRun queuedMission) + private async Task StartMissionRun(MissionRun queuedMissionRun) { var result = await RobotController.StartMission( - queuedMission.Robot.Id, - queuedMission.Id + queuedMissionRun.Robot.Id, + queuedMissionRun.Id ); if (result.Result is not OkObjectResult) { @@ -93,7 +93,7 @@ private async Task StartMission(MissionRun queuedMission) errorMessage = returnObject.Value?.ToString() ?? errorMessage; throw new MissionException(errorMessage); } - _logger.LogInformation("Started mission '{id}'", queuedMission.Id); + _logger.LogInformation("Started mission run '{id}'", queuedMissionRun.Id); } } } diff --git a/backend/api/EventHandlers/MqttEventHandler.cs b/backend/api/EventHandlers/MqttEventHandler.cs index 4b41e4318..ba1125ff2 100644 --- a/backend/api/EventHandlers/MqttEventHandler.cs +++ b/backend/api/EventHandlers/MqttEventHandler.cs @@ -248,7 +248,7 @@ private static void UpdateCurrentAssetIfChanged(string newCurrentAsset, ref Robo private async void OnMissionUpdate(object? sender, MqttReceivedArgs mqttArgs) { var provider = GetServiceProvider(); - var missionService = provider.GetRequiredService(); + var missionRunService = provider.GetRequiredService(); var robotService = provider.GetRequiredService(); var robotModelService = provider.GetRequiredService(); @@ -268,12 +268,12 @@ private async void OnMissionUpdate(object? sender, MqttReceivedArgs mqttArgs) return; } - var flotillaMission = await missionService.UpdateMissionStatusByIsarMissionId( + var flotillaMissionRun = await missionRunService.UpdateMissionRunStatusByIsarMissionId( isarMission.MissionId, status ); - if (flotillaMission is null) + if (flotillaMissionRun is null) { _logger.LogError( "No mission found with ISARMissionId '{isarMissionId}'. Could not update status to '{status}'", @@ -285,7 +285,7 @@ private async void OnMissionUpdate(object? sender, MqttReceivedArgs mqttArgs) _logger.LogInformation( "Mission '{id}' (ISARMissionID='{isarId}') status updated to '{status}' for robot '{robotName}' with ISAR id '{isarId}'", - flotillaMission.Id, + flotillaMissionRun.Id, isarMission.MissionId, isarMission.Status, isarMission.RobotName, @@ -303,8 +303,8 @@ private async void OnMissionUpdate(object? sender, MqttReceivedArgs mqttArgs) return; } - robot.Status = flotillaMission.IsCompleted ? RobotStatus.Available : RobotStatus.Busy; - if (flotillaMission.IsCompleted) + robot.Status = flotillaMissionRun.IsCompleted ? RobotStatus.Available : RobotStatus.Busy; + if (flotillaMissionRun.IsCompleted) { robot.CurrentMissionId = null; } @@ -317,7 +317,7 @@ private async void OnMissionUpdate(object? sender, MqttReceivedArgs mqttArgs) robot.Status ); - if (flotillaMission.IsCompleted) + if (flotillaMissionRun.IsCompleted) { int timeRangeInDays = _configuration.GetValue( "TimeRangeForMissionDurationEstimationInDays" @@ -325,7 +325,7 @@ private async void OnMissionUpdate(object? sender, MqttReceivedArgs mqttArgs) long minEpochTime = DateTimeOffset.Now .AddDays(-timeRangeInDays) .ToUnixTimeSeconds(); - var missionsForEstimation = await missionService.ReadAll( + var missionRunsForEstimation = await missionRunService.ReadAll( new MissionRunQueryStringParameters { MinDesiredStartTime = minEpochTime, @@ -334,7 +334,7 @@ private async void OnMissionUpdate(object? sender, MqttReceivedArgs mqttArgs) } ); var model = robot.Model; - model.UpdateAverageDurationPerTag(missionsForEstimation); + model.UpdateAverageDurationPerTag(missionRunsForEstimation); await robotModelService.Update(model); @@ -349,7 +349,7 @@ private async void OnMissionUpdate(object? sender, MqttReceivedArgs mqttArgs) private async void OnTaskUpdate(object? sender, MqttReceivedArgs mqttArgs) { var provider = GetServiceProvider(); - var missionService = provider.GetRequiredService(); + var missionRunService = provider.GetRequiredService(); var task = (IsarTaskMessage)mqttArgs.Message; IsarTaskStatus status; try @@ -366,7 +366,7 @@ private async void OnTaskUpdate(object? sender, MqttReceivedArgs mqttArgs) return; } - bool success = await missionService.UpdateTaskStatusByIsarTaskId( + bool success = await missionRunService.UpdateTaskStatusByIsarTaskId( task.MissionId, task.TaskId, status @@ -387,7 +387,7 @@ private async void OnTaskUpdate(object? sender, MqttReceivedArgs mqttArgs) private async void OnStepUpdate(object? sender, MqttReceivedArgs mqttArgs) { var provider = GetServiceProvider(); - var missionService = provider.GetRequiredService(); + var missionRunService = provider.GetRequiredService(); var step = (IsarStepMessage)mqttArgs.Message; @@ -413,7 +413,7 @@ private async void OnStepUpdate(object? sender, MqttReceivedArgs mqttArgs) return; } - bool success = await missionService.UpdateStepStatusByIsarStepId( + bool success = await missionRunService.UpdateStepStatusByIsarStepId( step.MissionId, step.TaskId, step.StepId, diff --git a/backend/api/Options/StorageOptions.cs b/backend/api/Options/StorageOptions.cs index 69657226e..ff2a77f5d 100644 --- a/backend/api/Options/StorageOptions.cs +++ b/backend/api/Options/StorageOptions.cs @@ -3,6 +3,6 @@ public class StorageOptions { public string CustomMissionContainerName { get; set; } = ""; - public string ConnectionString { get; set; } = ""; + public string AccountName { get; set; } = ""; } } diff --git a/backend/api/Program.cs b/backend/api/Program.cs index 771af4de4..a64ab199b 100644 --- a/backend/api/Program.cs +++ b/backend/api/Program.cs @@ -48,9 +48,12 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); -builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddHostedService(); builder.Services.AddHostedService(); diff --git a/backend/api/Services/AreaService.cs b/backend/api/Services/AreaService.cs index 61a0d09b7..7c451f515 100644 --- a/backend/api/Services/AreaService.cs +++ b/backend/api/Services/AreaService.cs @@ -1,6 +1,7 @@ using Api.Controllers.Models; using Api.Database.Context; using Api.Database.Models; +using Api.Utilities; using Microsoft.EntityFrameworkCore; namespace Api.Services @@ -11,9 +12,9 @@ public interface IAreaService public abstract Task ReadById(string id); - public abstract Task> ReadByAsset(string asset); + public abstract Task> ReadByAsset(string assetCode); - public abstract Task ReadByAssetAndName(string assetName, string name); + public abstract Task ReadByAssetAndName(string assetCode, string areaName); public abstract Task Create(CreateAreaQuery newArea); @@ -21,7 +22,7 @@ public interface IAreaService public abstract Task Update(Area area); - public abstract Task AddSafePosition(string asset, string name, SafePosition safePosition); + public abstract Task AddSafePosition(string assetCode, string areaName, SafePosition safePosition); public abstract Task Delete(string id); @@ -40,10 +41,17 @@ public interface IAreaService public class AreaService : IAreaService { private readonly FlotillaDbContext _context; + private readonly IAssetService _assetService; + private readonly IInstallationService _installationService; + private readonly IDeckService _deckService; - public AreaService(FlotillaDbContext context) + public AreaService( + FlotillaDbContext context, IAssetService assetService, IInstallationService installationService, IDeckService deckService) { _context = context; + _assetService = assetService; + _installationService = installationService; + _deckService = deckService; } public async Task> ReadAll() @@ -54,7 +62,7 @@ public async Task> ReadAll() private IQueryable GetAreas() { return _context.Areas.Include(a => a.SafePositions) - .Include(a => a.Deck).ThenInclude(d => d.Installation).ThenInclude(i => i.Asset); + .Include(a => a.Deck).Include(d => d.Installation).Include(i => i.Asset); } public async Task ReadById(string id) @@ -63,141 +71,100 @@ private IQueryable GetAreas() .FirstOrDefaultAsync(a => a.Id.Equals(id)); } - public async Task ReadByAssetAndName(Asset? asset, string name) + public async Task ReadByAssetAndName(Asset? asset, string areaName) { if (asset == null) return null; + return await _context.Areas.Where(a => - a.Name.ToLower().Equals(name.ToLower()) && - a.Asset.Equals(asset) - ).Include(a => a.SafePositions).FirstOrDefaultAsync(); + a.Name.ToLower().Equals(areaName.ToLower()) && + a.Asset.Id.Equals(asset.Id) + ).Include(a => a.SafePositions).Include(a => a.Asset) + .Include(a => a.Installation).Include(a => a.Deck).FirstOrDefaultAsync(); } - public async Task> ReadByAsset(string assetName) + public async Task ReadByAssetAndName(string assetCode, string areaName) { - var asset = await ReadAssetByName(assetName); + var asset = await _assetService.ReadByName(assetCode); if (asset == null) - return new List(); + return null; + return await _context.Areas.Where(a => - a.Asset.Equals(asset)).Include(a => a.SafePositions).ToListAsync(); + a.Asset.Id.Equals(asset.Id) && + a.Name.ToLower().Equals(areaName.ToLower()) + ).Include(a => a.SafePositions).Include(a => a.Asset) + .Include(a => a.Installation).Include(a => a.Deck).FirstOrDefaultAsync(); } - public async Task ReadByAssetAndName(string assetName, string name) + public async Task> ReadByAsset(string assetCode) { - // TODO: can we assume that this combination will be unique? Are area names specific enough? - var asset = await ReadAssetByName(assetName); + var asset = await _assetService.ReadByName(assetCode); if (asset == null) - return null; + return new List(); + return await _context.Areas.Where(a => - a.Asset.Equals(asset) && - a.Name.ToLower().Equals(name.ToLower()) - ).Include(a => a.SafePositions).FirstOrDefaultAsync(); + a.Asset.Id.Equals(asset.Id)).Include(a => a.SafePositions).Include(a => a.Asset) + .Include(a => a.Installation).Include(a => a.Deck).ToListAsync(); } - public async Task ReadAreaByAssetAndInstallationAndDeckAndName(Asset? asset, Installation? installation, Deck? deck, string name) + public async Task ReadByAssetAndInstallationAndDeckAndName(Asset? asset, Installation? installation, Deck? deck, string areaName) { if (asset == null || installation == null || deck == null) return null; + return await _context.Areas.Where(a => - a.Deck.Equals(deck) && - a.Installation.Equals(installation) && - a.Asset.Equals(asset) && - a.Name.ToLower().Equals(name.ToLower()) - ).Include(a => a.Deck).ThenInclude(d => d.Installation).ThenInclude(i => i.Asset) + a.Deck.Id.Equals(deck.Id) && + a.Installation.Id.Equals(installation.Id) && + a.Asset.Id.Equals(asset.Id) && + a.Name.ToLower().Equals(areaName.ToLower()) + ).Include(a => a.Deck).Include(d => d.Installation).Include(i => i.Asset) .Include(a => a.SafePositions).FirstOrDefaultAsync(); } - public async Task ReadDeckByAssetAndInstallationAndName(Asset? asset, Installation? installation, string name) - { - if (asset == null || installation == null) - return null; - return await _context.Decks.Where(a => - a.Installation.Equals(installation) && - a.Asset.Equals(asset) && - a.Name.ToLower().Equals(name.ToLower()) - ).Include(d => d.Installation).Include(i => i.Asset).FirstOrDefaultAsync(); - } - - public async Task ReadInstallationByAssetAndName(Asset? asset, string name) - { - if (asset == null) - return null; - return await _context.Installations.Where(a => - a.Asset.Equals(asset) && - a.Name.ToLower().Equals(name.ToLower()) - ).Include(i => i.Asset).FirstOrDefaultAsync(); - } - - public async Task ReadAssetByName(string asset) + public async Task Create(CreateAreaQuery newAreaQuery, List positions) { - return await _context.Assets.Where(a => - a.ShortName.ToLower().Equals(asset.ToLower()) - ).FirstOrDefaultAsync(); - } - - public async Task Create(CreateAreaQuery newAreaQuery, List safePositions) - { - var sp = new List(); - foreach (var p in safePositions) + var safePositions = new List(); + foreach (var pose in positions) { - sp.Add(new SafePosition(p)); + safePositions.Add(new SafePosition(pose)); } - var asset = await ReadAssetByName(newAreaQuery.AssetCode); + var asset = await _assetService.ReadByName(newAreaQuery.AssetCode); if (asset == null) { - asset = new Asset - { - Name = "", // TODO: - ShortName = newAreaQuery.AssetCode - }; - await _context.Assets.AddAsync(asset); - await _context.SaveChangesAsync(); + throw new AssetNotFoundException($"No asset with name {newAreaQuery.AssetCode} could be found"); } - var installation = await ReadInstallationByAssetAndName(asset, newAreaQuery.InstallationName); + var installation = await _installationService.ReadByAssetAndName(asset, newAreaQuery.InstallationCode); if (installation == null) { - installation = new Installation - { - Asset = asset, - Name = "", // TODO: - ShortName = newAreaQuery.InstallationName - }; - await _context.Installations.AddAsync(installation); - await _context.SaveChangesAsync(); + throw new InstallationNotFoundException($"No installation with name {newAreaQuery.InstallationCode} could be found"); } - var deck = await ReadDeckByAssetAndInstallationAndName(asset, installation, newAreaQuery.DeckName); + var deck = await _deckService.ReadByAssetAndInstallationAndName(asset, installation, newAreaQuery.DeckName); if (deck == null) { - deck = new Deck - { - Installation = installation, - Asset = asset, - Name = newAreaQuery.DeckName - }; - await _context.Decks.AddAsync(deck); - await _context.SaveChangesAsync(); + throw new DeckNotFoundException($"No deck with name {newAreaQuery.DeckName} could be found"); } - var existingArea = await ReadAreaByAssetAndInstallationAndDeckAndName( + var existingArea = await ReadByAssetAndInstallationAndDeckAndName( asset, installation, deck, newAreaQuery.AreaName); if (existingArea != null) { - // TODO: maybe just append safe positions, or return an error + throw new AreaNotFoundException($"No area with name {newAreaQuery.AreaName} could be found"); } var newArea = new Area { Name = newAreaQuery.AreaName, DefaultLocalizationPose = newAreaQuery.DefaultLocalizationPose, - SafePositions = sp, + SafePositions = safePositions, MapMetadata = new MapMetadata(), Deck = deck, Installation = installation, Asset = asset }; + await _context.Areas.AddAsync(newArea); await _context.SaveChangesAsync(); return newArea; @@ -209,13 +176,14 @@ public async Task Create(CreateAreaQuery newArea) return area; } - public async Task AddSafePosition(string asset, string name, SafePosition safePosition) + public async Task AddSafePosition(string assetCode, string areaName, SafePosition safePosition) { - var area = await ReadByAssetAndName(asset, name); + var area = await ReadByAssetAndName(assetCode, areaName); if (area is null) { return null; } + area.SafePositions.Add(safePosition); _context.Areas.Update(area); await _context.SaveChangesAsync(); diff --git a/backend/api/Services/AssetService.cs b/backend/api/Services/AssetService.cs new file mode 100644 index 000000000..8cbd6930e --- /dev/null +++ b/backend/api/Services/AssetService.cs @@ -0,0 +1,107 @@ +using Api.Controllers.Models; +using Api.Database.Context; +using Api.Database.Models; +using Microsoft.EntityFrameworkCore; + +namespace Api.Services +{ + public interface IAssetService + { + public abstract Task> ReadAll(); + + public abstract Task ReadById(string id); + + public abstract Task ReadByName(string asset); + + public abstract Task Create(CreateAssetQuery newAsset); + + public abstract Task Update(Asset asset); + + public abstract Task Delete(string id); + + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage( + "Globalization", + "CA1309:Use ordinal StringComparison", + Justification = "EF Core refrains from translating string comparison overloads to SQL" + )] + [System.Diagnostics.CodeAnalysis.SuppressMessage( + "Globalization", + "CA1304:Specify CultureInfo", + Justification = "Entity framework does not support translating culture info to SQL calls" + )] + public class AssetService : IAssetService + { + private readonly FlotillaDbContext _context; + + public AssetService(FlotillaDbContext context) + { + _context = context; + } + + public async Task> ReadAll() + { + return await GetAssets().ToListAsync(); + } + + private IQueryable GetAssets() + { + return _context.Assets; + } + + public async Task ReadById(string id) + { + return await GetAssets() + .FirstOrDefaultAsync(a => a.Id.Equals(id)); + } + + public async Task ReadByName(string assetCode) + { + if (assetCode == null) + return null; + return await _context.Assets.Where(a => + a.AssetCode.ToLower().Equals(assetCode.ToLower()) + ).FirstOrDefaultAsync(); + } + + public async Task Create(CreateAssetQuery newAssetQuery) + { + var asset = await ReadByName(newAssetQuery.AssetCode); + if (asset == null) + { + asset = new Asset + { + Name = newAssetQuery.Name, + AssetCode = newAssetQuery.AssetCode + }; + await _context.Assets.AddAsync(asset); + await _context.SaveChangesAsync(); + } + + return asset!; + } + + public async Task Update(Asset asset) + { + var entry = _context.Update(asset); + await _context.SaveChangesAsync(); + return entry.Entity; + } + + public async Task Delete(string id) + { + var asset = await GetAssets() + .FirstOrDefaultAsync(ev => ev.Id.Equals(id)); + if (asset is null) + { + return null; + } + + _context.Assets.Remove(asset); + await _context.SaveChangesAsync(); + + return asset; + } + } +} diff --git a/backend/api/Services/CustomMissionService.cs b/backend/api/Services/CustomMissionService.cs new file mode 100644 index 000000000..d3cbc0ea1 --- /dev/null +++ b/backend/api/Services/CustomMissionService.cs @@ -0,0 +1,59 @@ + + +using System.Text.Json; +using Api.Database.Models; +using Api.Options; +using Microsoft.Extensions.Options; + +namespace Api.Services +{ + + public interface ICustomMissionService + { + Task UploadSource(List tasks); + Task?> GetMissionTasksFromMissionId(string id); + } + + public class CustomMissionService : ICustomMissionService + { + private readonly IOptions _storageOptions; + private readonly IBlobService _blobService; + + public CustomMissionService(IOptions storageOptions, IBlobService blobService) + { + _storageOptions = storageOptions; + _blobService = blobService; + } + + public async Task UploadFile(string fileName, Stream fileStream) + { + throw new NotImplementedException(); + } + + public async Task UploadSource(List tasks) + { + string json = JsonSerializer.Serialize(tasks); + string id = Guid.NewGuid().ToString(); + _blobService.UploadJsonToBlob(json, id, _storageOptions.Value.CustomMissionContainerName, _storageOptions.Value.AccountName, false); + + return id; + } + + public async Task?> GetMissionTasksFromMissionId(string id) + { + List? content; + try + { + byte[] rawContent = await _blobService.DownloadBlob(id, _storageOptions.Value.CustomMissionContainerName, _storageOptions.Value.AccountName); + var rawBinaryContent = new BinaryData(rawContent); + content = rawBinaryContent.ToObjectFromJson>(); + } + catch (Exception) + { + return null; + } + + return content; + } + } +} diff --git a/backend/api/Services/DeckService.cs b/backend/api/Services/DeckService.cs new file mode 100644 index 000000000..fe27d1280 --- /dev/null +++ b/backend/api/Services/DeckService.cs @@ -0,0 +1,144 @@ +using Api.Controllers.Models; +using Api.Database.Context; +using Api.Database.Models; +using Api.Utilities; +using Microsoft.EntityFrameworkCore; + +namespace Api.Services +{ + public interface IDeckService + { + public abstract Task> ReadAll(); + + public abstract Task ReadById(string id); + + public abstract Task> ReadByAsset(string assetCode); + + public abstract Task ReadByName(string deckName); + + public abstract Task ReadByAssetAndInstallationAndName(Asset asset, Installation installation, string deckName); + + public abstract Task Create(CreateDeckQuery newDeck); + + public abstract Task Update(Deck deck); + + public abstract Task Delete(string id); + + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage( + "Globalization", + "CA1309:Use ordinal StringComparison", + Justification = "EF Core refrains from translating string comparison overloads to SQL" + )] + [System.Diagnostics.CodeAnalysis.SuppressMessage( + "Globalization", + "CA1304:Specify CultureInfo", + Justification = "Entity framework does not support translating culture info to SQL calls" + )] + public class DeckService : IDeckService + { + private readonly FlotillaDbContext _context; + private readonly IAssetService _assetService; + private readonly IInstallationService _installationService; + + public DeckService(FlotillaDbContext context, IAssetService assetService, IInstallationService installationService) + { + _context = context; + _assetService = assetService; + _installationService = installationService; + } + + public async Task> ReadAll() + { + return await GetDecks().ToListAsync(); + } + + private IQueryable GetDecks() + { + return _context.Decks; + } + + public async Task ReadById(string id) + { + return await GetDecks() + .FirstOrDefaultAsync(a => a.Id.Equals(id)); + } + + public async Task> ReadByAsset(string assetCode) + { + var asset = await _assetService.ReadByName(assetCode); + if (asset == null) + return new List(); + return await _context.Decks.Where(a => + a.Asset.Id.Equals(asset.Id)).ToListAsync(); + } + + public async Task ReadByName(string deckName) + { + if (deckName == null) + return null; + return await _context.Decks.Where(a => + a.Name.ToLower().Equals(deckName.ToLower()) + ).FirstOrDefaultAsync(); + } + + public async Task ReadByAssetAndInstallationAndName(Asset asset, Installation installation, string name) + { + return await _context.Decks.Where(a => + a.Installation.Id.Equals(installation.Id) && + a.Asset.Id.Equals(asset.Id) && + a.Name.ToLower().Equals(name.ToLower()) + ).Include(d => d.Installation).Include(i => i.Asset).FirstOrDefaultAsync(); + } + + public async Task Create(CreateDeckQuery newDeckQuery) + { + var asset = await _assetService.ReadByName(newDeckQuery.AssetCode); + if (asset == null) + { + throw new AssetNotFoundException($"No asset with name {newDeckQuery.AssetCode} could be found"); + } + var installation = await _installationService.ReadByAssetAndName(asset, newDeckQuery.InstallationCode); + if (installation == null) + { + throw new InstallationNotFoundException($"No installation with name {newDeckQuery.InstallationCode} could be found"); + } + var deck = await ReadByAssetAndInstallationAndName(asset, installation, newDeckQuery.Name); + if (deck == null) + { + deck = new Deck + { + Name = newDeckQuery.Name, + Asset = asset, + Installation = installation + }; + await _context.Decks.AddAsync(deck); + await _context.SaveChangesAsync(); + } + return deck!; + } + + public async Task Update(Deck deck) + { + var entry = _context.Update(deck); + await _context.SaveChangesAsync(); + return entry.Entity; + } + + public async Task Delete(string id) + { + var deck = await GetDecks() + .FirstOrDefaultAsync(ev => ev.Id.Equals(id)); + if (deck is null) + { + return null; + } + + _context.Decks.Remove(deck); + await _context.SaveChangesAsync(); + + return deck; + } + } +} diff --git a/backend/api/Services/EchoService.cs b/backend/api/Services/EchoService.cs index 5648be8c7..6530fce71 100644 --- a/backend/api/Services/EchoService.cs +++ b/backend/api/Services/EchoService.cs @@ -15,7 +15,6 @@ public interface IEchoService public abstract Task GetMissionById(int missionId); public abstract Task> GetEchoPlantInfos(); - public abstract Task GetRobotPoseFromPoseId(int poseId); } @@ -79,40 +78,13 @@ public async Task GetMissionById(int missionId) if (echoMission is null) throw new JsonException("Failed to deserialize mission from Echo"); - var mission = ProcessEchoMission(echoMission); - if (mission == null) + var processedEchoMission = ProcessEchoMission(echoMission); + if (processedEchoMission == null) { throw new InvalidDataException($"EchoMission with id: {missionId} is invalid."); } - return mission; - } - - public async Task GetMissionByPath(string relativePath) - { - var response = await _echoApi.CallWebApiForAppAsync( - ServiceName, - options => - { - options.HttpMethod = HttpMethod.Get; - options.RelativePath = relativePath; - } - ); - - response.EnsureSuccessStatusCode(); - - var echoMission = await response.Content.ReadFromJsonAsync(); - - if (echoMission is null) - throw new JsonException("Failed to deserialize mission from Echo"); - - var mission = ProcessEchoMission(echoMission); - if (mission == null) - { - throw new InvalidDataException($"EchoMission with relative path: {relativePath} is invalid."); - } - - return mission; + return processedEchoMission; } public async Task> GetEchoPlantInfos() @@ -207,13 +179,13 @@ private List ProcessAvailableEchoMission(List> ReadAll(); + + public abstract Task ReadById(string id); + + public abstract Task> ReadByAsset(string assetCode); + + public abstract Task ReadByAssetAndName(Asset asset, string installationCode); + + public abstract Task ReadByAssetAndName(string assetCode, string installationCode); + + public abstract Task Create(CreateInstallationQuery newInstallation); + + public abstract Task Update(Installation installation); + + public abstract Task Delete(string id); + + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage( + "Globalization", + "CA1309:Use ordinal StringComparison", + Justification = "EF Core refrains from translating string comparison overloads to SQL" + )] + [System.Diagnostics.CodeAnalysis.SuppressMessage( + "Globalization", + "CA1304:Specify CultureInfo", + Justification = "Entity framework does not support translating culture info to SQL calls" + )] + public class InstallationService : IInstallationService + { + private readonly FlotillaDbContext _context; + private readonly IAssetService _assetService; + + public InstallationService(FlotillaDbContext context, IAssetService assetService) + { + _context = context; + _assetService = assetService; + } + + public async Task> ReadAll() + { + return await GetInstallations().ToListAsync(); + } + + private IQueryable GetInstallations() + { + return _context.Installations.Include(i => i.Asset); + } + + public async Task ReadById(string id) + { + return await GetInstallations() + .FirstOrDefaultAsync(a => a.Id.Equals(id)); + } + + public async Task> ReadByAsset(string assetCode) + { + var asset = await _assetService.ReadByName(assetCode); + if (asset == null) + return new List(); + return await _context.Installations.Where(a => + a.Asset.Id.Equals(asset.Id)).ToListAsync(); + } + + public async Task ReadByAssetAndName(Asset asset, string installationCode) + { + return await _context.Installations.Where(a => + a.InstallationCode.ToLower().Equals(installationCode.ToLower()) && + a.Asset.Id.Equals(asset.Id)).FirstOrDefaultAsync(); + } + + public async Task ReadByAssetAndName(string assetCode, string installationCode) + { + var asset = await _assetService.ReadByName(assetCode); + if (asset == null) + return null; + return await _context.Installations.Where(a => + a.Asset.Id.Equals(asset.Id) && + a.InstallationCode.ToLower().Equals(installationCode.ToLower()) + ).FirstOrDefaultAsync(); + } + + public async Task Create(CreateInstallationQuery newInstallationQuery) + { + var asset = await _assetService.ReadByName(newInstallationQuery.AssetCode); + if (asset == null) + { + throw new AssetNotFoundException($"No asset with name {newInstallationQuery.AssetCode} could be found"); + } + var installation = await ReadByAssetAndName(asset, newInstallationQuery.InstallationCode); + if (installation == null) + { + installation = new Installation + { + Name = newInstallationQuery.Name, + InstallationCode = newInstallationQuery.InstallationCode, + Asset = asset, + }; + await _context.Installations.AddAsync(installation); + await _context.SaveChangesAsync(); + } + return installation!; + } + + public async Task Update(Installation installation) + { + var entry = _context.Update(installation); + await _context.SaveChangesAsync(); + return entry.Entity; + } + + public async Task Delete(string id) + { + var installation = await GetInstallations() + .FirstOrDefaultAsync(ev => ev.Id.Equals(id)); + if (installation is null) + { + return null; + } + + _context.Installations.Remove(installation); + await _context.SaveChangesAsync(); + + return installation; + } + } +} diff --git a/backend/api/Services/IsarService.cs b/backend/api/Services/IsarService.cs index 5c28fbc8b..6efb457d3 100644 --- a/backend/api/Services/IsarService.cs +++ b/backend/api/Services/IsarService.cs @@ -8,7 +8,7 @@ namespace Api.Services { public interface IIsarService { - public abstract Task StartMission(Robot robot, MissionRun mission); + public abstract Task StartMission(Robot robot, MissionRun missionRun); public abstract Task StopMission(Robot robot); @@ -68,13 +68,13 @@ private async Task CallApi( return response; } - public async Task StartMission(Robot robot, MissionRun mission) + public async Task StartMission(Robot robot, MissionRun missionRun) { var response = await CallApi( HttpMethod.Post, robot.IsarUri, "schedule/start-mission", - new { mission_definition = new IsarMissionDefinition(mission) } + new { mission_definition = new IsarMissionDefinition(missionRun) } ); if (!response.IsSuccessStatusCode) diff --git a/backend/api/Services/MapService.cs b/backend/api/Services/MapService.cs index 0977dba10..00dc61f59 100644 --- a/backend/api/Services/MapService.cs +++ b/backend/api/Services/MapService.cs @@ -3,6 +3,7 @@ using Api.Options; using Azure.Storage.Blobs.Models; using Microsoft.Extensions.Options; + namespace Api.Services { public interface IMapService @@ -70,21 +71,21 @@ public async Task FetchMapImage(string mapName, string assetCode) return map; } - public async Task AssignMapToMission(MissionRun mission) + public async Task AssignMapToMission(MissionRun missionRun) { MapMetadata? mapMetadata; var positions = new List(); - foreach (var task in mission.Tasks) + foreach (var task in missionRun.Tasks) { positions.Add(task.InspectionTarget); } try { - mapMetadata = await ChooseMapFromPositions(positions, mission.AssetCode); + mapMetadata = await ChooseMapFromPositions(positions, missionRun.AssetCode); } catch (ArgumentOutOfRangeException) { - _logger.LogWarning("Unable to find a map for mission '{missionId}'", mission.Id); + _logger.LogWarning("Unable to find a map for mission '{missionId}'", missionRun.Id); return; } @@ -93,8 +94,8 @@ public async Task AssignMapToMission(MissionRun mission) return; } - mission.MapMetadata = mapMetadata; - _logger.LogInformation("Assigned map {map} to mission {mission}", mapMetadata.MapName, mission.Name); + missionRun.MapMetadata = mapMetadata; + _logger.LogInformation("Assigned map {map} to mission {mission}", mapMetadata.MapName, missionRun.Name); } private Boundary ExtractMapMetadata(BlobItem map) diff --git a/backend/api/Services/MissionDefinitionService.cs b/backend/api/Services/MissionDefinitionService.cs index b05804c0e..486eb874a 100644 --- a/backend/api/Services/MissionDefinitionService.cs +++ b/backend/api/Services/MissionDefinitionService.cs @@ -12,13 +12,13 @@ namespace Api.Services { public interface IMissionDefinitionService { - public abstract Task Create(MissionDefinition mission); + public abstract Task Create(MissionDefinition missionDefinition); public abstract Task ReadById(string id); public abstract Task> ReadAll(MissionDefinitionQueryStringParameters parameters); - public abstract Task Update(MissionDefinition mission); + public abstract Task Update(MissionDefinition missionDefinition); public abstract Task Delete(string id); } @@ -47,35 +47,34 @@ public MissionDefinitionService(FlotillaDbContext context) _context = context; } - public async Task Create(MissionDefinition mission) + public async Task Create(MissionDefinition missionDefinition) { - await _context.MissionDefinitions.AddAsync(mission); + await _context.MissionDefinitions.AddAsync(missionDefinition); await _context.SaveChangesAsync(); - return mission; + return missionDefinition; } - private IQueryable GetMissionsWithSubModels() + private IQueryable GetMissionDefinitionsWithSubModels() { return _context.MissionDefinitions - .Include(mission => mission.Area) - .ThenInclude(robot => robot.Deck) - .ThenInclude(robot => robot.Installation) - .ThenInclude(robot => robot.Asset) - .Include(mission => mission.Source) - .Include(mission => mission.LastRun) - .ThenInclude(planTask => planTask == null ? null : planTask.StartTime); + .Include(missionDefinition => missionDefinition.Area) + .ThenInclude(area => area.Deck) + .ThenInclude(area => area.Installation) + .ThenInclude(area => area.Asset) + .Include(missionDefinition => missionDefinition.Source) + .Include(missionDefinition => missionDefinition.LastRun); } public async Task ReadById(string id) { - return await GetMissionsWithSubModels() - .FirstOrDefaultAsync(mission => mission.Id.Equals(id)); + return await GetMissionDefinitionsWithSubModels().Where(m => m.Deprecated == false) + .FirstOrDefaultAsync(missionDefinition => missionDefinition.Id.Equals(id)); } public async Task> ReadAll(MissionDefinitionQueryStringParameters parameters) { - var query = GetMissionsWithSubModels(); + var query = GetMissionDefinitionsWithSubModels().Where(m => m.Deprecated == false); var filter = ConstructFilter(parameters); query = query.Where(filter); @@ -91,9 +90,9 @@ public async Task> ReadAll(MissionDefinitionQuerySt ); } - public async Task Update(MissionDefinition mission) + public async Task Update(MissionDefinition missionDefinition) { - var entry = _context.Update(mission); + var entry = _context.Update(missionDefinition); await _context.SaveChangesAsync(); return entry.Entity; } @@ -101,26 +100,26 @@ public async Task Update(MissionDefinition mission) public async Task Delete(string id) { // We do not delete the source here as more than one mission definition may be using it - var mission = await ReadById(id); - if (mission is null) + var missionDefinition = await ReadById(id); + if (missionDefinition is null) { return null; } - _context.MissionDefinitions.Remove(mission); + missionDefinition.Deprecated = true; await _context.SaveChangesAsync(); - return mission; + return missionDefinition; } - private static void SearchByName(ref IQueryable missions, string? name) + private static void SearchByName(ref IQueryable missionDefinitions, string? name) { - if (!missions.Any() || string.IsNullOrWhiteSpace(name)) + if (!missionDefinitions.Any() || string.IsNullOrWhiteSpace(name)) return; - missions = missions.Where( - mission => - mission.Name != null && mission.Name.ToLower().Contains(name.Trim().ToLower()) + missionDefinitions = missionDefinitions.Where( + missionDefinition => + missionDefinition.Name != null && missionDefinition.Name.ToLower().Contains(name.Trim().ToLower()) ); } @@ -135,44 +134,44 @@ MissionDefinitionQueryStringParameters parameters ) { Expression> areaFilter = parameters.Area is null - ? mission => true - : mission => - mission.Area.Name.ToLower().Equals(parameters.Area.Trim().ToLower()); + ? missionDefinition => true + : missionDefinition => + missionDefinition.Area.Name.ToLower().Equals(parameters.Area.Trim().ToLower()); Expression> assetFilter = parameters.AssetCode is null - ? mission => true - : mission => - mission.AssetCode.ToLower().Equals(parameters.AssetCode.Trim().ToLower()); + ? missionDefinition => true + : missionDefinition => + missionDefinition.AssetCode.ToLower().Equals(parameters.AssetCode.Trim().ToLower()); Expression> missionTypeFilter = parameters.SourceType is null - ? mission => true - : mission => - mission.Source.Type.Equals(parameters.SourceType); + ? missionDefinition => true + : missionDefinition => + missionDefinition.Source.Type.Equals(parameters.SourceType); // The parameter of the filter expression - var mission = Expression.Parameter(typeof(MissionRun)); + var missionRunExpression = Expression.Parameter(typeof(MissionRun)); // Combining the body of the filters to create the combined filter, using invoke to force parameter substitution Expression body = Expression.AndAlso( - Expression.Invoke(assetFilter, mission), + Expression.Invoke(assetFilter, missionRunExpression), Expression.AndAlso( - Expression.Invoke(areaFilter, mission), - Expression.Invoke(missionTypeFilter, mission) + Expression.Invoke(areaFilter, missionRunExpression), + Expression.Invoke(missionTypeFilter, missionRunExpression) ) ); // Constructing the resulting lambda expression by combining parameter and body - return Expression.Lambda>(body, mission); + return Expression.Lambda>(body, missionRunExpression); } - private static void ApplySort(ref IQueryable missions, string orderByQueryString) + private static void ApplySort(ref IQueryable missionDefinitions, string orderByQueryString) { - if (!missions.Any()) + if (!missionDefinitions.Any()) return; if (string.IsNullOrWhiteSpace(orderByQueryString)) { - missions = missions.OrderBy(x => x.Name); + missionDefinitions = missionDefinitions.OrderBy(x => x.Name); return; } @@ -216,9 +215,9 @@ private static void ApplySort(ref IQueryable missions, string string orderQuery = orderQueryBuilder.ToString().TrimEnd(',', ' '); - missions = string.IsNullOrWhiteSpace(orderQuery) - ? missions.OrderBy(mission => mission.Name) - : missions.OrderBy(orderQuery); + missionDefinitions = string.IsNullOrWhiteSpace(orderQuery) + ? missionDefinitions.OrderBy(missionDefinition => missionDefinition.Name) + : missionDefinitions.OrderBy(orderQuery); } } } diff --git a/backend/api/Services/MissionRunService.cs b/backend/api/Services/MissionRunService.cs index a5efe1b54..ce1d950ae 100644 --- a/backend/api/Services/MissionRunService.cs +++ b/backend/api/Services/MissionRunService.cs @@ -14,7 +14,7 @@ namespace Api.Services { public interface IMissionRunService { - public abstract Task Create(MissionRun mission); + public abstract Task Create(MissionRun missionRun); public abstract Task> ReadAll(MissionRunQueryStringParameters parameters); @@ -22,7 +22,7 @@ public interface IMissionRunService public abstract Task Update(MissionRun mission); - public abstract Task UpdateMissionStatusByIsarMissionId( + public abstract Task UpdateMissionRunStatusByIsarMissionId( string isarMissionId, MissionStatus missionStatus ); @@ -69,34 +69,34 @@ public MissionRunService(FlotillaDbContext context, ILogger l _logger = logger; } - private IQueryable GetMissionsWithSubModels() + private IQueryable GetMissionRunsWithSubModels() { return _context.MissionRuns - .Include(mission => mission.Area) - .ThenInclude(a => a.Deck) - .ThenInclude(d => d.Installation) - .ThenInclude(i => i.Asset) - .Include(mission => mission.Robot) + .Include(missionRun => missionRun.Area) + .ThenInclude(area => area.Deck) + .ThenInclude(deck => deck.Installation) + .ThenInclude(installation => installation.Asset) + .Include(missionRun => missionRun.Robot) .ThenInclude(robot => robot.VideoStreams) - .Include(mission => mission.Robot) + .Include(missionRun => missionRun.Robot) .ThenInclude(robot => robot.Model) - .Include(mission => mission.Tasks) + .Include(missionRun => missionRun.Tasks) .ThenInclude(planTask => planTask.Inspections) - .Include(mission => mission.Tasks) + .Include(missionRun => missionRun.Tasks) .ThenInclude(task => task.Inspections); } - public async Task Create(MissionRun mission) + public async Task Create(MissionRun missionRun) { - await _context.MissionRuns.AddAsync(mission); + await _context.MissionRuns.AddAsync(missionRun); await _context.SaveChangesAsync(); - return mission; + return missionRun; } public async Task> ReadAll(MissionRunQueryStringParameters parameters) { - var query = GetMissionsWithSubModels(); + var query = GetMissionRunsWithSubModels(); var filter = ConstructFilter(parameters); query = query.Where(filter); @@ -116,50 +116,50 @@ public async Task> ReadAll(MissionRunQueryStringParameters public async Task ReadById(string id) { - return await GetMissionsWithSubModels() - .FirstOrDefaultAsync(mission => mission.Id.Equals(id)); + return await GetMissionRunsWithSubModels() + .FirstOrDefaultAsync(missionRun => missionRun.Id.Equals(id)); } - public async Task Update(MissionRun mission) + public async Task Update(MissionRun missionRun) { - var entry = _context.Update(mission); + var entry = _context.Update(missionRun); await _context.SaveChangesAsync(); return entry.Entity; } public async Task Delete(string id) { - var mission = await GetMissionsWithSubModels() + var missionRun = await GetMissionRunsWithSubModels() .FirstOrDefaultAsync(ev => ev.Id.Equals(id)); - if (mission is null) + if (missionRun is null) { return null; } - _context.MissionRuns.Remove(mission); + _context.MissionRuns.Remove(missionRun); await _context.SaveChangesAsync(); - return mission; + return missionRun; } #region ISAR Specific methods private async Task ReadByIsarMissionId(string isarMissionId) { - return await GetMissionsWithSubModels() + return await GetMissionRunsWithSubModels() .FirstOrDefaultAsync( - mission => - mission.IsarMissionId != null && mission.IsarMissionId.Equals(isarMissionId) + missionRun => + missionRun.IsarMissionId != null && missionRun.IsarMissionId.Equals(isarMissionId) ); } - public async Task UpdateMissionStatusByIsarMissionId( + public async Task UpdateMissionRunStatusByIsarMissionId( string isarMissionId, MissionStatus missionStatus ) { - var mission = await ReadByIsarMissionId(isarMissionId); - if (mission is null) + var missionRun = await ReadByIsarMissionId(isarMissionId); + if (missionRun is null) { _logger.LogWarning( "Could not update mission status for ISAR mission with id: {id} as the mission was not found", @@ -168,11 +168,11 @@ MissionStatus missionStatus return null; } - mission.Status = missionStatus; + missionRun.Status = missionStatus; await _context.SaveChangesAsync(); - return mission; + return missionRun; } public async Task UpdateTaskStatusByIsarTaskId( @@ -181,18 +181,18 @@ public async Task UpdateTaskStatusByIsarTaskId( IsarTaskStatus taskStatus ) { - var mission = await ReadByIsarMissionId(isarMissionId); - if (mission is null) + var missionRun = await ReadByIsarMissionId(isarMissionId); + if (missionRun is null) { _logger.LogWarning( - "Could not update task status for ISAR task with id: {id} in mission with id: {missionId} as the mission was not found", + "Could not update task status for ISAR task with id: {id} in mission run with id: {missionId} as the mission was not found", isarTaskId, isarMissionId ); return false; } - var task = mission.GetTaskByIsarId(isarTaskId); + var task = missionRun.GetTaskByIsarId(isarTaskId); if (task is null) { _logger.LogWarning( @@ -203,12 +203,12 @@ IsarTaskStatus taskStatus } task.UpdateStatus(taskStatus); - if (taskStatus == IsarTaskStatus.InProgress && mission.Status != MissionStatus.Ongoing) + if (taskStatus == IsarTaskStatus.InProgress && missionRun.Status != MissionStatus.Ongoing) { // If mission was set to failed and then ISAR recovered connection, we need to reset the coming tasks - mission.Status = MissionStatus.Ongoing; + missionRun.Status = MissionStatus.Ongoing; foreach ( - var taskItem in mission.Tasks.Where( + var taskItem in missionRun.Tasks.Where( taskItem => taskItem.TaskOrder > task.TaskOrder ) ) @@ -233,8 +233,8 @@ public async Task UpdateStepStatusByIsarStepId( IsarStepStatus stepStatus ) { - var mission = await ReadByIsarMissionId(isarMissionId); - if (mission is null) + var missionRun = await ReadByIsarMissionId(isarMissionId); + if (missionRun is null) { _logger.LogWarning( "Could not update step status for ISAR inspection with id: {id} in mission with id: {missionId} as the mission was not found", @@ -244,7 +244,7 @@ IsarStepStatus stepStatus return false; } - var task = mission.GetTaskByIsarId(isarTaskId); + var task = missionRun.GetTaskByIsarId(isarTaskId); if (task is null) { _logger.LogWarning( @@ -274,35 +274,35 @@ IsarStepStatus stepStatus #endregion ISAR Specific methods - private static void SearchByName(ref IQueryable missions, string? name) + private static void SearchByName(ref IQueryable missionRuns, string? name) { - if (!missions.Any() || string.IsNullOrWhiteSpace(name)) + if (!missionRuns.Any() || string.IsNullOrWhiteSpace(name)) return; - missions = missions.Where( - mission => - mission.Name != null && mission.Name.ToLower().Contains(name.Trim().ToLower()) + missionRuns = missionRuns.Where( + missionRun => + missionRun.Name != null && missionRun.Name.ToLower().Contains(name.Trim().ToLower()) ); } - private static void SearchByRobotName(ref IQueryable missions, string? robotName) + private static void SearchByRobotName(ref IQueryable missionRuns, string? robotName) { - if (!missions.Any() || string.IsNullOrWhiteSpace(robotName)) + if (!missionRuns.Any() || string.IsNullOrWhiteSpace(robotName)) return; - missions = missions.Where( - mission => mission.Robot.Name.ToLower().Contains(robotName.Trim().ToLower()) + missionRuns = missionRuns.Where( + missionRun => missionRun.Robot.Name.ToLower().Contains(robotName.Trim().ToLower()) ); } - private static void SearchByTag(ref IQueryable missions, string? tag) + private static void SearchByTag(ref IQueryable missionRuns, string? tag) { - if (!missions.Any() || string.IsNullOrWhiteSpace(tag)) + if (!missionRuns.Any() || string.IsNullOrWhiteSpace(tag)) return; - missions = missions.Where( - mission => - mission.Tasks.Any( + missionRuns = missionRuns.Where( + missionRun => + missionRun.Tasks.Any( task => task.TagId != null && task.TagId.ToLower().Contains(tag.Trim().ToLower()) @@ -321,26 +321,26 @@ MissionRunQueryStringParameters parameters ) { Expression> areaFilter = parameters.Area is null - ? mission => true - : mission => - mission.Area.Name.ToLower().Equals(parameters.Area.Trim().ToLower()); + ? missionRun => true + : missionRun => + missionRun.Area.Name.ToLower().Equals(parameters.Area.Trim().ToLower()); Expression> assetFilter = parameters.AssetCode is null - ? mission => true - : mission => - mission.AssetCode.ToLower().Equals(parameters.AssetCode.Trim().ToLower()); + ? missionRun => true + : missionRun => + missionRun.AssetCode.ToLower().Equals(parameters.AssetCode.Trim().ToLower()); Expression> statusFilter = parameters.Statuses is null ? mission => true : mission => parameters.Statuses.Contains(mission.Status); Expression> robotTypeFilter = parameters.RobotModelType is null - ? mission => true - : mission => mission.Robot.Model.Type.Equals(parameters.RobotModelType); + ? missionRun => true + : missionRun => missionRun.Robot.Model.Type.Equals(parameters.RobotModelType); Expression> robotIdFilter = parameters.RobotId is null - ? mission => true - : mission => mission.Robot.Id.Equals(parameters.RobotId); + ? missionRun => true + : missionRun => missionRun.Robot.Id.Equals(parameters.RobotId); Expression> inspectionTypeFilter = parameters.InspectionTypes is null ? mission => true @@ -353,20 +353,20 @@ MissionRunQueryStringParameters parameters var minStartTime = DateTimeOffset.FromUnixTimeSeconds(parameters.MinStartTime); var maxStartTime = DateTimeOffset.FromUnixTimeSeconds(parameters.MaxStartTime); - Expression> startTimeFilter = mission => - mission.StartTime == null + Expression> startTimeFilter = missionRun => + missionRun.StartTime == null || ( - DateTimeOffset.Compare(mission.StartTime.Value, minStartTime) >= 0 - && DateTimeOffset.Compare(mission.StartTime.Value, maxStartTime) <= 0 + DateTimeOffset.Compare(missionRun.StartTime.Value, minStartTime) >= 0 + && DateTimeOffset.Compare(missionRun.StartTime.Value, maxStartTime) <= 0 ); var minEndTime = DateTimeOffset.FromUnixTimeSeconds(parameters.MinEndTime); var maxEndTime = DateTimeOffset.FromUnixTimeSeconds(parameters.MaxEndTime); - Expression> endTimeFilter = mission => - mission.EndTime == null + Expression> endTimeFilter = missionRun => + missionRun.EndTime == null || ( - DateTimeOffset.Compare(mission.EndTime.Value, minEndTime) >= 0 - && DateTimeOffset.Compare(mission.EndTime.Value, maxEndTime) <= 0 + DateTimeOffset.Compare(missionRun.EndTime.Value, minEndTime) >= 0 + && DateTimeOffset.Compare(missionRun.EndTime.Value, maxEndTime) <= 0 ); var minDesiredStartTime = DateTimeOffset.FromUnixTimeSeconds( @@ -375,29 +375,29 @@ MissionRunQueryStringParameters parameters var maxDesiredStartTime = DateTimeOffset.FromUnixTimeSeconds( parameters.MaxDesiredStartTime ); - Expression> desiredStartTimeFilter = mission => - DateTimeOffset.Compare(mission.DesiredStartTime, minDesiredStartTime) >= 0 - && DateTimeOffset.Compare(mission.DesiredStartTime, maxDesiredStartTime) <= 0; + Expression> desiredStartTimeFilter = missionRun => + DateTimeOffset.Compare(missionRun.DesiredStartTime, minDesiredStartTime) >= 0 + && DateTimeOffset.Compare(missionRun.DesiredStartTime, maxDesiredStartTime) <= 0; // The parameter of the filter expression - var mission = Expression.Parameter(typeof(MissionRun)); + var missionRun = Expression.Parameter(typeof(MissionRun)); // Combining the body of the filters to create the combined filter, using invoke to force parameter substitution Expression body = Expression.AndAlso( - Expression.Invoke(assetFilter, mission), + Expression.Invoke(assetFilter, missionRun), Expression.AndAlso( - Expression.Invoke(statusFilter, mission), + Expression.Invoke(statusFilter, missionRun), Expression.AndAlso( - Expression.Invoke(robotIdFilter, mission), + Expression.Invoke(robotIdFilter, missionRun), Expression.AndAlso( - Expression.Invoke(inspectionTypeFilter, mission), + Expression.Invoke(inspectionTypeFilter, missionRun), Expression.AndAlso( - Expression.Invoke(desiredStartTimeFilter, mission), + Expression.Invoke(desiredStartTimeFilter, missionRun), Expression.AndAlso( - Expression.Invoke(startTimeFilter, mission), + Expression.Invoke(startTimeFilter, missionRun), Expression.AndAlso( - Expression.Invoke(endTimeFilter, mission), - Expression.Invoke(robotTypeFilter, mission) + Expression.Invoke(endTimeFilter, missionRun), + Expression.Invoke(robotTypeFilter, missionRun) ) ) ) @@ -407,17 +407,17 @@ MissionRunQueryStringParameters parameters ); // Constructing the resulting lambda expression by combining parameter and body - return Expression.Lambda>(body, mission); + return Expression.Lambda>(body, missionRun); } - private static void ApplySort(ref IQueryable missions, string orderByQueryString) + private static void ApplySort(ref IQueryable missionRuns, string orderByQueryString) { - if (!missions.Any()) + if (!missionRuns.Any()) return; if (string.IsNullOrWhiteSpace(orderByQueryString)) { - missions = missions.OrderBy(x => x.Name); + missionRuns = missionRuns.OrderBy(x => x.Name); return; } @@ -448,7 +448,7 @@ private static void ApplySort(ref IQueryable missions, string orderB if (objectProperty == null) throw new InvalidDataException( - $"Mission has no property '{propertyFromQueryName}' for ordering" + $"MissionRun has no property '{propertyFromQueryName}' for ordering" ); string sortingOrder = param.EndsWith(" desc", StringComparison.OrdinalIgnoreCase) @@ -461,9 +461,9 @@ private static void ApplySort(ref IQueryable missions, string orderB string orderQuery = orderQueryBuilder.ToString().TrimEnd(',', ' '); - missions = string.IsNullOrWhiteSpace(orderQuery) - ? missions.OrderBy(mission => mission.Name) - : missions.OrderBy(orderQuery); + missionRuns = string.IsNullOrWhiteSpace(orderQuery) + ? missionRuns.OrderBy(missionRun => missionRun.Name) + : missionRuns.OrderBy(orderQuery); } } } diff --git a/backend/api/Services/Models/IsarMissionDefinition.cs b/backend/api/Services/Models/IsarMissionDefinition.cs index d230f72e7..3d3232798 100644 --- a/backend/api/Services/Models/IsarMissionDefinition.cs +++ b/backend/api/Services/Models/IsarMissionDefinition.cs @@ -25,11 +25,11 @@ public IsarMissionDefinition(List tasks) Tasks = tasks; } - public IsarMissionDefinition(MissionRun mission) + public IsarMissionDefinition(MissionRun missionRun) { - Id = mission.IsarMissionId; - Name = mission.Name; - Tasks = mission.Tasks.Select(task => new IsarTaskDefinition(task, mission)).ToList(); + Id = missionRun.IsarMissionId; + Name = missionRun.Name; + Tasks = missionRun.Tasks.Select(task => new IsarTaskDefinition(task, missionRun)).ToList(); } } @@ -47,7 +47,7 @@ public struct IsarTaskDefinition [JsonPropertyName("inspections")] public List Inspections { get; set; } - public IsarTaskDefinition(MissionTask missionTask, MissionRun mission) + public IsarTaskDefinition(MissionTask missionTask, MissionRun missionRun) { Id = missionTask.IsarTaskId; Pose = new IsarPose(missionTask.RobotPose); @@ -55,7 +55,7 @@ public IsarTaskDefinition(MissionTask missionTask, MissionRun mission) var isarInspections = new List(); foreach (var inspection in missionTask.Inspections) { - isarInspections.Add(new IsarInspectionDefinition(inspection, missionTask, mission)); + isarInspections.Add(new IsarInspectionDefinition(inspection, missionTask, missionRun)); } Inspections = isarInspections; } @@ -81,7 +81,7 @@ public struct IsarInspectionDefinition [JsonPropertyName("metadata")] public Dictionary? Metadata { get; set; } - public IsarInspectionDefinition(Inspection inspection, MissionTask task, MissionRun mission) + public IsarInspectionDefinition(Inspection inspection, MissionTask task, MissionRun missionRun) { Id = inspection.IsarStepId; Type = inspection.InspectionType.ToString(); @@ -95,12 +95,12 @@ public IsarInspectionDefinition(Inspection inspection, MissionTask task, Mission Duration = inspection.VideoDuration; var metadata = new Dictionary { - { "map", mission.MapMetadata?.MapName }, - { "description", mission.Description }, - { "estimated_duration", mission.EstimatedDuration.ToString() }, - { "asset_code", mission.AssetCode }, - { "mission_name", mission.Name }, - { "status_reason", mission.StatusReason } + { "map", missionRun.MapMetadata?.MapName }, + { "description", missionRun.Description }, + { "estimated_duration", missionRun.EstimatedDuration.ToString() }, + { "asset_code", missionRun.AssetCode }, + { "mission_name", missionRun.Name }, + { "status_reason", missionRun.StatusReason } }; Metadata = metadata; } diff --git a/backend/api/Services/SortingService.cs b/backend/api/Services/SortingService.cs new file mode 100644 index 000000000..041d54e95 --- /dev/null +++ b/backend/api/Services/SortingService.cs @@ -0,0 +1,65 @@ +using System.Linq.Dynamic.Core; +using System.Reflection; +using System.Text; +using Api.Database.Models; + +namespace Api.Services +{ + public class SortingService + { + public static void ApplySort(ref IQueryable missions, string orderByQueryString) where T : SortableRecord + { + if (!missions.Any()) + return; + + if (string.IsNullOrWhiteSpace(orderByQueryString)) + { + missions = missions.OrderBy(x => x.Name); + return; + } + + string[] orderParams = orderByQueryString + .Trim() + .Split(',') + .Select(parameterString => parameterString.Trim()) + .ToArray(); + + var propertyInfos = typeof(T).GetProperties( + BindingFlags.Public | BindingFlags.Instance + ); + var orderQueryBuilder = new StringBuilder(); + + foreach (string param in orderParams) + { + if (string.IsNullOrWhiteSpace(param)) + continue; + + string propertyFromQueryName = param.Split(" ")[0]; + var objectProperty = propertyInfos.FirstOrDefault( + pi => + pi.Name.Equals( + propertyFromQueryName, + StringComparison.Ordinal) + ); + + if (objectProperty == null) + throw new InvalidDataException( + $"Mission has no property '{propertyFromQueryName}' for ordering" + ); + + string sortingOrder = param.EndsWith(" desc", StringComparison.OrdinalIgnoreCase) + ? "descending" + : "ascending"; + + string sortParameter = $"{objectProperty.Name} {sortingOrder}, "; + orderQueryBuilder.Append(sortParameter); + } + + string orderQuery = orderQueryBuilder.ToString().TrimEnd(',', ' '); + + missions = string.IsNullOrWhiteSpace(orderQuery) + ? missions.OrderBy(mission => mission.Name) + : missions.OrderBy(orderQuery); + } + } +} diff --git a/backend/api/Services/SourceService.cs b/backend/api/Services/SourceService.cs deleted file mode 100644 index ef158c85b..000000000 --- a/backend/api/Services/SourceService.cs +++ /dev/null @@ -1,96 +0,0 @@ - - -using System.Text.Json; -using Api.Database.Models; -using Api.Options; -using Azure.Storage.Blobs; -using Microsoft.Extensions.Options; - -namespace Api.Services -{ - - public interface ISourceService - { - Task UploadSource(string id, List tasks); - List? GetMissionTasksFromMissionId(string id); - List? GetMissionTasksFromURL(string url); - } - - public class SourceService : ISourceService - { - private readonly IOptions _storageOptions; - - public SourceService(IOptions storageOptions) - { - _storageOptions = storageOptions; - } - - public async Task CreateContainer(string containerName) - { - var blobServiceClient = new BlobServiceClient(_storageOptions.Value.ConnectionString); - var containerClient = blobServiceClient.GetBlobContainerClient(containerName); - await containerClient.CreateIfNotExistsAsync(); - - return true; - } - - public async Task UploadFile(string fileName, Stream fileStream) - { - var blobServiceClient = new BlobServiceClient(_storageOptions.Value.ConnectionString); - var containerClient = blobServiceClient.GetBlobContainerClient(_storageOptions.Value.CustomMissionContainerName); - containerClient.CreateIfNotExists(); - - var blobClient = containerClient.GetBlobClient(fileName); - _ = await blobClient.UploadAsync(fileStream, true); - //var hash = $"0x{BitConverter.ToString(blobProperties.Value.ContentHash).Replace("-", string.Empty)}"; - return blobClient.Uri; - } - - public Task UploadSource(string id, List tasks) - { - var memoryStream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(JsonSerializer.Serialize(tasks))); - - var taskUri = UploadFile(id, memoryStream); - - return taskUri; - } - - public List? GetMissionTasksFromMissionId(string id) - { - var blobServiceClient = new BlobServiceClient(_storageOptions.Value.ConnectionString); - var containerClient = blobServiceClient.GetBlobContainerClient(_storageOptions.Value.CustomMissionContainerName); - containerClient.CreateIfNotExists(); - - var blobClient = containerClient.GetBlobClient(id); - - List? content; - try - { - content = blobClient.DownloadContent().Value.Content.ToObjectFromJson>(); - } - catch (Exception) - { - return null; - } - - return content; - } - - public List? GetMissionTasksFromURL(string url) - { - var blobClient = new BlobClient(new Uri(url)); - - List? content; - try - { - content = blobClient.DownloadContent().Value.Content.ToObjectFromJson>(); - } - catch (Exception) - { - return null; - } - - return content; - } - } -} diff --git a/backend/api/Utilities/Exceptions.cs b/backend/api/Utilities/Exceptions.cs index 09a053d17..731b9bf15 100644 --- a/backend/api/Utilities/Exceptions.cs +++ b/backend/api/Utilities/Exceptions.cs @@ -11,6 +11,31 @@ public MissionException(string message, int isarStatusCode) : base(message) } } + public class MissionSourceTypeException : Exception + { + public MissionSourceTypeException(string message) : base(message) { } + } + + public class AssetNotFoundException : Exception + { + public AssetNotFoundException(string message) : base(message) { } + } + + public class InstallationNotFoundException : Exception + { + public InstallationNotFoundException(string message) : base(message) { } + } + + public class DeckNotFoundException : Exception + { + public DeckNotFoundException(string message) : base(message) { } + } + + public class AreaNotFoundException : Exception + { + public AreaNotFoundException(string message) : base(message) { } + } + public class MissionNotFoundException : Exception { public MissionNotFoundException(string message) : base(message) { } diff --git a/backend/api/appsettings.Development.json b/backend/api/appsettings.Development.json index 47f239155..79ba4ee57 100644 --- a/backend/api/appsettings.Development.json +++ b/backend/api/appsettings.Development.json @@ -37,7 +37,7 @@ }, "Blob": { "CustomMissionContainerName": "custommission", - "ConnectionString": "" + "AccountName": "" }, "Database": { "UseInMemoryDatabase": true diff --git a/backend/api/appsettings.Production.json b/backend/api/appsettings.Production.json index 18688b217..06675d568 100644 --- a/backend/api/appsettings.Production.json +++ b/backend/api/appsettings.Production.json @@ -17,7 +17,7 @@ "https://*.equinor.com/" ], "Blob": { - "CustomMissionContainerName": "", + "CustomMissionContainerName": "custommission", "AccountName": "" }, "Mqtt": { diff --git a/backend/api/appsettings.Staging.json b/backend/api/appsettings.Staging.json index cf8e20187..4bb33df4f 100644 --- a/backend/api/appsettings.Staging.json +++ b/backend/api/appsettings.Staging.json @@ -19,7 +19,7 @@ "https://localhost:3001" ], "Blob": { - "CustomMissionContainerName": "", + "CustomMissionContainerName": "custommission", "AccountName": "" }, "Mqtt": { diff --git a/backend/api/appsettings.Test.json b/backend/api/appsettings.Test.json index 1fb3dab01..ca2dd4e60 100644 --- a/backend/api/appsettings.Test.json +++ b/backend/api/appsettings.Test.json @@ -12,7 +12,7 @@ "https://localhost:3001" ], "Blob": { - "CustomMissionContainerName": "", + "CustomMissionContainerName": "custommission", "AccountName": "" }, "Mqtt": { diff --git a/frontend/src/api/ApiCaller.tsx b/frontend/src/api/ApiCaller.tsx index 330c5bc1d..8f308229e 100644 --- a/frontend/src/api/ApiCaller.tsx +++ b/frontend/src/api/ApiCaller.tsx @@ -8,7 +8,7 @@ import { MissionRunQueryParameters } from 'models/MissionRunQueryParameters' import { MissionDefinitionQueryParameters, SourceType } from 'models/MissionDefinitionQueryParameters' import { PaginatedResponse, PaginationHeader, PaginationHeaderName } from 'models/PaginatedResponse' import { Pose } from 'models/Pose' -import { AssetDeck } from 'models/AssetDeck' +import { Area } from 'models/Area' import { timeout } from 'utils/timeout' import { tokenReverificationInterval } from 'components/Contexts/AuthProvider' import { TaskStatus } from 'models/Task' @@ -226,8 +226,8 @@ export class BackendAPICaller { return result.content } - static async getMissionById(missionId: string): Promise { - const path: string = 'missions/' + missionId + static async getMissionRunById(missionId: string): Promise { + const path: string = 'missions/runs/' + missionId const result = await BackendAPICaller.GET(path).catch((e) => { console.error(`Failed to GET /${path}: ` + e) throw e @@ -261,7 +261,7 @@ export class BackendAPICaller { echoMissionId: echoMissionId, desiredStartTime: new Date(), assetCode: assetCode, - areaName: '', + areaName: '', // TODO: we need a way of populating the area database, then including area in MissionDefinition } const result = await BackendAPICaller.POST(path, body).catch((e) => { console.error(`Failed to POST /${path}: ` + e) @@ -341,17 +341,17 @@ export class BackendAPICaller { } } - static async getAssetDecks(): Promise { - const path: string = 'asset-decks' - const result = await this.GET(path).catch((e) => { + static async getAreas(): Promise { + const path: string = 'areas' + const result = await this.GET(path).catch((e) => { console.error(`Failed to GET /${path}: ` + e) throw e }) return result.content } - static async getAssetDeckMapMetadata(id: string): Promise { - const path: string = 'asset-decks/' + id + '/map-metadata' + static async getAreasMapMetadata(id: string): Promise { + const path: string = 'areas/' + id + '/map-metadata' const result = await this.GET(path).catch((e) => { console.error(`Failed to GET /${path}: ` + e) throw e @@ -360,7 +360,7 @@ export class BackendAPICaller { } static async reRunMission(missionId: string, failedTasksOnly: boolean = false): Promise { - let mission = await this.getMissionById(missionId) + let mission = await this.getMissionRunById(missionId) // TODO: utilise reschedule endpoint instead of copying diff --git a/frontend/src/components/Pages/FrontPage/MissionOverview/MissionQueueView.tsx b/frontend/src/components/Pages/FrontPage/MissionOverview/MissionQueueView.tsx index 8cc0af54f..31a48a872 100644 --- a/frontend/src/components/Pages/FrontPage/MissionOverview/MissionQueueView.tsx +++ b/frontend/src/components/Pages/FrontPage/MissionOverview/MissionQueueView.tsx @@ -86,6 +86,7 @@ export function MissionQueueView({ refreshInterval }: RefreshProps) { if (selectedRobot === undefined) return selectedEchoMissions.forEach((mission: MissionDefinition) => { + // TODO: as a final parameter here we likely want mission.AreaName, and maybe also installation and deck codes BackendAPICaller.postMission(mission.echoMissionId, selectedRobot.id, assetCode) }) diff --git a/frontend/src/components/Pages/MissionPage/MissionPage.tsx b/frontend/src/components/Pages/MissionPage/MissionPage.tsx index 435f5842f..2f7807c34 100644 --- a/frontend/src/components/Pages/MissionPage/MissionPage.tsx +++ b/frontend/src/components/Pages/MissionPage/MissionPage.tsx @@ -40,7 +40,7 @@ export function MissionPage() { useEffect(() => { if (missionId) { - BackendAPICaller.getMissionById(missionId).then((mission) => { + BackendAPICaller.getMissionRunById(missionId).then((mission) => { setSelectedMission(mission) updateVideoStreams(mission) }) @@ -51,7 +51,7 @@ export function MissionPage() { const timeDelay = 1000 const id = setInterval(() => { if (missionId) { - BackendAPICaller.getMissionById(missionId).then((mission) => { + BackendAPICaller.getMissionRunById(missionId).then((mission) => { setSelectedMission(mission) }) } diff --git a/frontend/src/components/Pages/RobotPage/AssetDeckMapView.tsx b/frontend/src/components/Pages/RobotPage/AreaMapView.tsx similarity index 92% rename from frontend/src/components/Pages/RobotPage/AssetDeckMapView.tsx rename to frontend/src/components/Pages/RobotPage/AreaMapView.tsx index 8bb8be121..ba8ea83b6 100644 --- a/frontend/src/components/Pages/RobotPage/AssetDeckMapView.tsx +++ b/frontend/src/components/Pages/RobotPage/AreaMapView.tsx @@ -6,14 +6,14 @@ import { useErrorHandler } from 'react-error-boundary' import { PlaceRobotInMap, InverseCalculatePixelPosition } from '../../../utils/MapMarkers' import { BackendAPICaller } from 'api/ApiCaller' import { MapMetadata } from 'models/MapMetadata' -import { AssetDeck } from 'models/AssetDeck' +import { Area } from 'models/Area' import { Position } from 'models/Position' import { Pose } from 'models/Pose' import { TranslateText } from 'components/Contexts/LanguageContext' - import { MapCompass } from 'utils/MapCompass' -interface AssetDeckProps { - assetDeck: AssetDeck + +interface AreaProps { + area: Area localizationPose: Pose setLocalizationPose: (newPose: Pose) => void } @@ -44,7 +44,7 @@ const StyledMapCompass = styled.div` align-items: end; ` -export function AssetDeckMapView({ assetDeck, localizationPose, setLocalizationPose }: AssetDeckProps) { +export function AreaMapView({ area, localizationPose, setLocalizationPose }: AreaProps) { const handleError = useErrorHandler() const [mapCanvas, setMapCanvas] = useState(document.createElement('canvas')) const [mapImage, setMapImage] = useState(document.createElement('img')) @@ -75,10 +75,10 @@ export function AssetDeckMapView({ assetDeck, localizationPose, setLocalizationP useEffect(() => { setIsLoading(true) setImageObjectURL(undefined) - BackendAPICaller.getAssetDeckMapMetadata(assetDeck.id) + BackendAPICaller.getAreasMapMetadata(area.id) .then((mapMetadata) => { setMapMetadata(mapMetadata) - BackendAPICaller.getMap(assetDeck.assetCode, mapMetadata.mapName) + BackendAPICaller.getMap(area.assetCode, mapMetadata.mapName) .then((imageBlob) => { setImageObjectURL(URL.createObjectURL(imageBlob)) }) @@ -91,7 +91,7 @@ export function AssetDeckMapView({ assetDeck, localizationPose, setLocalizationP setImageObjectURL(NoMap) }) //.catch((e) => handleError(e)) - }, [assetDeck]) + }, [area]) useEffect(() => { if (!imageObjectURL) { @@ -139,7 +139,7 @@ export function AssetDeckMapView({ assetDeck, localizationPose, setLocalizationP return } const assetPosition = InverseCalculatePixelPosition(mapMetadata, pixelPosition) - let newPose: Pose = assetDeck.defaultLocalizationPose + let newPose: Pose = area.defaultLocalizationPose newPose.position.x = assetPosition[0] newPose.position.y = assetPosition[1] setLocalizationPose(newPose) diff --git a/frontend/src/components/Pages/RobotPage/LocalizationDialog.tsx b/frontend/src/components/Pages/RobotPage/LocalizationDialog.tsx index 5e4bbf86e..134116c54 100644 --- a/frontend/src/components/Pages/RobotPage/LocalizationDialog.tsx +++ b/frontend/src/components/Pages/RobotPage/LocalizationDialog.tsx @@ -4,9 +4,9 @@ import { TranslateText } from 'components/Contexts/LanguageContext' import { Icons } from 'utils/icons' import { useState, useEffect } from 'react' import { BackendAPICaller } from 'api/ApiCaller' -import { AssetDeck } from 'models/AssetDeck' +import { Area } from 'models/Area' import { Robot } from 'models/Robot' -import { AssetDeckMapView } from './AssetDeckMapView' +import { AreaMapView } from './AreaMapView' import { Pose } from 'models/Pose' import { Orientation } from 'models/Orientation' @@ -39,8 +39,8 @@ interface RobotProps { export const LocalizationDialog = ({ robot }: RobotProps): JSX.Element => { const [isLocalizationDialogOpen, setIsLocalizationDialogOpen] = useState(false) - const [selectedAssetDeck, setSelectedAssetDeck] = useState() - const [assetDecks, setAssetDecks] = useState() + const [selectedArea, setSelectedArea] = useState() + const [areas, setAreas] = useState() const [localisationPose, setLocalizationPose] = useState() const [selectedDirection, setSelectedDirecion] = useState() @@ -52,24 +52,24 @@ export const LocalizationDialog = ({ robot }: RobotProps): JSX.Element => { ]) useEffect(() => { - BackendAPICaller.getAssetDecks().then((response: AssetDeck[]) => { - setAssetDecks(response) + BackendAPICaller.getAreas().then((response: Area[]) => { + setAreas(response) }) }, []) - const getAssetDeckNames = (assetDecks: AssetDeck[]): Map => { - var assetDeckNameMap = new Map() - assetDecks.forEach((assetDeck: AssetDeck) => { - assetDeckNameMap.set(assetDeck.deckName, assetDeck) + const getAreaNames = (areas: Area[]): Map => { + var areaNameMap = new Map() + areas.forEach((area: Area) => { + areaNameMap.set(area.deckName, area) }) - return assetDeckNameMap + return areaNameMap } const onSelectedDeck = (changes: AutocompleteChanges) => { const selectedDeckName = changes.selectedItems[0] - const selectedAssetDeck = assetDecks?.find((assetDeck) => assetDeck.deckName === selectedDeckName) - setSelectedAssetDeck(selectedAssetDeck) - let newPose = selectedAssetDeck?.defaultLocalizationPose + const selectedArea = areas?.find((area) => area.deckName === selectedDeckName) + setSelectedArea(selectedArea) + let newPose = selectedArea?.defaultLocalizationPose if (newPose && selectedDirection) { newPose.orientation = selectedDirection } @@ -92,16 +92,17 @@ export const LocalizationDialog = ({ robot }: RobotProps): JSX.Element => { const onLocalizationDialogClose = () => { setIsLocalizationDialogOpen(false) - setSelectedAssetDeck(undefined) + setSelectedArea(undefined) } const onClickLocalize = () => { - if (selectedAssetDeck && localisationPose) { - BackendAPICaller.postLocalizationMission(localisationPose, robot.id, selectedAssetDeck.id) + if (selectedArea && localisationPose) { + BackendAPICaller.postLocalizationMission(localisationPose, robot.id, selectedArea.id) } onLocalizationDialogClose() } - const assetDeckNames = assetDecks ? Array.from(getAssetDeckNames(assetDecks).keys()).sort() : [] + + const areaNames = areas ? Array.from(getAreaNames(areas).keys()).sort() : [] return ( <> @@ -122,7 +123,7 @@ export const LocalizationDialog = ({ robot }: RobotProps): JSX.Element => { {TranslateText('Localize robot')} @@ -132,9 +133,9 @@ export const LocalizationDialog = ({ robot }: RobotProps): JSX.Element => { onOptionsChange={onSelectedDirection} /> - {selectedAssetDeck && localisationPose && ( - @@ -150,7 +151,7 @@ export const LocalizationDialog = ({ robot }: RobotProps): JSX.Element => { {' '} {TranslateText('Cancel')}{' '} - diff --git a/frontend/src/models/AssetDeck.ts b/frontend/src/models/Area.ts similarity index 62% rename from frontend/src/models/AssetDeck.ts rename to frontend/src/models/Area.ts index 1758186ae..ff3c21e57 100644 --- a/frontend/src/models/AssetDeck.ts +++ b/frontend/src/models/Area.ts @@ -1,7 +1,9 @@ import { Pose } from './Pose' -export interface AssetDeck { +export interface Area { id: string + areaName: string + installationCode: string assetCode: string deckName: string defaultLocalizationPose: Pose diff --git a/frontend/src/models/Robot.ts b/frontend/src/models/Robot.ts index f76674974..e62037196 100644 --- a/frontend/src/models/Robot.ts +++ b/frontend/src/models/Robot.ts @@ -1,4 +1,4 @@ -import { AssetDeck } from './AssetDeck' +import { Area } from './Area' import { BatteryStatus } from './Battery' import { Pose } from './Pose' import { RobotModel } from './RobotModel' @@ -27,5 +27,5 @@ export interface Robot { port?: number videoStreams?: VideoStream[] isarUri?: string - currentAssetDeck?: AssetDeck + currentArea?: Area } From da01bf672af74ecc3f528e2c63dff8a5b4ffd3f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20Chirico=20Indreb=C3=B8?= Date: Wed, 5 Jul 2023 14:06:42 +0200 Subject: [PATCH 04/10] Move mission sorting to new class --- backend/api/Controllers/MissionController.cs | 4 +- .../api/Database/Models/MissionDefinition.cs | 2 +- backend/api/Database/Models/MissionRun.cs | 2 +- backend/api/Database/Models/SortableRecord.cs | 2 +- .../api/Services/MissionDefinitionService.cs | 60 +------------------ backend/api/Services/MissionRunService.cs | 60 +------------------ backend/api/Services/SortingService.cs | 5 +- 7 files changed, 10 insertions(+), 125 deletions(-) diff --git a/backend/api/Controllers/MissionController.cs b/backend/api/Controllers/MissionController.cs index e77fc9397..1257af8d3 100644 --- a/backend/api/Controllers/MissionController.cs +++ b/backend/api/Controllers/MissionController.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Globalization; +using System.Globalization; +using System.Text.Json; using Api.Controllers.Models; using Api.Database.Models; using Api.Services; diff --git a/backend/api/Database/Models/MissionDefinition.cs b/backend/api/Database/Models/MissionDefinition.cs index 228eca2a2..f2136ee72 100644 --- a/backend/api/Database/Models/MissionDefinition.cs +++ b/backend/api/Database/Models/MissionDefinition.cs @@ -4,7 +4,7 @@ #pragma warning disable CS8618 namespace Api.Database.Models { - public class MissionDefinition + public class MissionDefinition : SortableRecord { [Key] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] diff --git a/backend/api/Database/Models/MissionRun.cs b/backend/api/Database/Models/MissionRun.cs index c8a7c4b61..e192ca7ce 100644 --- a/backend/api/Database/Models/MissionRun.cs +++ b/backend/api/Database/Models/MissionRun.cs @@ -5,7 +5,7 @@ #pragma warning disable CS8618 namespace Api.Database.Models { - public class MissionRun + public class MissionRun : SortableRecord { [Key] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] diff --git a/backend/api/Database/Models/SortableRecord.cs b/backend/api/Database/Models/SortableRecord.cs index 33127dc6c..bdfc90f67 100644 --- a/backend/api/Database/Models/SortableRecord.cs +++ b/backend/api/Database/Models/SortableRecord.cs @@ -1,4 +1,4 @@ -#pragma warning disable CS8618 +#pragma warning disable CS8618 namespace Api.Database.Models { public interface SortableRecord diff --git a/backend/api/Services/MissionDefinitionService.cs b/backend/api/Services/MissionDefinitionService.cs index 486eb874a..eb8db5be5 100644 --- a/backend/api/Services/MissionDefinitionService.cs +++ b/backend/api/Services/MissionDefinitionService.cs @@ -1,7 +1,5 @@ using System.Linq.Dynamic.Core; using System.Linq.Expressions; -using System.Reflection; -using System.Text; using Api.Controllers.Models; using Api.Database.Context; using Api.Database.Models; @@ -81,7 +79,7 @@ public async Task> ReadAll(MissionDefinitionQuerySt SearchByName(ref query, parameters.NameSearch); - ApplySort(ref query, parameters.OrderBy); + SortingService.ApplySort(ref query, parameters.OrderBy); return await PagedList.ToPagedListAsync( query, @@ -163,61 +161,5 @@ MissionDefinitionQueryStringParameters parameters // Constructing the resulting lambda expression by combining parameter and body return Expression.Lambda>(body, missionRunExpression); } - - private static void ApplySort(ref IQueryable missionDefinitions, string orderByQueryString) - { - if (!missionDefinitions.Any()) - return; - - if (string.IsNullOrWhiteSpace(orderByQueryString)) - { - missionDefinitions = missionDefinitions.OrderBy(x => x.Name); - return; - } - - string[] orderParams = orderByQueryString - .Trim() - .Split(',') - .Select(parameterString => parameterString.Trim()) - .ToArray(); - - var propertyInfos = typeof(MissionDefinition).GetProperties( - BindingFlags.Public | BindingFlags.Instance - ); - var orderQueryBuilder = new StringBuilder(); - - foreach (string param in orderParams) - { - if (string.IsNullOrWhiteSpace(param)) - continue; - - string propertyFromQueryName = param.Split(" ")[0]; - var objectProperty = propertyInfos.FirstOrDefault( - pi => - pi.Name.Equals( - propertyFromQueryName, - StringComparison.InvariantCultureIgnoreCase - ) - ); - - if (objectProperty == null) - throw new InvalidDataException( - $"Mission has no property '{propertyFromQueryName}' for ordering" - ); - - string sortingOrder = param.EndsWith(" desc", StringComparison.OrdinalIgnoreCase) - ? "descending" - : "ascending"; - - string sortParameter = $"{objectProperty.Name} {sortingOrder}, "; - orderQueryBuilder.Append(sortParameter); - } - - string orderQuery = orderQueryBuilder.ToString().TrimEnd(',', ' '); - - missionDefinitions = string.IsNullOrWhiteSpace(orderQuery) - ? missionDefinitions.OrderBy(missionDefinition => missionDefinition.Name) - : missionDefinitions.OrderBy(orderQuery); - } } } diff --git a/backend/api/Services/MissionRunService.cs b/backend/api/Services/MissionRunService.cs index ce1d950ae..659b92110 100644 --- a/backend/api/Services/MissionRunService.cs +++ b/backend/api/Services/MissionRunService.cs @@ -1,7 +1,5 @@ using System.Linq.Dynamic.Core; using System.Linq.Expressions; -using System.Reflection; -using System.Text; using Api.Controllers.Models; using Api.Database.Context; using Api.Database.Models; @@ -105,7 +103,7 @@ public async Task> ReadAll(MissionRunQueryStringParameters SearchByRobotName(ref query, parameters.RobotNameSearch); SearchByTag(ref query, parameters.TagSearch); - ApplySort(ref query, parameters.OrderBy); + SortingService.ApplySort(ref query, parameters.OrderBy); return await PagedList.ToPagedListAsync( query, @@ -409,61 +407,5 @@ MissionRunQueryStringParameters parameters // Constructing the resulting lambda expression by combining parameter and body return Expression.Lambda>(body, missionRun); } - - private static void ApplySort(ref IQueryable missionRuns, string orderByQueryString) - { - if (!missionRuns.Any()) - return; - - if (string.IsNullOrWhiteSpace(orderByQueryString)) - { - missionRuns = missionRuns.OrderBy(x => x.Name); - return; - } - - string[] orderParams = orderByQueryString - .Trim() - .Split(',') - .Select(parameterString => parameterString.Trim()) - .ToArray(); - - var propertyInfos = typeof(MissionRun).GetProperties( - BindingFlags.Public | BindingFlags.Instance - ); - var orderQueryBuilder = new StringBuilder(); - - foreach (string param in orderParams) - { - if (string.IsNullOrWhiteSpace(param)) - continue; - - string propertyFromQueryName = param.Split(" ")[0]; - var objectProperty = propertyInfos.FirstOrDefault( - pi => - pi.Name.Equals( - propertyFromQueryName, - StringComparison.InvariantCultureIgnoreCase - ) - ); - - if (objectProperty == null) - throw new InvalidDataException( - $"MissionRun has no property '{propertyFromQueryName}' for ordering" - ); - - string sortingOrder = param.EndsWith(" desc", StringComparison.OrdinalIgnoreCase) - ? "descending" - : "ascending"; - - string sortParameter = $"{objectProperty.Name} {sortingOrder}, "; - orderQueryBuilder.Append(sortParameter); - } - - string orderQuery = orderQueryBuilder.ToString().TrimEnd(',', ' '); - - missionRuns = string.IsNullOrWhiteSpace(orderQuery) - ? missionRuns.OrderBy(missionRun => missionRun.Name) - : missionRuns.OrderBy(orderQuery); - } } } diff --git a/backend/api/Services/SortingService.cs b/backend/api/Services/SortingService.cs index 041d54e95..3303fffe9 100644 --- a/backend/api/Services/SortingService.cs +++ b/backend/api/Services/SortingService.cs @@ -1,4 +1,4 @@ -using System.Linq.Dynamic.Core; +using System.Linq.Dynamic.Core; using System.Reflection; using System.Text; using Api.Database.Models; @@ -39,7 +39,8 @@ public static void ApplySort(ref IQueryable missions, string orderByQueryS pi => pi.Name.Equals( propertyFromQueryName, - StringComparison.Ordinal) + StringComparison.InvariantCultureIgnoreCase + ) ); if (objectProperty == null) From 6e05bb090a23b21d88422203ef5f3d5b78efa732 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20Chirico=20Indreb=C3=B8?= Date: Wed, 5 Jul 2023 15:57:16 +0200 Subject: [PATCH 05/10] Use BlobService for custom mission source --- backend/api/Services/CustomMissionService.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/backend/api/Services/CustomMissionService.cs b/backend/api/Services/CustomMissionService.cs index d3cbc0ea1..3b33dadf3 100644 --- a/backend/api/Services/CustomMissionService.cs +++ b/backend/api/Services/CustomMissionService.cs @@ -25,11 +25,6 @@ public CustomMissionService(IOptions storageOptions, IBlobServic _blobService = blobService; } - public async Task UploadFile(string fileName, Stream fileStream) - { - throw new NotImplementedException(); - } - public async Task UploadSource(List tasks) { string json = JsonSerializer.Serialize(tasks); From 2d1b01e83d6e11a97f3062abf0eced16ddc67e1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20Chirico=20Indreb=C3=B8?= Date: Fri, 7 Jul 2023 14:34:02 +0200 Subject: [PATCH 06/10] Map installation->plant, asset->installation --- backend/README.md | 2 +- backend/api.test/EndpointTest.cs | 82 ++++----- .../EventHandlers/TestMissionScheduler.cs | 16 +- backend/api.test/Mocks/MapServiceMock.cs | 4 +- backend/api.test/Services/MissionService.cs | 16 +- backend/api.test/Services/RobotService.cs | 2 +- backend/api/Controllers/AreaController.cs | 40 ++--- backend/api/Controllers/AssetController.cs | 147 ----------------- backend/api/Controllers/DeckController.cs | 22 +-- backend/api/Controllers/EchoController.cs | 6 +- .../api/Controllers/InstallationController.cs | 12 +- backend/api/Controllers/MissionController.cs | 28 ++-- .../api/Controllers/Models/AreaResponse.cs | 4 +- .../Models/CondensedMissionDefinition.cs | 2 +- .../api/Controllers/Models/CreateAreaQuery.cs | 2 +- .../Controllers/Models/CreateAssetQuery.cs | 8 - .../api/Controllers/Models/CreateDeckQuery.cs | 2 +- .../Models/CreateInstallationQuery.cs | 1 - .../Controllers/Models/CreatePlantQuery.cs | 9 + .../Controllers/Models/CreateRobotQuery.cs | 2 +- .../Controllers/Models/CustomMissionQuery.cs | 2 +- backend/api/Controllers/Models/EchoMission.cs | 2 +- .../api/Controllers/Models/EchoPlantInfo.cs | 2 +- .../MissionDefinitionQueryStringParameters.cs | 4 +- .../Models/MissionRunQueryStringParameters.cs | 4 +- .../Models/ScheduledMissionQuery.cs | 2 +- backend/api/Controllers/PlantController.cs | 155 ++++++++++++++++++ backend/api/Controllers/RobotController.cs | 22 +-- .../api/Controllers/RobotModelController.cs | 2 +- .../api/Database/Context/FlotillaDbContext.cs | 16 +- backend/api/Database/Context/InitDb.cs | 139 ++++++++-------- backend/api/Database/Models/Area.cs | 6 +- backend/api/Database/Models/AssetDeck.cs | 26 +++ backend/api/Database/Models/Deck.cs | 4 +- backend/api/Database/Models/Installation.cs | 7 +- .../api/Database/Models/MissionDefinition.cs | 4 +- backend/api/Database/Models/MissionRun.cs | 2 +- .../Database/Models/{Asset.cs => Plant.cs} | 11 +- backend/api/Database/Models/Robot.cs | 6 +- backend/api/Database/Models/SortableRecord.cs | 2 +- backend/api/EventHandlers/MqttEventHandler.cs | 14 +- .../api/MQTT/MessageModels/IsarRobotInfo.cs | 2 +- backend/api/Program.cs | 2 +- backend/api/Services/AreaService.cs | 80 ++++----- backend/api/Services/AssetService.cs | 107 ------------ backend/api/Services/DeckService.cs | 42 ++--- backend/api/Services/EchoService.cs | 6 +- backend/api/Services/InstallationService.cs | 46 +----- backend/api/Services/MapService.cs | 14 +- .../api/Services/MissionDefinitionService.cs | 16 +- backend/api/Services/MissionRunService.cs | 12 +- .../Services/Models/IsarMissionDefinition.cs | 2 +- backend/api/Services/PlantService.cs | 137 ++++++++++++++++ backend/api/Services/SortingService.cs | 2 +- backend/api/Utilities/Exceptions.cs | 8 +- frontend/src/api/ApiCaller.tsx | 22 +-- .../src/components/Contexts/APIUpdater.tsx | 6 +- .../src/components/Contexts/AssetContext.tsx | 40 ----- .../Contexts/InstallationContext.tsx | 40 +++++ frontend/src/components/Header/Header.tsx | 22 +-- .../src/components/Pages/FlotillaSite.tsx | 6 +- .../MissionOverview/CreateMissionButton.tsx | 14 +- .../MissionOverview/MissionQueueView.tsx | 12 +- .../MissionOverview/ScheduleMissionDialog.tsx | 10 +- .../FrontPage/RobotCards/RobotStatusView.tsx | 12 +- .../MapPosition/MissionMapView.tsx | 4 +- .../Pages/RobotPage/AreaMapView.tsx | 2 +- frontend/src/language/en.json | 6 +- frontend/src/language/no.json | 6 +- frontend/src/models/Area.ts | 2 +- frontend/src/models/CustomMission.ts | 4 +- frontend/src/models/EchoMission.ts | 2 +- frontend/src/models/Mission.ts | 2 +- frontend/src/models/Robot.ts | 2 +- 74 files changed, 782 insertions(+), 747 deletions(-) delete mode 100644 backend/api/Controllers/AssetController.cs delete mode 100644 backend/api/Controllers/Models/CreateAssetQuery.cs create mode 100644 backend/api/Controllers/Models/CreatePlantQuery.cs create mode 100644 backend/api/Controllers/PlantController.cs create mode 100644 backend/api/Database/Models/AssetDeck.cs rename backend/api/Database/Models/{Asset.cs => Plant.cs} (73%) delete mode 100644 backend/api/Services/AssetService.cs create mode 100644 backend/api/Services/PlantService.cs delete mode 100644 frontend/src/components/Contexts/AssetContext.tsx create mode 100644 frontend/src/components/Contexts/InstallationContext.tsx diff --git a/backend/README.md b/backend/README.md index 1543a36c4..f13e00e91 100644 --- a/backend/README.md +++ b/backend/README.md @@ -248,8 +248,8 @@ The access matrix looks like this: | -------------------------- | ------------- | -------- | --------- | | Area | Read | Read | CRUD | | Deck | Read | Read | CRUD | +| Plant | Read | Read | CRUD | | Installation | Read | Read | CRUD | -| Asset | Read | Read | CRUD | | Echo | Read | Read | CRUD | | Missions | Read | Read | CRUD | | Robots | Read | Read | CRUD | diff --git a/backend/api.test/EndpointTest.cs b/backend/api.test/EndpointTest.cs index 19ecaf521..be11d9f9e 100644 --- a/backend/api.test/EndpointTest.cs +++ b/backend/api.test/EndpointTest.cs @@ -102,10 +102,10 @@ public void Dispose() GC.SuppressFinalize(this); } - private async Task PopulateAreaDb(string assetName, string installationName, string deckName, string areaName) + private async Task PopulateAreaDb(string installationCode, string plantCode, string deckName, string areaName) { - string assetUrl = $"/assets"; string installationUrl = $"/installations"; + string plantUrl = $"/plants"; string deckUrl = $"/decks"; string areaUrl = $"/areas"; var testPose = new Pose @@ -125,43 +125,43 @@ private async Task PopulateAreaDb(string assetName, string installationName, str } }; - var assetQuery = new CreateAssetQuery + var installationQuery = new CreateInstallationQuery { - AssetCode = assetName, - Name = assetName + InstallationCode = installationCode, + Name = installationCode }; - var installationQuery = new CreateInstallationQuery + var plantQuery = new CreatePlantQuery { - AssetCode = assetName, - InstallationCode = installationName, - Name = installationName + InstallationCode = installationCode, + PlantCode = plantCode, + Name = plantCode }; var deckQuery = new CreateDeckQuery { - AssetCode = assetName, - InstallationCode = installationName, + InstallationCode = installationCode, + PlantCode = plantCode, Name = deckName }; var areaQuery = new CreateAreaQuery { - AssetCode = assetName, - InstallationCode = installationName, + InstallationCode = installationCode, + PlantCode = plantCode, DeckName = deckName, AreaName = areaName, DefaultLocalizationPose = testPose }; - var assetContent = new StringContent( - JsonSerializer.Serialize(assetQuery), + var installationContent = new StringContent( + JsonSerializer.Serialize(installationQuery), null, "application/json" ); - var installationContent = new StringContent( - JsonSerializer.Serialize(installationQuery), + var plantContent = new StringContent( + JsonSerializer.Serialize(plantQuery), null, "application/json" ); @@ -179,10 +179,10 @@ private async Task PopulateAreaDb(string assetName, string installationName, str ); // Act - var assetResponse = await _client.PostAsync(assetUrl, assetContent); - Assert.NotNull(assetResponse); var installationResponse = await _client.PostAsync(installationUrl, installationContent); Assert.NotNull(installationResponse); + var plantResponse = await _client.PostAsync(plantUrl, plantContent); + Assert.NotNull(plantResponse); var deckResponse = await _client.PostAsync(deckUrl, deckContent); Assert.NotNull(deckResponse); var areaResponse = await _client.PostAsync(areaUrl, areaContent); @@ -272,7 +272,7 @@ public async Task StartMissionTest() Assert.True(robots != null); var robot = robots[0]; string robotId = robot.Id; - string testAsset = "TestAsset"; + string testInstallation = "TestInstallation"; string testArea = "testArea"; int echoMissionId = 95; @@ -280,7 +280,7 @@ public async Task StartMissionTest() var query = new ScheduledMissionQuery { RobotId = robotId, - AssetCode = testAsset, + InstallationCode = testInstallation, AreaName = testArea, EchoMissionId = echoMissionId, DesiredStartTime = DateTimeOffset.UtcNow @@ -304,12 +304,12 @@ public async Task StartMissionTest() public async Task AreaTest() { // Arrange - string testAsset = "TestAsset"; string testInstallation = "TestInstallation"; + string testPlant = "TestPlant"; string testDeck = "testDeck2"; string testArea = "testArea"; - string assetUrl = $"/assets"; string installationUrl = $"/installations"; + string plantUrl = $"/plants"; string deckUrl = $"/decks"; string areaUrl = $"/areas"; var testPose = new Pose @@ -329,43 +329,43 @@ public async Task AreaTest() } }; - var assetQuery = new CreateAssetQuery + var installationQuery = new CreateInstallationQuery { - AssetCode = testAsset, - Name = testAsset + InstallationCode = testInstallation, + Name = testInstallation }; - var installationQuery = new CreateInstallationQuery + var plantQuery = new CreatePlantQuery { - AssetCode = testAsset, InstallationCode = testInstallation, - Name = testInstallation + PlantCode = testPlant, + Name = testPlant }; var deckQuery = new CreateDeckQuery { - AssetCode = testAsset, InstallationCode = testInstallation, + PlantCode = testPlant, Name = testDeck }; var areaQuery = new CreateAreaQuery { - AssetCode = testAsset, InstallationCode = testInstallation, + PlantCode = testPlant, DeckName = testDeck, AreaName = testArea, DefaultLocalizationPose = testPose }; - var assetContent = new StringContent( - JsonSerializer.Serialize(assetQuery), + var installationContent = new StringContent( + JsonSerializer.Serialize(installationQuery), null, "application/json" ); - var installationContent = new StringContent( - JsonSerializer.Serialize(installationQuery), + var plantContent = new StringContent( + JsonSerializer.Serialize(plantQuery), null, "application/json" ); @@ -383,14 +383,14 @@ public async Task AreaTest() ); // Act - var assetResponse = await _client.PostAsync(assetUrl, assetContent); var installationResponse = await _client.PostAsync(installationUrl, installationContent); + var plantResponse = await _client.PostAsync(plantUrl, plantContent); var deckResponse = await _client.PostAsync(deckUrl, deckContent); var areaResponse = await _client.PostAsync(areaUrl, areaContent); // Assert - Assert.True(assetResponse.IsSuccessStatusCode); Assert.True(installationResponse.IsSuccessStatusCode); + Assert.True(plantResponse.IsSuccessStatusCode); Assert.True(deckResponse.IsSuccessStatusCode); Assert.True(areaResponse.IsSuccessStatusCode); var area = await areaResponse.Content.ReadFromJsonAsync(_serializerOptions); @@ -401,11 +401,11 @@ public async Task AreaTest() public async Task SafePositionTest() { // Arrange - Add Safe Position - string testAsset = "testAsset"; string testInstallation = "testInstallation"; + string testPlant = "testPlant"; string testDeck = "testDeck"; string testArea = "testArea"; - string addSafePositionUrl = $"/areas/{testAsset}/{testInstallation}/{testDeck}/{testArea}/safe-position"; + string addSafePositionUrl = $"/areas/{testInstallation}/{testPlant}/{testDeck}/{testArea}/safe-position"; var testPosition = new Position { X = 1, @@ -429,7 +429,7 @@ public async Task SafePositionTest() "application/json" ); - await PopulateAreaDb("testAsset", "testInstallation", "testDeck", "testArea"); + await PopulateAreaDb("testInstallation", "testPlant", "testDeck", "testArea"); var areaResponse = await _client.PostAsync(addSafePositionUrl, content); Assert.True(areaResponse.IsSuccessStatusCode); @@ -446,7 +446,7 @@ public async Task SafePositionTest() string robotId = robot.Id; // Act - string goToSafePositionUrl = $"/robots/{robotId}/{testAsset}/{testArea}/go-to-safe-position"; + string goToSafePositionUrl = $"/robots/{robotId}/{testInstallation}/{testArea}/go-to-safe-position"; var missionResponse = await _client.PostAsync(goToSafePositionUrl, null); // Assert diff --git a/backend/api.test/EventHandlers/TestMissionScheduler.cs b/backend/api.test/EventHandlers/TestMissionScheduler.cs index c0290b132..eebf92ffa 100644 --- a/backend/api.test/EventHandlers/TestMissionScheduler.cs +++ b/backend/api.test/EventHandlers/TestMissionScheduler.cs @@ -22,16 +22,16 @@ namespace Api.Test.EventHandlers [Collection("Database collection")] public class TestMissionScheduler : IDisposable { - private static readonly Asset testAsset = new() + private static readonly Installation testInstallation = new() { - AssetCode = "test", + InstallationCode = "test", Name = "test test" }; - private static readonly Installation testInstallation = new() + private static readonly Plant testPlant = new() { - InstallationCode = "test", + PlantCode = "test", Name = "test test", - Asset = testAsset + Installation = testInstallation }; private static MissionRun ScheduledMission => @@ -45,12 +45,12 @@ public class TestMissionScheduler : IDisposable { Deck = new Deck { + Plant = testPlant, Installation = testInstallation, - Asset = testAsset, Name = "testDeck" }, - Asset = testAsset, Installation = testInstallation, + Plant = testPlant, Name = "testArea", MapMetadata = new MapMetadata() { @@ -67,7 +67,7 @@ public class TestMissionScheduler : IDisposable Boundary = new(), TransformationMatrices = new() }, - AssetCode = "testAsset" + InstallationCode = "testInstallation" }; private readonly MissionScheduler _scheduledMissionEventHandler; diff --git a/backend/api.test/Mocks/MapServiceMock.cs b/backend/api.test/Mocks/MapServiceMock.cs index 808f71db3..a5a5e4a7a 100644 --- a/backend/api.test/Mocks/MapServiceMock.cs +++ b/backend/api.test/Mocks/MapServiceMock.cs @@ -9,7 +9,7 @@ namespace Api.Test.Mocks { public class MockMapService : IMapService { - public async Task ChooseMapFromPositions(IList positions, string assetCode) + public async Task ChooseMapFromPositions(IList positions, string installationCode) { await Task.Run(() => Thread.Sleep(1)); return new MapMetadata(); @@ -20,7 +20,7 @@ public async Task AssignMapToMission(MissionRun mission) await Task.Run(() => Thread.Sleep(1)); } - public async Task FetchMapImage(string mapName, string assetCode) + public async Task FetchMapImage(string mapName, string installationCode) { await Task.Run(() => Thread.Sleep(1)); string filePath = Directory.GetCurrentDirectory() + "Images/MockMapImage.png"; diff --git a/backend/api.test/Services/MissionService.cs b/backend/api.test/Services/MissionService.cs index 010887420..275634129 100644 --- a/backend/api.test/Services/MissionService.cs +++ b/backend/api.test/Services/MissionService.cs @@ -47,16 +47,16 @@ public async Task Create() .ReadAll(new MissionRunQueryStringParameters()) .Result.Count; - var testAsset = new Asset + var testInstallation = new Installation { - AssetCode = "test", + InstallationCode = "test", Name = "test test" }; - var testInstallation = new Installation + var testPlant = new Plant { - InstallationCode = "test", + PlantCode = "test", Name = "test test", - Asset = testAsset + Installation = testInstallation }; MissionRun missionRun = @@ -70,18 +70,18 @@ public async Task Create() { Deck = new Deck { + Plant = testPlant, Installation = testInstallation, - Asset = testAsset, Name = "testDeck" }, - Asset = testAsset, Installation = testInstallation, + Plant = testPlant, Name = "testArea", MapMetadata = new MapMetadata() { MapName = "testMap" }, DefaultLocalizationPose = new Pose(), SafePositions = new List() }, - AssetCode = "testAsset", + InstallationCode = "testInstallation", DesiredStartTime = DateTime.Now }; diff --git a/backend/api.test/Services/RobotService.cs b/backend/api.test/Services/RobotService.cs index d2482c086..18121cf50 100644 --- a/backend/api.test/Services/RobotService.cs +++ b/backend/api.test/Services/RobotService.cs @@ -71,7 +71,7 @@ public async Task Create() IsarId = "", SerialNumber = "", VideoStreams = new List() { videoStreamQuery }, - CurrentAsset = "", + CurrentInstallation = "", RobotType = RobotType.Robot, Host = "", Port = 1, diff --git a/backend/api/Controllers/AreaController.cs b/backend/api/Controllers/AreaController.cs index 42f4e579d..85d2289fb 100644 --- a/backend/api/Controllers/AreaController.cs +++ b/backend/api/Controllers/AreaController.cs @@ -47,10 +47,10 @@ public async Task> Create([FromBody] CreateAreaQuery _logger.LogInformation("Creating new area"); try { - var existingArea = await _areaService.ReadByAssetAndName(area.AssetCode, area.AreaName); + var existingArea = await _areaService.ReadByInstallationAndName(area.InstallationCode, area.AreaName); if (existingArea != null) { - _logger.LogWarning("An area for given name and asset already exists"); + _logger.LogWarning("An area for given name and installation already exists"); return Conflict($"Area already exists"); } @@ -63,8 +63,8 @@ public async Task> Create([FromBody] CreateAreaQuery { Id = newArea.Id, DeckName = newArea.Deck.Name, + PlantCode = newArea.Plant.PlantCode, InstallationCode = newArea.Installation.InstallationCode, - AssetCode = newArea.Asset.AssetCode, AreaName = newArea.Name, MapMetadata = newArea.MapMetadata, DefaultLocalizationPose = newArea.DefaultLocalizationPose, @@ -91,35 +91,35 @@ public async Task> Create([FromBody] CreateAreaQuery /// [HttpPost] [Authorize(Roles = Role.Admin)] - [Route("{assetCode}/{installationCode}/{deckName}/{areaName}/safe-position")] + [Route("{installationCode}/{plantCode}/{deckName}/{areaName}/safe-position")] [ProducesResponseType(typeof(AreaResponse), StatusCodes.Status201Created)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] public async Task> AddSafePosition( - [FromRoute] string assetCode, [FromRoute] string installationCode, + [FromRoute] string plantCode, [FromRoute] string deckName, [FromRoute] string areaName, [FromBody] Pose safePosition ) { - _logger.LogInformation(@"Adding new safe position to {Asset}, {Installation}, - {Deck}, {Area}", assetCode, installationCode, deckName, areaName); + _logger.LogInformation(@"Adding new safe position to {Installation}, {Plant}, + {Deck}, {Area}", installationCode, plantCode, deckName, areaName); try { - var area = await _areaService.AddSafePosition(assetCode, areaName, new SafePosition(safePosition)); + var area = await _areaService.AddSafePosition(installationCode, areaName, new SafePosition(safePosition)); if (area != null) { - _logger.LogInformation(@"Successfully added new safe position for asset '{assetId}' - and name '{name}'", assetCode, areaName); + _logger.LogInformation(@"Successfully added new safe position for installation '{installationId}' + and name '{name}'", installationCode, areaName); var response = new AreaResponse { Id = area.Id, DeckName = area.Deck.Name, + PlantCode = area.Plant.PlantCode, InstallationCode = area.Installation.InstallationCode, - AssetCode = area.Asset.AssetCode, AreaName = area.Name, MapMetadata = area.MapMetadata, DefaultLocalizationPose = area.DefaultLocalizationPose, @@ -129,9 +129,9 @@ [FromBody] Pose safePosition } else { - _logger.LogInformation(@"No area with asset {assetCode}, installation {installationCode}, - deck {deckName} and name {areaName} could be found.", assetCode, installationCode, deckName, areaName); - return NotFound(@$"No area with asset {assetCode}, installation {installationCode}, + _logger.LogInformation(@"No area with installation {installationCode}, plant {plantCode}, + deck {deckName} and name {areaName} could be found.", installationCode, plantCode, deckName, areaName); + return NotFound(@$"No area with installation {installationCode}, plant {plantCode}, deck {deckName} and name {areaName} could be found."); } } @@ -162,8 +162,8 @@ public async Task> DeleteArea([FromRoute] string id) { Id = area.Id, DeckName = area.Deck.Name, + PlantCode = area.Plant.PlantCode, InstallationCode = area.Installation.InstallationCode, - AssetCode = area.Asset.AssetCode, AreaName = area.Name, MapMetadata = area.MapMetadata, DefaultLocalizationPose = area.DefaultLocalizationPose, @@ -173,10 +173,10 @@ public async Task> DeleteArea([FromRoute] string id) } /// - /// List all asset areas in the Flotilla database + /// List all installation areas in the Flotilla database /// /// - /// This query gets all asset areas + /// This query gets all installation areas /// [HttpGet] [Authorize(Roles = Role.Any)] @@ -194,8 +194,8 @@ public async Task>> GetAreas() { Id = area.Id, DeckName = area.Deck.Name, + PlantCode = area.Plant.PlantCode, InstallationCode = area.Installation.InstallationCode, - AssetCode = area.Asset.AssetCode, AreaName = area.Name, MapMetadata = area.MapMetadata, DefaultLocalizationPose = area.DefaultLocalizationPose, @@ -232,8 +232,8 @@ public async Task> GetAreaById([FromRoute] string id) { Id = area.Id, DeckName = area.Deck.Name, + PlantCode = area.Plant.PlantCode, InstallationCode = area.Installation.InstallationCode, - AssetCode = area.Asset.AssetCode, AreaName = area.Name, MapMetadata = area.MapMetadata, DefaultLocalizationPose = area.DefaultLocalizationPose, @@ -277,7 +277,7 @@ public async Task> GetMapMetadata([FromRoute] string i }; try { - mapMetadata = await _mapService.ChooseMapFromPositions(positions, area.Deck.Installation.Asset.AssetCode); + mapMetadata = await _mapService.ChooseMapFromPositions(positions, area.Deck.Plant.Installation.InstallationCode); } catch (RequestFailedException e) { diff --git a/backend/api/Controllers/AssetController.cs b/backend/api/Controllers/AssetController.cs deleted file mode 100644 index b6025d4f1..000000000 --- a/backend/api/Controllers/AssetController.cs +++ /dev/null @@ -1,147 +0,0 @@ -using Api.Controllers.Models; -using Api.Database.Models; -using Api.Services; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; - -namespace Api.Controllers -{ - [ApiController] - [Route("assets")] - public class AssetController : ControllerBase - { - private readonly IAssetService _assetService; - - private readonly IMapService _mapService; - - private readonly ILogger _logger; - - public AssetController( - ILogger logger, - IMapService mapService, - IAssetService assetService - ) - { - _logger = logger; - _mapService = mapService; - _assetService = assetService; - } - - /// - /// List all assets in the Flotilla database - /// - /// - /// This query gets all assets - /// - [HttpGet] - [Authorize(Roles = Role.Any)] - [ProducesResponseType(typeof(IList), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task>> GetAssets() - { - try - { - var assets = await _assetService.ReadAll(); - return Ok(assets); - } - catch (Exception e) - { - _logger.LogError(e, "Error during GET of assets from database"); - throw; - } - } - - /// - /// Lookup asset by specified id. - /// - [HttpGet] - [Authorize(Roles = Role.Any)] - [Route("{id}")] - [ProducesResponseType(typeof(Asset), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task> GetAssetById([FromRoute] string id) - { - try - { - var asset = await _assetService.ReadById(id); - if (asset == null) - return NotFound($"Could not find asset with id {id}"); - return Ok(asset); - } - catch (Exception e) - { - _logger.LogError(e, "Error during GET of asset from database"); - throw; - } - - } - - /// - /// Add a new asset - /// - /// - /// This query adds a new asset to the database - /// - [HttpPost] - [Authorize(Roles = Role.Admin)] - [ProducesResponseType(typeof(Asset), StatusCodes.Status201Created)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task> Create([FromBody] CreateAssetQuery asset) - { - _logger.LogInformation("Creating new asset"); - try - { - var existingAsset = await _assetService.ReadByName(asset.AssetCode); - if (existingAsset != null) - { - _logger.LogInformation("An asset for given name and asset already exists"); - return BadRequest($"Asset already exists"); - } - - var newAsset = await _assetService.Create(asset); - _logger.LogInformation( - "Succesfully created new asset with id '{assetId}'", - newAsset.Id - ); - return CreatedAtAction( - nameof(GetAssetById), - new { id = newAsset.Id }, - newAsset - ); - } - catch (Exception e) - { - _logger.LogError(e, "Error while creating new asset"); - throw; - } - } - - /// - /// Deletes the asset with the specified id from the database. - /// - [HttpDelete] - [Authorize(Roles = Role.Admin)] - [Route("{id}")] - [ProducesResponseType(typeof(Asset), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task> DeleteAsset([FromRoute] string id) - { - var asset = await _assetService.Delete(id); - if (asset is null) - return NotFound($"Asset with id {id} not found"); - return Ok(asset); - } - } -} diff --git a/backend/api/Controllers/DeckController.cs b/backend/api/Controllers/DeckController.cs index f17089d85..596f79a32 100644 --- a/backend/api/Controllers/DeckController.cs +++ b/backend/api/Controllers/DeckController.cs @@ -11,8 +11,8 @@ namespace Api.Controllers public class DeckController : ControllerBase { private readonly IDeckService _deckService; - private readonly IAssetService _assetService; private readonly IInstallationService _installationService; + private readonly IPlantService _plantService; private readonly IMapService _mapService; @@ -22,15 +22,15 @@ public DeckController( ILogger logger, IMapService mapService, IDeckService deckService, - IAssetService assetService, - IInstallationService installationService + IInstallationService installationService, + IPlantService plantService ) { _logger = logger; _mapService = mapService; _deckService = deckService; - _assetService = assetService; _installationService = installationService; + _plantService = plantService; } /// @@ -106,17 +106,17 @@ public async Task> Create([FromBody] CreateDeckQuery deck) _logger.LogInformation("Creating new deck"); try { - var existingAsset = await _assetService.ReadByName(deck.AssetCode); - if (existingAsset == null) - { - return NotFound($"Could not find asset with name {deck.AssetCode}"); - } - var existingInstallation = await _installationService.ReadByAssetAndName(existingAsset, deck.InstallationCode); + var existingInstallation = await _installationService.ReadByName(deck.InstallationCode); if (existingInstallation == null) { return NotFound($"Could not find installation with name {deck.InstallationCode}"); } - var existingDeck = await _deckService.ReadByAssetAndInstallationAndName(existingAsset, existingInstallation, deck.Name); + var existingPlant = await _plantService.ReadByInstallationAndName(existingInstallation, deck.PlantCode); + if (existingPlant == null) + { + return NotFound($"Could not find plant with name {deck.PlantCode}"); + } + var existingDeck = await _deckService.ReadByInstallationAndPlantAndName(existingInstallation, existingPlant, deck.Name); if (existingDeck != null) { _logger.LogInformation("An deck for given name and deck already exists"); diff --git a/backend/api/Controllers/EchoController.cs b/backend/api/Controllers/EchoController.cs index 16aac397c..fa5ba1546 100644 --- a/backend/api/Controllers/EchoController.cs +++ b/backend/api/Controllers/EchoController.cs @@ -23,7 +23,7 @@ public EchoController(ILogger logger, IEchoService echoService) } /// - /// List all available Echo missions for the asset + /// List all available Echo missions for the installation /// /// /// These missions are created in the Echo mission planner @@ -36,11 +36,11 @@ public EchoController(ILogger logger, IEchoService echoService) [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] [ProducesResponseType(StatusCodes.Status502BadGateway)] - public async Task>> GetAvailableEchoMissions(string? installationCode) + public async Task>> GetAvailableEchoMissions(string? plantCode) { try { - var missions = await _echoService.GetAvailableMissions(installationCode); + var missions = await _echoService.GetAvailableMissions(plantCode); return Ok(missions); } catch (HttpRequestException e) diff --git a/backend/api/Controllers/InstallationController.cs b/backend/api/Controllers/InstallationController.cs index b44853f4c..1ddda7522 100644 --- a/backend/api/Controllers/InstallationController.cs +++ b/backend/api/Controllers/InstallationController.cs @@ -11,7 +11,6 @@ namespace Api.Controllers public class InstallationController : ControllerBase { private readonly IInstallationService _installationService; - private readonly IAssetService _assetService; private readonly IMapService _mapService; @@ -20,14 +19,12 @@ public class InstallationController : ControllerBase public InstallationController( ILogger logger, IMapService mapService, - IInstallationService installationService, - IAssetService assetService + IInstallationService installationService ) { _logger = logger; _mapService = mapService; _installationService = installationService; - _assetService = assetService; } /// @@ -103,12 +100,7 @@ public async Task> Create([FromBody] CreateInstallati _logger.LogInformation("Creating new installation"); try { - var existingAsset = await _assetService.ReadByName(installation.AssetCode); - if (existingAsset == null) - { - return NotFound($"Asset with asset code {installation.AssetCode} not found"); - } - var existingInstallation = await _installationService.ReadByAssetAndName(existingAsset, installation.InstallationCode); + var existingInstallation = await _installationService.ReadByName(installation.InstallationCode); if (existingInstallation != null) { _logger.LogInformation("An installation for given name and installation already exists"); diff --git a/backend/api/Controllers/MissionController.cs b/backend/api/Controllers/MissionController.cs index 1257af8d3..eb1eceb46 100644 --- a/backend/api/Controllers/MissionController.cs +++ b/backend/api/Controllers/MissionController.cs @@ -194,7 +194,7 @@ public async Task> GetMissionDefinitionById([Fro /// [HttpGet] [Authorize(Roles = Role.Any)] - [Route("{assetCode}/{mapName}/map")] + [Route("{installationCode}/{mapName}/map")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] @@ -202,11 +202,11 @@ public async Task> GetMissionDefinitionById([Fro [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] [ProducesResponseType(StatusCodes.Status502BadGateway)] - public async Task> GetMap([FromRoute] string assetCode, string mapName) + public async Task> GetMap([FromRoute] string installationCode, string mapName) { try { - byte[] mapStream = await _mapService.FetchMapImage(mapName, assetCode); + byte[] mapStream = await _mapService.FetchMapImage(mapName, installationCode); return File(mapStream, "image/png"); } catch (Azure.RequestFailedException) @@ -253,7 +253,7 @@ [FromBody] ScheduleMissionQuery scheduledMissionQuery t => { var tagPosition = _stidService - .GetTagPosition(t.TagId, missionDefinition.AssetCode) + .GetTagPosition(t.TagId, missionDefinition.InstallationCode) .Result; return new MissionTask(t, tagPosition); } @@ -276,7 +276,7 @@ [FromBody] ScheduleMissionQuery scheduledMissionQuery Status = MissionStatus.Pending, DesiredStartTime = scheduledMissionQuery.DesiredStartTime, Tasks = missionTasks, - AssetCode = missionDefinition.AssetCode, + InstallationCode = missionDefinition.InstallationCode, Area = missionDefinition.Area, MapMetadata = new MapMetadata() }; @@ -353,19 +353,19 @@ [FromBody] ScheduledMissionQuery scheduledMissionQuery t => { var tagPosition = _stidService - .GetTagPosition(t.TagId, scheduledMissionQuery.AssetCode) + .GetTagPosition(t.TagId, scheduledMissionQuery.InstallationCode) .Result; return new MissionTask(t, tagPosition); } ) .ToList(); - var area = await _areaService.ReadByAssetAndName(scheduledMissionQuery.AssetCode, scheduledMissionQuery.AreaName); + var area = await _areaService.ReadByInstallationAndName(scheduledMissionQuery.InstallationCode, scheduledMissionQuery.AreaName); if (area == null) { // This is disabled for now as the Area database is not yet populated - //return NotFound($"Could not find area with name {scheduledMissionQuery.AreaName} in asset {scheduledMissionQuery.AssetCode}"); + //return NotFound($"Could not find area with name {scheduledMissionQuery.AreaName} in installation {scheduledMissionQuery.InstallationCode}"); } // TODO: search for if a source with the given type and URL exists, then reuse it @@ -380,7 +380,7 @@ [FromBody] ScheduledMissionQuery scheduledMissionQuery }, Name = echoMission.Name, InspectionFrequency = scheduledMissionQuery.InspectionFrequency, - AssetCode = scheduledMissionQuery.AssetCode, + InstallationCode = scheduledMissionQuery.InstallationCode, Area = area }; @@ -392,7 +392,7 @@ [FromBody] ScheduledMissionQuery scheduledMissionQuery Status = MissionStatus.Pending, DesiredStartTime = scheduledMissionQuery.DesiredStartTime, Tasks = missionTasks, - AssetCode = scheduledMissionQuery.AssetCode, + InstallationCode = scheduledMissionQuery.InstallationCode, Area = area, MapMetadata = new MapMetadata() }; @@ -434,10 +434,10 @@ [FromBody] CustomMissionQuery customMissionQuery var missionTasks = customMissionQuery.Tasks.Select(task => new MissionTask(task)).ToList(); - var area = await _areaService.ReadByAssetAndName(customMissionQuery.AssetCode, customMissionQuery.AreaName); + var area = await _areaService.ReadByInstallationAndName(customMissionQuery.InstallationCode, customMissionQuery.AreaName); if (area == null) - return NotFound($"Could not find area with name {customMissionQuery.AreaName} in asset {customMissionQuery.AssetCode}"); + return NotFound($"Could not find area with name {customMissionQuery.AreaName} in installation {customMissionQuery.InstallationCode}"); string sourceURL = await _customMissionService.UploadSource(missionTasks); @@ -450,7 +450,7 @@ [FromBody] CustomMissionQuery customMissionQuery }, Name = customMissionQuery.Name, InspectionFrequency = customMissionQuery.InspectionFrequency, - AssetCode = customMissionQuery.AssetCode, + InstallationCode = customMissionQuery.InstallationCode, Area = area }; @@ -464,7 +464,7 @@ [FromBody] CustomMissionQuery customMissionQuery Status = MissionStatus.Pending, DesiredStartTime = customMissionQuery.DesiredStartTime ?? DateTimeOffset.UtcNow, Tasks = missionTasks, - AssetCode = customMissionQuery.AssetCode, + InstallationCode = customMissionQuery.InstallationCode, Area = area, MapMetadata = new MapMetadata() }; diff --git a/backend/api/Controllers/Models/AreaResponse.cs b/backend/api/Controllers/Models/AreaResponse.cs index 9b0a3ad3a..b0d5e2b8b 100644 --- a/backend/api/Controllers/Models/AreaResponse.cs +++ b/backend/api/Controllers/Models/AreaResponse.cs @@ -7,9 +7,9 @@ public class AreaResponse public string DeckName { get; set; } - public string InstallationCode { get; set; } + public string PlantCode { get; set; } - public string AssetCode { get; set; } + public string InstallationCode { get; set; } public string AreaName { get; set; } diff --git a/backend/api/Controllers/Models/CondensedMissionDefinition.cs b/backend/api/Controllers/Models/CondensedMissionDefinition.cs index 15e28f569..65d832226 100644 --- a/backend/api/Controllers/Models/CondensedMissionDefinition.cs +++ b/backend/api/Controllers/Models/CondensedMissionDefinition.cs @@ -7,6 +7,6 @@ public class CondensedMissionDefinition public string Name { get; set; } - public string AssetCode { get; set; } + public string InstallationCode { get; set; } } } diff --git a/backend/api/Controllers/Models/CreateAreaQuery.cs b/backend/api/Controllers/Models/CreateAreaQuery.cs index 2abd7f051..a90ea30f8 100644 --- a/backend/api/Controllers/Models/CreateAreaQuery.cs +++ b/backend/api/Controllers/Models/CreateAreaQuery.cs @@ -4,8 +4,8 @@ namespace Api.Controllers.Models { public struct CreateAreaQuery { - public string AssetCode { get; set; } public string InstallationCode { get; set; } + public string PlantCode { get; set; } public string DeckName { get; set; } public string AreaName { get; set; } diff --git a/backend/api/Controllers/Models/CreateAssetQuery.cs b/backend/api/Controllers/Models/CreateAssetQuery.cs deleted file mode 100644 index 9341141b8..000000000 --- a/backend/api/Controllers/Models/CreateAssetQuery.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Api.Controllers.Models -{ - public struct CreateAssetQuery - { - public string AssetCode { get; set; } - public string Name { get; set; } - } -} diff --git a/backend/api/Controllers/Models/CreateDeckQuery.cs b/backend/api/Controllers/Models/CreateDeckQuery.cs index 235f5cf08..284e0584b 100644 --- a/backend/api/Controllers/Models/CreateDeckQuery.cs +++ b/backend/api/Controllers/Models/CreateDeckQuery.cs @@ -2,8 +2,8 @@ { public struct CreateDeckQuery { - public string AssetCode { get; set; } public string InstallationCode { get; set; } + public string PlantCode { get; set; } public string Name { get; set; } } } diff --git a/backend/api/Controllers/Models/CreateInstallationQuery.cs b/backend/api/Controllers/Models/CreateInstallationQuery.cs index 4bab44759..e28214ab9 100644 --- a/backend/api/Controllers/Models/CreateInstallationQuery.cs +++ b/backend/api/Controllers/Models/CreateInstallationQuery.cs @@ -2,7 +2,6 @@ { public struct CreateInstallationQuery { - public string AssetCode { get; set; } public string InstallationCode { get; set; } public string Name { get; set; } } diff --git a/backend/api/Controllers/Models/CreatePlantQuery.cs b/backend/api/Controllers/Models/CreatePlantQuery.cs new file mode 100644 index 000000000..7812c7793 --- /dev/null +++ b/backend/api/Controllers/Models/CreatePlantQuery.cs @@ -0,0 +1,9 @@ +namespace Api.Controllers.Models +{ + public struct CreatePlantQuery + { + public string InstallationCode { get; set; } + public string PlantCode { get; set; } + public string Name { get; set; } + } +} diff --git a/backend/api/Controllers/Models/CreateRobotQuery.cs b/backend/api/Controllers/Models/CreateRobotQuery.cs index 5675176e1..b0ce047df 100644 --- a/backend/api/Controllers/Models/CreateRobotQuery.cs +++ b/backend/api/Controllers/Models/CreateRobotQuery.cs @@ -12,7 +12,7 @@ public struct CreateRobotQuery public string SerialNumber { get; set; } - public string CurrentAsset { get; set; } + public string CurrentInstallation { get; set; } public Area CurrentArea { get; set; } diff --git a/backend/api/Controllers/Models/CustomMissionQuery.cs b/backend/api/Controllers/Models/CustomMissionQuery.cs index 0e7fc7fad..06227546b 100644 --- a/backend/api/Controllers/Models/CustomMissionQuery.cs +++ b/backend/api/Controllers/Models/CustomMissionQuery.cs @@ -32,7 +32,7 @@ public struct CustomMissionQuery public DateTimeOffset? DesiredStartTime { get; set; } - public string AssetCode { get; set; } + public string InstallationCode { get; set; } public TimeSpan? InspectionFrequency { get; set; } diff --git a/backend/api/Controllers/Models/EchoMission.cs b/backend/api/Controllers/Models/EchoMission.cs index 9c4d053a3..2b8a27765 100644 --- a/backend/api/Controllers/Models/EchoMission.cs +++ b/backend/api/Controllers/Models/EchoMission.cs @@ -7,7 +7,7 @@ public class EchoMission public string Name { get; set; } - public string AssetCode { get; set; } + public string InstallationCode { get; set; } public Uri URL { get; set; } diff --git a/backend/api/Controllers/Models/EchoPlantInfo.cs b/backend/api/Controllers/Models/EchoPlantInfo.cs index c03366abc..9e955004d 100644 --- a/backend/api/Controllers/Models/EchoPlantInfo.cs +++ b/backend/api/Controllers/Models/EchoPlantInfo.cs @@ -3,7 +3,7 @@ namespace Api.Controllers.Models { public class EchoPlantInfo { - public string InstallationCode { get; set; } + public string PlantCode { get; set; } public string ProjectDescription { get; set; } } } diff --git a/backend/api/Controllers/Models/MissionDefinitionQueryStringParameters.cs b/backend/api/Controllers/Models/MissionDefinitionQueryStringParameters.cs index 7bcfb995d..70f5da587 100644 --- a/backend/api/Controllers/Models/MissionDefinitionQueryStringParameters.cs +++ b/backend/api/Controllers/Models/MissionDefinitionQueryStringParameters.cs @@ -11,9 +11,9 @@ public MissionDefinitionQueryStringParameters() } /// - /// Filter for the asset code of the mission + /// Filter for the installation code of the mission /// - public string? AssetCode { get; set; } + public string? InstallationCode { get; set; } /// /// Filter for the area of the mission diff --git a/backend/api/Controllers/Models/MissionRunQueryStringParameters.cs b/backend/api/Controllers/Models/MissionRunQueryStringParameters.cs index 3a69e63d9..dbff7e22a 100644 --- a/backend/api/Controllers/Models/MissionRunQueryStringParameters.cs +++ b/backend/api/Controllers/Models/MissionRunQueryStringParameters.cs @@ -16,9 +16,9 @@ public MissionRunQueryStringParameters() public List? Statuses { get; set; } /// - /// Filter for the asset code of the mission + /// Filter for the installation code of the mission /// - public string? AssetCode { get; set; } + public string? InstallationCode { get; set; } /// /// Filter for the area of the mission diff --git a/backend/api/Controllers/Models/ScheduledMissionQuery.cs b/backend/api/Controllers/Models/ScheduledMissionQuery.cs index 4e34ae1c3..52e580984 100644 --- a/backend/api/Controllers/Models/ScheduledMissionQuery.cs +++ b/backend/api/Controllers/Models/ScheduledMissionQuery.cs @@ -5,7 +5,7 @@ public struct ScheduledMissionQuery public string RobotId { get; set; } public int EchoMissionId { get; set; } public DateTimeOffset DesiredStartTime { get; set; } - public string AssetCode { get; set; } + public string InstallationCode { get; set; } public string AreaName { get; set; } public TimeSpan? InspectionFrequency { get; set; } } diff --git a/backend/api/Controllers/PlantController.cs b/backend/api/Controllers/PlantController.cs new file mode 100644 index 000000000..b836b8dd9 --- /dev/null +++ b/backend/api/Controllers/PlantController.cs @@ -0,0 +1,155 @@ +using Api.Controllers.Models; +using Api.Database.Models; +using Api.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Api.Controllers +{ + [ApiController] + [Route("plants")] + public class PlantController : ControllerBase + { + private readonly IPlantService _plantService; + private readonly IInstallationService _installationService; + + private readonly IMapService _mapService; + + private readonly ILogger _logger; + + public PlantController( + ILogger logger, + IMapService mapService, + IPlantService plantService, + IInstallationService installationService + ) + { + _logger = logger; + _mapService = mapService; + _plantService = plantService; + _installationService = installationService; + } + + /// + /// List all plants in the Flotilla database + /// + /// + /// This query gets all plants + /// + [HttpGet] + [Authorize(Roles = Role.Any)] + [ProducesResponseType(typeof(IList), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task>> GetPlants() + { + try + { + var plants = await _plantService.ReadAll(); + return Ok(plants); + } + catch (Exception e) + { + _logger.LogError(e, "Error during GET of plants from database"); + throw; + } + } + + /// + /// Lookup plant by specified id. + /// + [HttpGet] + [Authorize(Roles = Role.Any)] + [Route("{id}")] + [ProducesResponseType(typeof(Plant), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task> GetPlantById([FromRoute] string id) + { + try + { + var plant = await _plantService.ReadById(id); + if (plant == null) + return NotFound($"Could not find plant with id {id}"); + return Ok(plant); + } + catch (Exception e) + { + _logger.LogError(e, "Error during GET of plant from database"); + throw; + } + + } + + /// + /// Add a new plant + /// + /// + /// This query adds a new plant to the database + /// + [HttpPost] + [Authorize(Roles = Role.Admin)] + [ProducesResponseType(typeof(Plant), StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task> Create([FromBody] CreatePlantQuery plant) + { + _logger.LogInformation("Creating new plant"); + try + { + var existingInstallation = await _installationService.ReadByName(plant.InstallationCode); + if (existingInstallation == null) + { + return NotFound($"Installation with installation code {plant.InstallationCode} not found"); + } + var existingPlant = await _plantService.ReadByInstallationAndName(existingInstallation, plant.PlantCode); + if (existingPlant != null) + { + _logger.LogInformation("An plant for given name and plant already exists"); + return BadRequest($"Plant already exists"); + } + + var newPlant = await _plantService.Create(plant); + _logger.LogInformation( + "Succesfully created new plant with id '{plantId}'", + newPlant.Id + ); + return CreatedAtAction( + nameof(GetPlantById), + new { id = newPlant.Id }, + newPlant + ); + } + catch (Exception e) + { + _logger.LogError(e, "Error while creating new plant"); + throw; + } + } + + /// + /// Deletes the plant with the specified id from the database. + /// + [HttpDelete] + [Authorize(Roles = Role.Admin)] + [Route("{id}")] + [ProducesResponseType(typeof(Plant), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task> DeletePlant([FromRoute] string id) + { + var plant = await _plantService.Delete(id); + if (plant is null) + return NotFound($"Plant with id {id} not found"); + return Ok(plant); + } + } +} diff --git a/backend/api/Controllers/RobotController.cs b/backend/api/Controllers/RobotController.cs index 91ae652ea..e1131aef8 100644 --- a/backend/api/Controllers/RobotController.cs +++ b/backend/api/Controllers/RobotController.cs @@ -38,7 +38,7 @@ IAreaService areaService } /// - /// List all robots on the asset. + /// List all robots on the installation. /// /// /// This query gets all robots @@ -683,8 +683,8 @@ [FromBody] ScheduleLocalizationMissionQuery scheduleLocalizationMissionQuery { Name = "Localization Mission", Robot = robot, + InstallationCode = "NA", Area = area, - AssetCode = "NA", Status = MissionStatus.Pending, DesiredStartTime = DateTimeOffset.UtcNow, Tasks = new List(), @@ -734,7 +734,7 @@ [FromBody] ScheduleLocalizationMissionQuery scheduleLocalizationMissionQuery /// This query starts a localization for a given robot /// [HttpPost] - [Route("{robotId}/{asset}/{areaName}/go-to-safe-position")] + [Route("{robotId}/{installation}/{areaName}/go-to-safe-position")] [Authorize(Roles = Role.User)] [ProducesResponseType(typeof(MissionRun), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] @@ -744,7 +744,7 @@ [FromBody] ScheduleLocalizationMissionQuery scheduleLocalizationMissionQuery [ProducesResponseType(StatusCodes.Status500InternalServerError)] public async Task> SendRobotToSafePosition( [FromRoute] string robotId, - [FromRoute] string asset, + [FromRoute] string installation, [FromRoute] string areaName ) { @@ -755,15 +755,15 @@ [FromRoute] string areaName return NotFound("Robot not found"); } - var assets = await _areaService.ReadByAsset(asset); + var installations = await _areaService.ReadByInstallation(installation); - if (!assets.Any()) + if (!installations.Any()) { - _logger.LogWarning("Could not find asset={asset}", asset); - return NotFound("No asset found"); + _logger.LogWarning("Could not find installation={installation}", installation); + return NotFound("No installation found"); } - var area = await _areaService.ReadByAssetAndName(asset, areaName); + var area = await _areaService.ReadByInstallationAndName(installation, areaName); if (area is null) { _logger.LogWarning("Could not find area={areaName}", areaName); @@ -772,7 +772,7 @@ [FromRoute] string areaName if (area.SafePositions.Count < 1) { - _logger.LogWarning("No safe position for asset={asset}, area={areaName}", asset, areaName); + _logger.LogWarning("No safe position for installation={installation}, area={areaName}", installation, areaName); return NotFound("No safe positions found"); } @@ -812,7 +812,7 @@ [FromRoute] string areaName { Name = "Drive to Safe Position", Robot = robot, - AssetCode = asset, + InstallationCode = installation, Area = area, Status = MissionStatus.Pending, DesiredStartTime = DateTimeOffset.UtcNow, diff --git a/backend/api/Controllers/RobotModelController.cs b/backend/api/Controllers/RobotModelController.cs index b7ae325c3..8a749cb69 100644 --- a/backend/api/Controllers/RobotModelController.cs +++ b/backend/api/Controllers/RobotModelController.cs @@ -213,7 +213,7 @@ public async Task> DeleteRobotModel([FromRoute] string { var robotModel = await _robotModelService.Delete(id); if (robotModel is null) - return NotFound($"Asset deck with id {id} not found"); + return NotFound($"Area with id {id} not found"); return Ok(robotModel); } diff --git a/backend/api/Database/Context/FlotillaDbContext.cs b/backend/api/Database/Context/FlotillaDbContext.cs index 5d72c5d07..d6c958f0b 100644 --- a/backend/api/Database/Context/FlotillaDbContext.cs +++ b/backend/api/Database/Context/FlotillaDbContext.cs @@ -11,10 +11,12 @@ public class FlotillaDbContext : DbContext public DbSet RobotModels => Set(); public DbSet MissionRuns => Set(); public DbSet MissionDefinitions => Set(); + public DbSet Plants => Set(); public DbSet Installations => Set(); - public DbSet Assets => Set(); public DbSet Decks => Set(); + public DbSet AssetDecks => Set(); public DbSet Areas => Set(); + public DbSet Sources => Set(); public DbSet SafePositions => Set(); public FlotillaDbContext(DbContextOptions options) : base(options) { } @@ -59,6 +61,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) } ); + modelBuilder.Entity().HasOne(m => m.Source); modelBuilder.Entity().OwnsOne(m => m.MapMetadata).OwnsOne(t => t.TransformationMatrices); modelBuilder.Entity().OwnsOne(m => m.MapMetadata).OwnsOne(b => b.Boundary); //modelBuilder.Entity().HasOne(m => m.LastRun).WithOne(m => m.MissionDefinition).HasForeignKey(m => m.Id); @@ -71,8 +74,11 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) poseBuilder.OwnsOne(pose => pose.Orientation); }); modelBuilder.Entity().HasOne(a => a.Deck); - modelBuilder.Entity().HasOne(a => a.Installation); - modelBuilder.Entity().HasOne(a => a.Asset); + modelBuilder.Entity().HasOne(a => a.Installation); + modelBuilder.Entity().HasOne(a => a.Plant); + modelBuilder.Entity().HasOne(d => d.Plant); + modelBuilder.Entity().HasOne(d => d.Installation); + modelBuilder.Entity().HasOne(a => a.Installation); modelBuilder.Entity().OwnsOne(s => s.Pose, poseBuilder => { @@ -83,9 +89,9 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) // There can only be one robot model per robot type modelBuilder.Entity().HasIndex(model => model.Type).IsUnique(); - // There can only be one unique asset and installation shortname - modelBuilder.Entity().HasIndex(a => new { a.AssetCode }).IsUnique(); + // There can only be one unique installation and plant shortname modelBuilder.Entity().HasIndex(a => new { a.InstallationCode }).IsUnique(); + modelBuilder.Entity().HasIndex(a => new { a.PlantCode }).IsUnique(); } // SQLite does not have proper support for DateTimeOffset via Entity Framework Core, see the limitations diff --git a/backend/api/Database/Context/InitDb.cs b/backend/api/Database/Context/InitDb.cs index 42c0ed231..4a944b66e 100644 --- a/backend/api/Database/Context/InitDb.cs +++ b/backend/api/Database/Context/InitDb.cs @@ -5,8 +5,8 @@ namespace Api.Database.Context; public static class InitDb { private static readonly List robots = GetRobots(); - private static readonly List assets = GetAssets(); private static readonly List installations = GetInstallations(); + private static readonly List plants = GetPlants(); private static readonly List decks = GetDecks(); private static readonly List areas = GetAreas(); private static readonly List sources = GetSources(); @@ -34,73 +34,29 @@ public static class InitDb RobotPose = new Pose() }; - private static List GetRobots() - { - var robot1 = new Robot - { - IsarId = "c68b679d-308b-460f-9fe0-87eaadbd8a6e", - Name = "R2-D2", - SerialNumber = "D2", - Status = RobotStatus.Available, - Enabled = true, - Host = "localhost", - Port = 3000, - VideoStreams = new List(), - Pose = new Pose() - }; - - var robot2 = new Robot - { - Name = "Shockwave", - IsarId = "c68b679d-308b-460f-9fe0-87eaadbd1234", - SerialNumber = "SS79", - Status = RobotStatus.Busy, - Enabled = true, - Host = "localhost", - Port = 3000, - VideoStreams = new List(), - Pose = new Pose() - }; - - var robot3 = new Robot - { - Name = "Ultron", - IsarId = "c68b679d-308b-460f-9fe0-87eaadbd5678", - SerialNumber = "Earth616", - Status = RobotStatus.Available, - Enabled = false, - Host = "localhost", - Port = 3000, - VideoStreams = new List(), - Pose = new Pose() - }; - - return new List(new Robot[] { robot1, robot2, robot3 }); - } - - private static List GetAssets() + private static List GetInstallations() { - var asset1 = new Asset + var installation1 = new Installation { Id = Guid.NewGuid().ToString(), Name = "Johan Sverdrup", - AssetCode = "JSV" + InstallationCode = "JSV" }; - return new List(new Asset[] { asset1 }); + return new List(new Installation[] { installation1 }); } - private static List GetInstallations() + private static List GetPlants() { - var installation1 = new Installation + var plant1 = new Plant { Id = Guid.NewGuid().ToString(), - Asset = assets[0], + Installation = installations[0], Name = "Johan Sverdrup - P1", - InstallationCode = "P1" + PlantCode = "P1" }; - return new List(new Installation[] { installation1 }); + return new List(new Plant[] { plant1 }); } private static List GetDecks() @@ -108,8 +64,8 @@ private static List GetDecks() var deck1 = new Deck { Id = Guid.NewGuid().ToString(), - Installation = installations[0], - Asset = installations[0].Asset, + Plant = plants[0], + Installation = plants[0].Installation, Name = "TestDeck" }; @@ -122,8 +78,8 @@ private static List GetAreas() { Id = Guid.NewGuid().ToString(), Deck = decks[0], - Installation = decks[0].Installation, - Asset = decks[0].Installation.Asset, + Plant = decks[0].Plant, + Installation = decks[0].Plant.Installation, Name = "AP320", MapMetadata = new MapMetadata(), DefaultLocalizationPose = new Pose { }, @@ -134,8 +90,8 @@ private static List GetAreas() { Id = Guid.NewGuid().ToString(), Deck = decks[0], - Installation = decks[0].Installation, - Asset = decks[0].Installation.Asset, + Plant = decks[0].Plant, + Installation = decks[0].Plant.Installation, Name = "AP330", MapMetadata = new MapMetadata(), DefaultLocalizationPose = new Pose { }, @@ -146,8 +102,8 @@ private static List GetAreas() { Id = "TestId", Deck = decks[0], - Installation = decks[0].Installation, - Asset = decks[0].Installation.Asset, + Plant = decks[0].Plant, + Installation = decks[0].Plant.Installation, Name = "testArea", MapMetadata = new MapMetadata(), DefaultLocalizationPose = new Pose { }, @@ -174,13 +130,60 @@ private static List GetSources() return new List(new Source[] { source1, source2 }); } + private static List GetRobots() + { + var robot1 = new Robot + { + IsarId = "c68b679d-308b-460f-9fe0-87eaadbd8a6e", + Name = "R2-D2", + SerialNumber = "D2", + Status = RobotStatus.Available, + Enabled = true, + Host = "localhost", + Port = 3000, + CurrentInstallation = "JSV", + VideoStreams = new List(), + Pose = new Pose() + }; + + var robot2 = new Robot + { + Name = "Shockwave", + IsarId = "c68b679d-308b-460f-9fe0-87eaadbd1234", + SerialNumber = "SS79", + Status = RobotStatus.Busy, + Enabled = true, + Host = "localhost", + Port = 3000, + CurrentInstallation = "JSV", + VideoStreams = new List(), + Pose = new Pose() + }; + + var robot3 = new Robot + { + Name = "Ultron", + IsarId = "c68b679d-308b-460f-9fe0-87eaadbd5678", + SerialNumber = "Earth616", + Status = RobotStatus.Available, + Enabled = false, + Host = "localhost", + Port = 3000, + CurrentInstallation = "JSV", + VideoStreams = new List(), + Pose = new Pose() + }; + + return new List(new Robot[] { robot1, robot2, robot3 }); + } + private static List GetMissionDefinitions() { var missionDefinition1 = new MissionDefinition { Id = Guid.NewGuid().ToString(), Name = "Placeholder Mission 1", - AssetCode = areas[0].Deck.Installation.Asset.AssetCode, + InstallationCode = areas[0].Deck.Plant.Installation.InstallationCode, Area = areas[0], Source = sources[0], Comment = "Interesting comment", @@ -192,7 +195,7 @@ private static List GetMissionDefinitions() { Id = Guid.NewGuid().ToString(), Name = "Placeholder Mission 2", - AssetCode = areas[1].Deck.Installation.Asset.AssetCode, + InstallationCode = areas[1].Deck.Plant.Installation.InstallationCode, Area = areas[1], Source = sources[1], InspectionFrequency = new DateTime().AddDays(7) - new DateTime(), @@ -203,7 +206,7 @@ private static List GetMissionDefinitions() { Id = Guid.NewGuid().ToString(), Name = "Placeholder Mission 3", - AssetCode = areas[1].Deck.Installation.Asset.AssetCode, + InstallationCode = areas[1].Deck.Plant.Installation.InstallationCode, Area = areas[1], Source = sources[1], LastRun = null @@ -218,7 +221,7 @@ private static List GetMissionRuns() { Name = "Placeholder Mission 1", Robot = robots[0], - AssetCode = areas[0].Deck.Installation.Asset.AssetCode, + InstallationCode = areas[0].Deck.Plant.Installation.InstallationCode, Area = areas[0], MissionId = missionDefinitions[0].Id, Status = MissionStatus.Successful, @@ -231,7 +234,7 @@ private static List GetMissionRuns() { Name = "Placeholder Mission 2", Robot = robots[1], - AssetCode = areas[1].Deck.Installation.Asset.AssetCode, + InstallationCode = areas[1].Deck.Plant.Installation.InstallationCode, Area = areas[1], MissionId = missionDefinitions[0].Id, Status = MissionStatus.Successful, @@ -245,7 +248,7 @@ private static List GetMissionRuns() { Name = "Placeholder Mission 3", Robot = robots[2], - AssetCode = areas[1].Deck.Installation.Asset.AssetCode, + InstallationCode = areas[1].Deck.Plant.Installation.InstallationCode, Area = areas[1], MissionId = missionDefinitions[1].Id, Status = MissionStatus.Successful, diff --git a/backend/api/Database/Models/Area.cs b/backend/api/Database/Models/Area.cs index 5c20757be..029a9c9b4 100644 --- a/backend/api/Database/Models/Area.cs +++ b/backend/api/Database/Models/Area.cs @@ -11,13 +11,13 @@ public class Area public string Id { get; set; } [Required] - public Deck Deck { get; set; } + public virtual Deck Deck { get; set; } [Required] - public Installation Installation { get; set; } + public virtual Plant Plant { get; set; } [Required] - public Asset Asset { get; set; } + public virtual Installation Installation { get; set; } [Required] [MaxLength(200)] diff --git a/backend/api/Database/Models/AssetDeck.cs b/backend/api/Database/Models/AssetDeck.cs new file mode 100644 index 000000000..7cca64621 --- /dev/null +++ b/backend/api/Database/Models/AssetDeck.cs @@ -0,0 +1,26 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +#pragma warning disable CS8618 +namespace Api.Database.Models +{ + public class AssetDeck + { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public string Id { get; set; } + + [Required] + [MaxLength(200)] + public string AssetCode { get; set; } + + [Required] + [MaxLength(200)] + public string DeckName { get; set; } + + //[Required] + //public Pose DefaultLocalizationPose { get; set; } + + //public IList SafePositions { get; set; } + } +} diff --git a/backend/api/Database/Models/Deck.cs b/backend/api/Database/Models/Deck.cs index eb05d5d85..cd7508aa5 100644 --- a/backend/api/Database/Models/Deck.cs +++ b/backend/api/Database/Models/Deck.cs @@ -11,10 +11,10 @@ public class Deck public string Id { get; set; } [Required] - public Installation Installation { get; set; } + public virtual Plant Plant { get; set; } [Required] - public Asset Asset { get; set; } + public virtual Installation Installation { get; set; } [Required] [MaxLength(200)] diff --git a/backend/api/Database/Models/Installation.cs b/backend/api/Database/Models/Installation.cs index 1e0157d9e..b8200464b 100644 --- a/backend/api/Database/Models/Installation.cs +++ b/backend/api/Database/Models/Installation.cs @@ -11,14 +11,11 @@ public class Installation public string Id { get; set; } [Required] - public Asset Asset { get; set; } + [MaxLength(200)] + public string Name { get; set; } [Required] [MaxLength(10)] public string InstallationCode { get; set; } - - [Required] - [MaxLength(200)] - public string Name { get; set; } } } diff --git a/backend/api/Database/Models/MissionDefinition.cs b/backend/api/Database/Models/MissionDefinition.cs index f2136ee72..d7e9a2cc9 100644 --- a/backend/api/Database/Models/MissionDefinition.cs +++ b/backend/api/Database/Models/MissionDefinition.cs @@ -18,7 +18,7 @@ public class MissionDefinition : SortableRecord public string Name { get; set; } [Required] - public string AssetCode { get; set; } + public string InstallationCode { get; set; } [MaxLength(1000)] public string? Comment { get; set; } @@ -29,6 +29,6 @@ public class MissionDefinition : SortableRecord public Area? Area { get; set; } - public bool Deprecated { get; set; } + public bool IsDeprecated { get; set; } } } diff --git a/backend/api/Database/Models/MissionRun.cs b/backend/api/Database/Models/MissionRun.cs index e192ca7ce..dd104464a 100644 --- a/backend/api/Database/Models/MissionRun.cs +++ b/backend/api/Database/Models/MissionRun.cs @@ -35,7 +35,7 @@ public MissionStatus Status [Required] [MaxLength(200)] - public string AssetCode { get; set; } + public string InstallationCode { get; set; } [Required] public DateTimeOffset DesiredStartTime { get; set; } diff --git a/backend/api/Database/Models/Asset.cs b/backend/api/Database/Models/Plant.cs similarity index 73% rename from backend/api/Database/Models/Asset.cs rename to backend/api/Database/Models/Plant.cs index 08c7ceae1..7a7141bb3 100644 --- a/backend/api/Database/Models/Asset.cs +++ b/backend/api/Database/Models/Plant.cs @@ -4,18 +4,21 @@ #pragma warning disable CS8618 namespace Api.Database.Models { - public class Asset + public class Plant { [Key] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public string Id { get; set; } [Required] - [MaxLength(200)] - public string Name { get; set; } + public virtual Installation Installation { get; set; } [Required] [MaxLength(10)] - public string AssetCode { get; set; } + public string PlantCode { get; set; } + + [Required] + [MaxLength(200)] + public string Name { get; set; } } } diff --git a/backend/api/Database/Models/Robot.cs b/backend/api/Database/Models/Robot.cs index 5ebe4cb21..02871e8ef 100644 --- a/backend/api/Database/Models/Robot.cs +++ b/backend/api/Database/Models/Robot.cs @@ -26,7 +26,7 @@ public class Robot [MaxLength(200)] public string SerialNumber { get; set; } - public string CurrentAsset { get; set; } + public string CurrentInstallation { get; set; } public Area? CurrentArea { get; set; } @@ -73,7 +73,7 @@ public Robot() IsarId = "defaultIsarId"; Name = "defaultId"; SerialNumber = "defaultSerialNumber"; - CurrentAsset = "defaultAsset"; + CurrentInstallation = "defaultAsset"; Status = RobotStatus.Offline; Enabled = false; Host = "localhost"; @@ -98,7 +98,7 @@ public Robot(CreateRobotQuery createQuery) IsarId = createQuery.IsarId; Name = createQuery.Name; SerialNumber = createQuery.SerialNumber; - CurrentAsset = createQuery.CurrentAsset; + CurrentInstallation = createQuery.CurrentInstallation; CurrentArea = createQuery.CurrentArea; VideoStreams = videoStreams; Host = createQuery.Host; diff --git a/backend/api/Database/Models/SortableRecord.cs b/backend/api/Database/Models/SortableRecord.cs index bdfc90f67..33127dc6c 100644 --- a/backend/api/Database/Models/SortableRecord.cs +++ b/backend/api/Database/Models/SortableRecord.cs @@ -1,4 +1,4 @@ -#pragma warning disable CS8618 +#pragma warning disable CS8618 namespace Api.Database.Models { public interface SortableRecord diff --git a/backend/api/EventHandlers/MqttEventHandler.cs b/backend/api/EventHandlers/MqttEventHandler.cs index ba1125ff2..b832e4eb0 100644 --- a/backend/api/EventHandlers/MqttEventHandler.cs +++ b/backend/api/EventHandlers/MqttEventHandler.cs @@ -122,7 +122,7 @@ private async void OnIsarRobotInfo(object? sender, MqttReceivedArgs mqttArgs) Name = isarRobotInfo.RobotName, RobotType = isarRobotInfo.RobotType, SerialNumber = isarRobotInfo.SerialNumber, - CurrentAsset = isarRobotInfo.CurrentAsset, + CurrentInstallation = isarRobotInfo.CurrentInstallation, VideoStreams = isarRobotInfo.VideoStreamQueries, Host = isarRobotInfo.Host, Port = isarRobotInfo.Port, @@ -166,9 +166,9 @@ private async void OnIsarRobotInfo(object? sender, MqttReceivedArgs mqttArgs) UpdatePortIfChanged(isarRobotInfo.Port, ref robot, ref updatedFields); - if (isarRobotInfo.CurrentAsset is not null) + if (isarRobotInfo.CurrentInstallation is not null) { - UpdateCurrentAssetIfChanged(isarRobotInfo.CurrentAsset, ref robot, ref updatedFields); + UpdateCurrentInstallationIfChanged(isarRobotInfo.CurrentInstallation, ref robot, ref updatedFields); } if (!updatedFields.IsNullOrEmpty()) @@ -233,16 +233,16 @@ private static void UpdatePortIfChanged(int port, ref Robot robot, robot.Port = port; } - private static void UpdateCurrentAssetIfChanged(string newCurrentAsset, ref Robot robot, + private static void UpdateCurrentInstallationIfChanged(string newCurrentInstallation, ref Robot robot, ref List updatedFields) { - if (newCurrentAsset.Equals(robot.CurrentAsset, StringComparison.Ordinal)) + if (newCurrentInstallation.Equals(robot.CurrentInstallation, StringComparison.Ordinal)) { return; } - updatedFields.Add($"\nCurrentAsset ({robot.CurrentAsset} -> {newCurrentAsset})\n"); - robot.CurrentAsset = newCurrentAsset; + updatedFields.Add($"\nCurrentInstallation ({robot.CurrentInstallation} -> {newCurrentInstallation})\n"); + robot.CurrentInstallation = newCurrentInstallation; } private async void OnMissionUpdate(object? sender, MqttReceivedArgs mqttArgs) diff --git a/backend/api/MQTT/MessageModels/IsarRobotInfo.cs b/backend/api/MQTT/MessageModels/IsarRobotInfo.cs index 4c00c1c79..7884d6176 100644 --- a/backend/api/MQTT/MessageModels/IsarRobotInfo.cs +++ b/backend/api/MQTT/MessageModels/IsarRobotInfo.cs @@ -20,7 +20,7 @@ public class IsarRobotInfoMessage : MqttMessage public string SerialNumber { get; set; } [JsonPropertyName("robot_asset")] - public string CurrentAsset { get; set; } + public string CurrentInstallation { get; set; } [JsonPropertyName("video_streams")] public List VideoStreamQueries { get; set; } diff --git a/backend/api/Program.cs b/backend/api/Program.cs index a64ab199b..c7c4d7da2 100644 --- a/backend/api/Program.cs +++ b/backend/api/Program.cs @@ -48,8 +48,8 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); -builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/backend/api/Services/AreaService.cs b/backend/api/Services/AreaService.cs index 7c451f515..281220d78 100644 --- a/backend/api/Services/AreaService.cs +++ b/backend/api/Services/AreaService.cs @@ -12,9 +12,9 @@ public interface IAreaService public abstract Task ReadById(string id); - public abstract Task> ReadByAsset(string assetCode); + public abstract Task> ReadByInstallation(string installationCode); - public abstract Task ReadByAssetAndName(string assetCode, string areaName); + public abstract Task ReadByInstallationAndName(string installationCode, string areaName); public abstract Task Create(CreateAreaQuery newArea); @@ -22,7 +22,7 @@ public interface IAreaService public abstract Task Update(Area area); - public abstract Task AddSafePosition(string assetCode, string areaName, SafePosition safePosition); + public abstract Task AddSafePosition(string installationCode, string areaName, SafePosition safePosition); public abstract Task Delete(string id); @@ -41,16 +41,16 @@ public interface IAreaService public class AreaService : IAreaService { private readonly FlotillaDbContext _context; - private readonly IAssetService _assetService; private readonly IInstallationService _installationService; + private readonly IPlantService _plantService; private readonly IDeckService _deckService; public AreaService( - FlotillaDbContext context, IAssetService assetService, IInstallationService installationService, IDeckService deckService) + FlotillaDbContext context, IInstallationService installationService, IPlantService plantService, IDeckService deckService) { _context = context; - _assetService = assetService; _installationService = installationService; + _plantService = plantService; _deckService = deckService; } @@ -62,7 +62,7 @@ public async Task> ReadAll() private IQueryable GetAreas() { return _context.Areas.Include(a => a.SafePositions) - .Include(a => a.Deck).Include(d => d.Installation).Include(i => i.Asset); + .Include(a => a.Deck).Include(d => d.Plant).Include(i => i.Installation); } public async Task ReadById(string id) @@ -71,53 +71,53 @@ private IQueryable GetAreas() .FirstOrDefaultAsync(a => a.Id.Equals(id)); } - public async Task ReadByAssetAndName(Asset? asset, string areaName) + public async Task ReadByInstallationAndName(Installation? installation, string areaName) { - if (asset == null) + if (installation == null) return null; return await _context.Areas.Where(a => a.Name.ToLower().Equals(areaName.ToLower()) && - a.Asset.Id.Equals(asset.Id) - ).Include(a => a.SafePositions).Include(a => a.Asset) - .Include(a => a.Installation).Include(a => a.Deck).FirstOrDefaultAsync(); + a.Installation.Id.Equals(installation.Id) + ).Include(a => a.SafePositions).Include(a => a.Installation) + .Include(a => a.Plant).Include(a => a.Deck).FirstOrDefaultAsync(); } - public async Task ReadByAssetAndName(string assetCode, string areaName) + public async Task ReadByInstallationAndName(string installationCode, string areaName) { - var asset = await _assetService.ReadByName(assetCode); - if (asset == null) + var installation = await _installationService.ReadByName(installationCode); + if (installation == null) return null; return await _context.Areas.Where(a => - a.Asset.Id.Equals(asset.Id) && + a.Installation.Id.Equals(installation.Id) && a.Name.ToLower().Equals(areaName.ToLower()) - ).Include(a => a.SafePositions).Include(a => a.Asset) - .Include(a => a.Installation).Include(a => a.Deck).FirstOrDefaultAsync(); + ).Include(a => a.SafePositions).Include(a => a.Installation) + .Include(a => a.Plant).Include(a => a.Deck).FirstOrDefaultAsync(); } - public async Task> ReadByAsset(string assetCode) + public async Task> ReadByInstallation(string installationCode) { - var asset = await _assetService.ReadByName(assetCode); - if (asset == null) + var installation = await _installationService.ReadByName(installationCode); + if (installation == null) return new List(); return await _context.Areas.Where(a => - a.Asset.Id.Equals(asset.Id)).Include(a => a.SafePositions).Include(a => a.Asset) - .Include(a => a.Installation).Include(a => a.Deck).ToListAsync(); + a.Installation.Id.Equals(installation.Id)).Include(a => a.SafePositions).Include(a => a.Installation) + .Include(a => a.Plant).Include(a => a.Deck).ToListAsync(); } - public async Task ReadByAssetAndInstallationAndDeckAndName(Asset? asset, Installation? installation, Deck? deck, string areaName) + public async Task ReadByInstallationAndPlantAndDeckAndName(Installation? installation, Plant? plant, Deck? deck, string areaName) { - if (asset == null || installation == null || deck == null) + if (installation == null || plant == null || deck == null) return null; return await _context.Areas.Where(a => a.Deck.Id.Equals(deck.Id) && + a.Plant.Id.Equals(plant.Id) && a.Installation.Id.Equals(installation.Id) && - a.Asset.Id.Equals(asset.Id) && a.Name.ToLower().Equals(areaName.ToLower()) - ).Include(a => a.Deck).Include(d => d.Installation).Include(i => i.Asset) + ).Include(a => a.Deck).Include(d => d.Plant).Include(i => i.Installation) .Include(a => a.SafePositions).FirstOrDefaultAsync(); } @@ -129,26 +129,26 @@ public async Task Create(CreateAreaQuery newAreaQuery, List position safePositions.Add(new SafePosition(pose)); } - var asset = await _assetService.ReadByName(newAreaQuery.AssetCode); - if (asset == null) + var installation = await _installationService.ReadByName(newAreaQuery.InstallationCode); + if (installation == null) { - throw new AssetNotFoundException($"No asset with name {newAreaQuery.AssetCode} could be found"); + throw new InstallationNotFoundException($"No installation with name {newAreaQuery.InstallationCode} could be found"); } - var installation = await _installationService.ReadByAssetAndName(asset, newAreaQuery.InstallationCode); - if (installation == null) + var plant = await _plantService.ReadByInstallationAndName(installation, newAreaQuery.PlantCode); + if (plant == null) { - throw new InstallationNotFoundException($"No installation with name {newAreaQuery.InstallationCode} could be found"); + throw new PlantNotFoundException($"No plant with name {newAreaQuery.PlantCode} could be found"); } - var deck = await _deckService.ReadByAssetAndInstallationAndName(asset, installation, newAreaQuery.DeckName); + var deck = await _deckService.ReadByInstallationAndPlantAndName(installation, plant, newAreaQuery.DeckName); if (deck == null) { throw new DeckNotFoundException($"No deck with name {newAreaQuery.DeckName} could be found"); } - var existingArea = await ReadByAssetAndInstallationAndDeckAndName( - asset, installation, deck, newAreaQuery.AreaName); + var existingArea = await ReadByInstallationAndPlantAndDeckAndName( + installation, plant, deck, newAreaQuery.AreaName); if (existingArea != null) { throw new AreaNotFoundException($"No area with name {newAreaQuery.AreaName} could be found"); @@ -161,8 +161,8 @@ public async Task Create(CreateAreaQuery newAreaQuery, List position SafePositions = safePositions, MapMetadata = new MapMetadata(), Deck = deck, - Installation = installation, - Asset = asset + Plant = plant, + Installation = installation }; await _context.Areas.AddAsync(newArea); @@ -176,9 +176,9 @@ public async Task Create(CreateAreaQuery newArea) return area; } - public async Task AddSafePosition(string assetCode, string areaName, SafePosition safePosition) + public async Task AddSafePosition(string installationCode, string areaName, SafePosition safePosition) { - var area = await ReadByAssetAndName(assetCode, areaName); + var area = await ReadByInstallationAndName(installationCode, areaName); if (area is null) { return null; diff --git a/backend/api/Services/AssetService.cs b/backend/api/Services/AssetService.cs deleted file mode 100644 index 8cbd6930e..000000000 --- a/backend/api/Services/AssetService.cs +++ /dev/null @@ -1,107 +0,0 @@ -using Api.Controllers.Models; -using Api.Database.Context; -using Api.Database.Models; -using Microsoft.EntityFrameworkCore; - -namespace Api.Services -{ - public interface IAssetService - { - public abstract Task> ReadAll(); - - public abstract Task ReadById(string id); - - public abstract Task ReadByName(string asset); - - public abstract Task Create(CreateAssetQuery newAsset); - - public abstract Task Update(Asset asset); - - public abstract Task Delete(string id); - - } - - [System.Diagnostics.CodeAnalysis.SuppressMessage( - "Globalization", - "CA1309:Use ordinal StringComparison", - Justification = "EF Core refrains from translating string comparison overloads to SQL" - )] - [System.Diagnostics.CodeAnalysis.SuppressMessage( - "Globalization", - "CA1304:Specify CultureInfo", - Justification = "Entity framework does not support translating culture info to SQL calls" - )] - public class AssetService : IAssetService - { - private readonly FlotillaDbContext _context; - - public AssetService(FlotillaDbContext context) - { - _context = context; - } - - public async Task> ReadAll() - { - return await GetAssets().ToListAsync(); - } - - private IQueryable GetAssets() - { - return _context.Assets; - } - - public async Task ReadById(string id) - { - return await GetAssets() - .FirstOrDefaultAsync(a => a.Id.Equals(id)); - } - - public async Task ReadByName(string assetCode) - { - if (assetCode == null) - return null; - return await _context.Assets.Where(a => - a.AssetCode.ToLower().Equals(assetCode.ToLower()) - ).FirstOrDefaultAsync(); - } - - public async Task Create(CreateAssetQuery newAssetQuery) - { - var asset = await ReadByName(newAssetQuery.AssetCode); - if (asset == null) - { - asset = new Asset - { - Name = newAssetQuery.Name, - AssetCode = newAssetQuery.AssetCode - }; - await _context.Assets.AddAsync(asset); - await _context.SaveChangesAsync(); - } - - return asset!; - } - - public async Task Update(Asset asset) - { - var entry = _context.Update(asset); - await _context.SaveChangesAsync(); - return entry.Entity; - } - - public async Task Delete(string id) - { - var asset = await GetAssets() - .FirstOrDefaultAsync(ev => ev.Id.Equals(id)); - if (asset is null) - { - return null; - } - - _context.Assets.Remove(asset); - await _context.SaveChangesAsync(); - - return asset; - } - } -} diff --git a/backend/api/Services/DeckService.cs b/backend/api/Services/DeckService.cs index fe27d1280..bd75576bb 100644 --- a/backend/api/Services/DeckService.cs +++ b/backend/api/Services/DeckService.cs @@ -12,11 +12,11 @@ public interface IDeckService public abstract Task ReadById(string id); - public abstract Task> ReadByAsset(string assetCode); + public abstract Task> ReadByInstallation(string installationCode); public abstract Task ReadByName(string deckName); - public abstract Task ReadByAssetAndInstallationAndName(Asset asset, Installation installation, string deckName); + public abstract Task ReadByInstallationAndPlantAndName(Installation installation, Plant plant, string deckName); public abstract Task Create(CreateDeckQuery newDeck); @@ -39,14 +39,14 @@ public interface IDeckService public class DeckService : IDeckService { private readonly FlotillaDbContext _context; - private readonly IAssetService _assetService; private readonly IInstallationService _installationService; + private readonly IPlantService _plantService; - public DeckService(FlotillaDbContext context, IAssetService assetService, IInstallationService installationService) + public DeckService(FlotillaDbContext context, IInstallationService installationService, IPlantService plantService) { _context = context; - _assetService = assetService; _installationService = installationService; + _plantService = plantService; } public async Task> ReadAll() @@ -65,13 +65,13 @@ private IQueryable GetDecks() .FirstOrDefaultAsync(a => a.Id.Equals(id)); } - public async Task> ReadByAsset(string assetCode) + public async Task> ReadByInstallation(string installationCode) { - var asset = await _assetService.ReadByName(assetCode); - if (asset == null) + var installation = await _installationService.ReadByName(installationCode); + if (installation == null) return new List(); return await _context.Decks.Where(a => - a.Asset.Id.Equals(asset.Id)).ToListAsync(); + a.Installation.Id.Equals(installation.Id)).ToListAsync(); } public async Task ReadByName(string deckName) @@ -83,35 +83,35 @@ public async Task> ReadByAsset(string assetCode) ).FirstOrDefaultAsync(); } - public async Task ReadByAssetAndInstallationAndName(Asset asset, Installation installation, string name) + public async Task ReadByInstallationAndPlantAndName(Installation installation, Plant plant, string name) { return await _context.Decks.Where(a => + a.Plant.Id.Equals(plant.Id) && a.Installation.Id.Equals(installation.Id) && - a.Asset.Id.Equals(asset.Id) && a.Name.ToLower().Equals(name.ToLower()) - ).Include(d => d.Installation).Include(i => i.Asset).FirstOrDefaultAsync(); + ).Include(d => d.Plant).Include(i => i.Installation).FirstOrDefaultAsync(); } public async Task Create(CreateDeckQuery newDeckQuery) { - var asset = await _assetService.ReadByName(newDeckQuery.AssetCode); - if (asset == null) - { - throw new AssetNotFoundException($"No asset with name {newDeckQuery.AssetCode} could be found"); - } - var installation = await _installationService.ReadByAssetAndName(asset, newDeckQuery.InstallationCode); + var installation = await _installationService.ReadByName(newDeckQuery.InstallationCode); if (installation == null) { throw new InstallationNotFoundException($"No installation with name {newDeckQuery.InstallationCode} could be found"); } - var deck = await ReadByAssetAndInstallationAndName(asset, installation, newDeckQuery.Name); + var plant = await _plantService.ReadByInstallationAndName(installation, newDeckQuery.PlantCode); + if (plant == null) + { + throw new PlantNotFoundException($"No plant with name {newDeckQuery.PlantCode} could be found"); + } + var deck = await ReadByInstallationAndPlantAndName(installation, plant, newDeckQuery.Name); if (deck == null) { deck = new Deck { Name = newDeckQuery.Name, - Asset = asset, - Installation = installation + Installation = installation, + Plant = plant }; await _context.Decks.AddAsync(deck); await _context.SaveChangesAsync(); diff --git a/backend/api/Services/EchoService.cs b/backend/api/Services/EchoService.cs index 6530fce71..4f0d4642e 100644 --- a/backend/api/Services/EchoService.cs +++ b/backend/api/Services/EchoService.cs @@ -183,7 +183,7 @@ private List ProcessAvailableEchoMission(List ProcessAvailableEchoMission(List echoPlantInfoResponse var echoPlantInfo = new EchoPlantInfo() { - InstallationCode = plant.InstallationCode, + PlantCode = plant.InstallationCode, ProjectDescription = plant.ProjectDescription }; diff --git a/backend/api/Services/InstallationService.cs b/backend/api/Services/InstallationService.cs index dda36166a..7ae952d4b 100644 --- a/backend/api/Services/InstallationService.cs +++ b/backend/api/Services/InstallationService.cs @@ -1,7 +1,6 @@ using Api.Controllers.Models; using Api.Database.Context; using Api.Database.Models; -using Api.Utilities; using Microsoft.EntityFrameworkCore; namespace Api.Services @@ -12,11 +11,7 @@ public interface IInstallationService public abstract Task ReadById(string id); - public abstract Task> ReadByAsset(string assetCode); - - public abstract Task ReadByAssetAndName(Asset asset, string installationCode); - - public abstract Task ReadByAssetAndName(string assetCode, string installationCode); + public abstract Task ReadByName(string installation); public abstract Task Create(CreateInstallationQuery newInstallation); @@ -39,12 +34,10 @@ public interface IInstallationService public class InstallationService : IInstallationService { private readonly FlotillaDbContext _context; - private readonly IAssetService _assetService; - public InstallationService(FlotillaDbContext context, IAssetService assetService) + public InstallationService(FlotillaDbContext context) { _context = context; - _assetService = assetService; } public async Task> ReadAll() @@ -54,7 +47,7 @@ public async Task> ReadAll() private IQueryable GetInstallations() { - return _context.Installations.Include(i => i.Asset); + return _context.Installations; } public async Task ReadById(string id) @@ -63,52 +56,29 @@ private IQueryable GetInstallations() .FirstOrDefaultAsync(a => a.Id.Equals(id)); } - public async Task> ReadByAsset(string assetCode) + public async Task ReadByName(string installationCode) { - var asset = await _assetService.ReadByName(assetCode); - if (asset == null) - return new List(); - return await _context.Installations.Where(a => - a.Asset.Id.Equals(asset.Id)).ToListAsync(); - } - - public async Task ReadByAssetAndName(Asset asset, string installationCode) - { - return await _context.Installations.Where(a => - a.InstallationCode.ToLower().Equals(installationCode.ToLower()) && - a.Asset.Id.Equals(asset.Id)).FirstOrDefaultAsync(); - } - - public async Task ReadByAssetAndName(string assetCode, string installationCode) - { - var asset = await _assetService.ReadByName(assetCode); - if (asset == null) + if (installationCode == null) return null; return await _context.Installations.Where(a => - a.Asset.Id.Equals(asset.Id) && a.InstallationCode.ToLower().Equals(installationCode.ToLower()) ).FirstOrDefaultAsync(); } public async Task Create(CreateInstallationQuery newInstallationQuery) { - var asset = await _assetService.ReadByName(newInstallationQuery.AssetCode); - if (asset == null) - { - throw new AssetNotFoundException($"No asset with name {newInstallationQuery.AssetCode} could be found"); - } - var installation = await ReadByAssetAndName(asset, newInstallationQuery.InstallationCode); + var installation = await ReadByName(newInstallationQuery.InstallationCode); if (installation == null) { installation = new Installation { Name = newInstallationQuery.Name, - InstallationCode = newInstallationQuery.InstallationCode, - Asset = asset, + InstallationCode = newInstallationQuery.InstallationCode }; await _context.Installations.AddAsync(installation); await _context.SaveChangesAsync(); } + return installation!; } diff --git a/backend/api/Services/MapService.cs b/backend/api/Services/MapService.cs index 00dc61f59..ce5169433 100644 --- a/backend/api/Services/MapService.cs +++ b/backend/api/Services/MapService.cs @@ -8,8 +8,8 @@ namespace Api.Services { public interface IMapService { - public Task FetchMapImage(string mapName, string assetCode); - public Task ChooseMapFromPositions(IList positions, string assetCode); + public Task FetchMapImage(string mapName, string installationCode); + public Task ChooseMapFromPositions(IList positions, string installationCode); public Task AssignMapToMission(MissionRun mission); } @@ -31,17 +31,17 @@ IBlobService blobService _blobService = blobService; } - public async Task FetchMapImage(string mapName, string assetCode) + public async Task FetchMapImage(string mapName, string installationCode) { - return await _blobService.DownloadBlob(mapName, assetCode, _blobOptions.Value.StorageAccount); + return await _blobService.DownloadBlob(mapName, installationCode, _blobOptions.Value.StorageAccount); } - public async Task ChooseMapFromPositions(IList positions, string assetCode) + public async Task ChooseMapFromPositions(IList positions, string installationCode) { var boundaries = new Dictionary(); var imageSizes = new Dictionary(); - var blobs = _blobService.FetchAllBlobs(assetCode, _blobOptions.Value.StorageAccount); + var blobs = _blobService.FetchAllBlobs(installationCode, _blobOptions.Value.StorageAccount); await foreach (var blob in blobs) { @@ -81,7 +81,7 @@ public async Task AssignMapToMission(MissionRun missionRun) } try { - mapMetadata = await ChooseMapFromPositions(positions, missionRun.AssetCode); + mapMetadata = await ChooseMapFromPositions(positions, missionRun.InstallationCode); } catch (ArgumentOutOfRangeException) { diff --git a/backend/api/Services/MissionDefinitionService.cs b/backend/api/Services/MissionDefinitionService.cs index eb8db5be5..fec3af743 100644 --- a/backend/api/Services/MissionDefinitionService.cs +++ b/backend/api/Services/MissionDefinitionService.cs @@ -58,21 +58,21 @@ private IQueryable GetMissionDefinitionsWithSubModels() return _context.MissionDefinitions .Include(missionDefinition => missionDefinition.Area) .ThenInclude(area => area.Deck) + .ThenInclude(area => area.Plant) .ThenInclude(area => area.Installation) - .ThenInclude(area => area.Asset) .Include(missionDefinition => missionDefinition.Source) .Include(missionDefinition => missionDefinition.LastRun); } public async Task ReadById(string id) { - return await GetMissionDefinitionsWithSubModels().Where(m => m.Deprecated == false) + return await GetMissionDefinitionsWithSubModels().Where(m => m.IsDeprecated == false) .FirstOrDefaultAsync(missionDefinition => missionDefinition.Id.Equals(id)); } public async Task> ReadAll(MissionDefinitionQueryStringParameters parameters) { - var query = GetMissionDefinitionsWithSubModels().Where(m => m.Deprecated == false); + var query = GetMissionDefinitionsWithSubModels().Where(m => m.IsDeprecated == false); var filter = ConstructFilter(parameters); query = query.Where(filter); @@ -104,7 +104,7 @@ public async Task Update(MissionDefinition missionDefinition) return null; } - missionDefinition.Deprecated = true; + missionDefinition.IsDeprecated = true; await _context.SaveChangesAsync(); return missionDefinition; @@ -122,7 +122,7 @@ private static void SearchByName(ref IQueryable missionDefini } /// - /// Filters by and + /// Filters by and /// /// Uses LINQ Expression trees (see ) /// @@ -136,10 +136,10 @@ MissionDefinitionQueryStringParameters parameters : missionDefinition => missionDefinition.Area.Name.ToLower().Equals(parameters.Area.Trim().ToLower()); - Expression> assetFilter = parameters.AssetCode is null + Expression> installationFilter = parameters.InstallationCode is null ? missionDefinition => true : missionDefinition => - missionDefinition.AssetCode.ToLower().Equals(parameters.AssetCode.Trim().ToLower()); + missionDefinition.InstallationCode.ToLower().Equals(parameters.InstallationCode.Trim().ToLower()); Expression> missionTypeFilter = parameters.SourceType is null ? missionDefinition => true @@ -151,7 +151,7 @@ MissionDefinitionQueryStringParameters parameters // Combining the body of the filters to create the combined filter, using invoke to force parameter substitution Expression body = Expression.AndAlso( - Expression.Invoke(assetFilter, missionRunExpression), + Expression.Invoke(installationFilter, missionRunExpression), Expression.AndAlso( Expression.Invoke(areaFilter, missionRunExpression), Expression.Invoke(missionTypeFilter, missionRunExpression) diff --git a/backend/api/Services/MissionRunService.cs b/backend/api/Services/MissionRunService.cs index 659b92110..9a3b269aa 100644 --- a/backend/api/Services/MissionRunService.cs +++ b/backend/api/Services/MissionRunService.cs @@ -72,8 +72,8 @@ private IQueryable GetMissionRunsWithSubModels() return _context.MissionRuns .Include(missionRun => missionRun.Area) .ThenInclude(area => area.Deck) - .ThenInclude(deck => deck.Installation) - .ThenInclude(installation => installation.Asset) + .ThenInclude(deck => deck.Plant) + .ThenInclude(plant => plant.Installation) .Include(missionRun => missionRun.Robot) .ThenInclude(robot => robot.VideoStreams) .Include(missionRun => missionRun.Robot) @@ -309,7 +309,7 @@ private static void SearchByTag(ref IQueryable missionRuns, string? } /// - /// Filters by and + /// Filters by and /// /// Uses LINQ Expression trees (see ) /// @@ -323,10 +323,10 @@ MissionRunQueryStringParameters parameters : missionRun => missionRun.Area.Name.ToLower().Equals(parameters.Area.Trim().ToLower()); - Expression> assetFilter = parameters.AssetCode is null + Expression> installationFilter = parameters.InstallationCode is null ? missionRun => true : missionRun => - missionRun.AssetCode.ToLower().Equals(parameters.AssetCode.Trim().ToLower()); + missionRun.InstallationCode.ToLower().Equals(parameters.InstallationCode.Trim().ToLower()); Expression> statusFilter = parameters.Statuses is null ? mission => true @@ -382,7 +382,7 @@ MissionRunQueryStringParameters parameters // Combining the body of the filters to create the combined filter, using invoke to force parameter substitution Expression body = Expression.AndAlso( - Expression.Invoke(assetFilter, missionRun), + Expression.Invoke(installationFilter, missionRun), Expression.AndAlso( Expression.Invoke(statusFilter, missionRun), Expression.AndAlso( diff --git a/backend/api/Services/Models/IsarMissionDefinition.cs b/backend/api/Services/Models/IsarMissionDefinition.cs index 3d3232798..023ef8dca 100644 --- a/backend/api/Services/Models/IsarMissionDefinition.cs +++ b/backend/api/Services/Models/IsarMissionDefinition.cs @@ -98,7 +98,7 @@ public IsarInspectionDefinition(Inspection inspection, MissionTask task, Mission { "map", missionRun.MapMetadata?.MapName }, { "description", missionRun.Description }, { "estimated_duration", missionRun.EstimatedDuration.ToString() }, - { "asset_code", missionRun.AssetCode }, + { "asset_code", missionRun.InstallationCode }, { "mission_name", missionRun.Name }, { "status_reason", missionRun.StatusReason } }; diff --git a/backend/api/Services/PlantService.cs b/backend/api/Services/PlantService.cs new file mode 100644 index 000000000..c9ff13d0a --- /dev/null +++ b/backend/api/Services/PlantService.cs @@ -0,0 +1,137 @@ +using Api.Controllers.Models; +using Api.Database.Context; +using Api.Database.Models; +using Api.Utilities; +using Microsoft.EntityFrameworkCore; + +namespace Api.Services +{ + public interface IPlantService + { + public abstract Task> ReadAll(); + + public abstract Task ReadById(string id); + + public abstract Task> ReadByInstallation(string installationCode); + + public abstract Task ReadByInstallationAndName(Installation installation, string plantCode); + + public abstract Task ReadByInstallationAndName(string installationCode, string plantCode); + + public abstract Task Create(CreatePlantQuery newPlant); + + public abstract Task Update(Plant plant); + + public abstract Task Delete(string id); + + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage( + "Globalization", + "CA1309:Use ordinal StringComparison", + Justification = "EF Core refrains from translating string comparison overloads to SQL" + )] + [System.Diagnostics.CodeAnalysis.SuppressMessage( + "Globalization", + "CA1304:Specify CultureInfo", + Justification = "Entity framework does not support translating culture info to SQL calls" + )] + public class PlantService : IPlantService + { + private readonly FlotillaDbContext _context; + private readonly IInstallationService _installationService; + + public PlantService(FlotillaDbContext context, IInstallationService installationService) + { + _context = context; + _installationService = installationService; + } + + public async Task> ReadAll() + { + return await GetPlants().ToListAsync(); + } + + private IQueryable GetPlants() + { + return _context.Plants.Include(i => i.Installation); + } + + public async Task ReadById(string id) + { + return await GetPlants() + .FirstOrDefaultAsync(a => a.Id.Equals(id)); + } + + public async Task> ReadByInstallation(string installationCode) + { + var installation = await _installationService.ReadByName(installationCode); + if (installation == null) + return new List(); + return await _context.Plants.Where(a => + a.Installation.Id.Equals(installation.Id)).ToListAsync(); + } + + public async Task ReadByInstallationAndName(Installation installation, string plantCode) + { + return await _context.Plants.Where(a => + a.PlantCode.ToLower().Equals(plantCode.ToLower()) && + a.Installation.Id.Equals(installation.Id)).FirstOrDefaultAsync(); + } + + public async Task ReadByInstallationAndName(string installationCode, string plantCode) + { + var installation = await _installationService.ReadByName(installationCode); + if (installation == null) + return null; + return await _context.Plants.Where(a => + a.Installation.Id.Equals(installation.Id) && + a.PlantCode.ToLower().Equals(plantCode.ToLower()) + ).FirstOrDefaultAsync(); + } + + public async Task Create(CreatePlantQuery newPlantQuery) + { + var installation = await _installationService.ReadByName(newPlantQuery.InstallationCode); + if (installation == null) + { + throw new InstallationNotFoundException($"No installation with name {newPlantQuery.InstallationCode} could be found"); + } + var plant = await ReadByInstallationAndName(installation, newPlantQuery.PlantCode); + if (plant == null) + { + plant = new Plant + { + Name = newPlantQuery.Name, + PlantCode = newPlantQuery.PlantCode, + Installation = installation, + }; + await _context.Plants.AddAsync(plant); + await _context.SaveChangesAsync(); + } + return plant!; + } + + public async Task Update(Plant plant) + { + var entry = _context.Update(plant); + await _context.SaveChangesAsync(); + return entry.Entity; + } + + public async Task Delete(string id) + { + var plant = await GetPlants() + .FirstOrDefaultAsync(ev => ev.Id.Equals(id)); + if (plant is null) + { + return null; + } + + _context.Plants.Remove(plant); + await _context.SaveChangesAsync(); + + return plant; + } + } +} diff --git a/backend/api/Services/SortingService.cs b/backend/api/Services/SortingService.cs index 3303fffe9..1f9c07be8 100644 --- a/backend/api/Services/SortingService.cs +++ b/backend/api/Services/SortingService.cs @@ -1,4 +1,4 @@ -using System.Linq.Dynamic.Core; +using System.Linq.Dynamic.Core; using System.Reflection; using System.Text; using Api.Database.Models; diff --git a/backend/api/Utilities/Exceptions.cs b/backend/api/Utilities/Exceptions.cs index 731b9bf15..2a903f45a 100644 --- a/backend/api/Utilities/Exceptions.cs +++ b/backend/api/Utilities/Exceptions.cs @@ -16,14 +16,14 @@ public class MissionSourceTypeException : Exception public MissionSourceTypeException(string message) : base(message) { } } - public class AssetNotFoundException : Exception + public class InstallationNotFoundException : Exception { - public AssetNotFoundException(string message) : base(message) { } + public InstallationNotFoundException(string message) : base(message) { } } - public class InstallationNotFoundException : Exception + public class PlantNotFoundException : Exception { - public InstallationNotFoundException(string message) : base(message) { } + public PlantNotFoundException(string message) : base(message) { } } public class DeckNotFoundException : Exception diff --git a/frontend/src/api/ApiCaller.tsx b/frontend/src/api/ApiCaller.tsx index 8f308229e..d37889d61 100644 --- a/frontend/src/api/ApiCaller.tsx +++ b/frontend/src/api/ApiCaller.tsx @@ -20,7 +20,7 @@ import { EchoMission } from 'models/EchoMission' /** Implements the request sent to the backend api. */ export class BackendAPICaller { static accessToken: string - static assetCode: string + static installationCode: string /** API is not ready until access token has been set for the first time */ private static async ApiReady() { @@ -139,9 +139,9 @@ export class BackendAPICaller { static async getMissionRuns(parameters: MissionRunQueryParameters): Promise> { let path: string = 'missions/runs?' - // Always filter by currently selected asset - const assetCode: string | null = BackendAPICaller.assetCode - if (assetCode) path = path + 'AssetCode=' + assetCode + '&' + // Always filter by currently selected installation + const installationCode: string | null = BackendAPICaller.installationCode + if (installationCode) path = path + 'InstallationCode=' + installationCode + '&' if (parameters.statuses) { parameters.statuses.forEach((status) => { @@ -194,9 +194,9 @@ export class BackendAPICaller { ): Promise> { let path: string = 'missions/definitions?' - // Always filter by currently selected asset - const assetCode: string | null = BackendAPICaller.assetCode - if (assetCode) path = path + 'AssetCode=' + assetCode + '&' + // Always filter by currently selected installation + const installationCode: string | null = BackendAPICaller.installationCode + if (installationCode) path = path + 'InstallationCode=' + installationCode + '&' if (parameters.area) path = path + 'Area=' + parameters.area + '&' if (parameters.sourceType) path = path + 'SourceType=' + parameters.sourceType + '&' @@ -252,7 +252,7 @@ export class BackendAPICaller { }) return result.content } - static async postMission(echoMissionId: number, robotId: string, assetCode: string | null) { + static async postMission(echoMissionId: number, robotId: string, installationCode: string | null) { const path: string = 'missions' const robots: Robot[] = await BackendAPICaller.getEnabledRobots() const desiredRobot = filterRobots(robots, robotId) @@ -260,7 +260,7 @@ export class BackendAPICaller { robotId: desiredRobot[0].id, echoMissionId: echoMissionId, desiredStartTime: new Date(), - assetCode: assetCode, + installationCode: installationCode, areaName: '', // TODO: we need a way of populating the area database, then including area in MissionDefinition } const result = await BackendAPICaller.POST(path, body).catch((e) => { @@ -315,8 +315,8 @@ export class BackendAPICaller { return BackendAPICaller.postControlMissionRequest(path, robotId) } - static async getMap(assetCode: string, mapName: string): Promise { - const path: string = 'missions/' + assetCode + '/' + mapName + '/map' + static async getMap(installationCode: string, mapName: string): Promise { + const path: string = 'missions/' + installationCode + '/' + mapName + '/map' const url = `${config.BACKEND_URL}/${path}` const headers = { diff --git a/frontend/src/components/Contexts/APIUpdater.tsx b/frontend/src/components/Contexts/APIUpdater.tsx index 0cec0c3aa..fb240a7e6 100644 --- a/frontend/src/components/Contexts/APIUpdater.tsx +++ b/frontend/src/components/Contexts/APIUpdater.tsx @@ -1,6 +1,6 @@ import { BackendAPICaller } from 'api/ApiCaller' import { useContext } from 'react' -import { AssetContext } from './AssetContext' +import { InstallationContext } from './InstallationContext' import { AuthContext } from './AuthProvider' type Props = { @@ -10,8 +10,8 @@ type Props = { // Can't use contexts inside the static class so we need a component to update it export const APIUpdater = (props: Props) => { const accessToken = useContext(AuthContext) - const assetCode = useContext(AssetContext).assetCode + const installationCode = useContext(InstallationContext).installationCode BackendAPICaller.accessToken = accessToken - BackendAPICaller.assetCode = assetCode + BackendAPICaller.installationCode = installationCode return <>{props.children} } diff --git a/frontend/src/components/Contexts/AssetContext.tsx b/frontend/src/components/Contexts/AssetContext.tsx deleted file mode 100644 index 16971abb5..000000000 --- a/frontend/src/components/Contexts/AssetContext.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { createContext, FC, useContext, useState } from 'react' - -interface IAssetContext { - assetCode: string - switchAsset: (selectedAsset: string) => void -} - -interface Props { - children: React.ReactNode -} - -const defaultAsset = { - assetCode: '', - switchAsset: (selectedAsset: string) => {}, -} - -export const AssetContext = createContext(defaultAsset) - -export const AssetProvider: FC = ({ children }) => { - const previousAsset = window.localStorage.getItem('assetString') - const [assetCode, setAsset] = useState(previousAsset || defaultAsset.assetCode) - - const switchAsset = (selectedAsset: string) => { - setAsset(selectedAsset.toLowerCase()) - window.localStorage.setItem('assetString', selectedAsset.toLowerCase()) - } - - return ( - - {children} - - ) -} - -export const useAssetContext = () => useContext(AssetContext) diff --git a/frontend/src/components/Contexts/InstallationContext.tsx b/frontend/src/components/Contexts/InstallationContext.tsx new file mode 100644 index 000000000..bec17c544 --- /dev/null +++ b/frontend/src/components/Contexts/InstallationContext.tsx @@ -0,0 +1,40 @@ +import { createContext, FC, useContext, useState } from 'react' + +interface IInstallationContext { + installationCode: string + switchInstallation: (selectedInstallation: string) => void +} + +interface Props { + children: React.ReactNode +} + +const defaultInstallation = { + installationCode: '', + switchInstallation: (selectedInstallation: string) => {}, +} + +export const InstallationContext = createContext(defaultInstallation) + +export const InstallationProvider: FC = ({ children }) => { + const previousInstallation = window.localStorage.getItem('installationString') + const [installationCode, setInstallation] = useState(previousInstallation || defaultInstallation.installationCode) + + const switchInstallation = (selectedInstallation: string) => { + setInstallation(selectedInstallation.toLowerCase()) + window.localStorage.setItem('installationString', selectedInstallation.toLowerCase()) + } + + return ( + + {children} + + ) +} + +export const useInstallationContext = () => useContext(InstallationContext) diff --git a/frontend/src/components/Header/Header.tsx b/frontend/src/components/Header/Header.tsx index a2325f53b..dc1e6f4f4 100644 --- a/frontend/src/components/Header/Header.tsx +++ b/frontend/src/components/Header/Header.tsx @@ -1,7 +1,7 @@ import { config } from 'config' import { Button, Icon, TopBar, Autocomplete, Typography } from '@equinor/eds-core-react' import { BackendAPICaller } from 'api/ApiCaller' -import { useAssetContext } from 'components/Contexts/AssetContext' +import { useInstallationContext } from 'components/Contexts/InstallationContext' import { EchoPlantInfo } from 'models/EchoMission' import { useEffect, useState } from 'react' import styled from 'styled-components' @@ -51,7 +51,7 @@ export function Header({ page }: { page: string }) { - {AssetPicker(page)} + {InstallationPicker(page)}