diff --git a/backend/README.md b/backend/README.md index d3821fa31..9f9475ae3 100644 --- a/backend/README.md +++ b/backend/README.md @@ -259,7 +259,3 @@ to monitor the backend of our application. We have one application insight instance for each environment. The connection strings for the AI instances are stored in the keyvault. - -## Custom Mission Loaders - -You can create your own mission loader to fetch missions from some external system. The custom mission loader needs to fulfill the [IMissionLoader](api/Services/MissionLoaders/MissionLoaderInterface.cs) interface. If your mission loader is an external API you might need to add it as a downstream API in [Program.cs](api/Program.cs) diff --git a/backend/api.test/Controllers/InspectionAreaControllerTests.cs b/backend/api.test/Controllers/InspectionAreaControllerTests.cs index a8d7a9011..58ff32f67 100644 --- a/backend/api.test/Controllers/InspectionAreaControllerTests.cs +++ b/backend/api.test/Controllers/InspectionAreaControllerTests.cs @@ -92,7 +92,7 @@ public async Task CheckThatInspectionAreaIsCorrectlyCreatedThroughEndpoint() } [Fact] - public async Task CheckThatMissionDefinitionIsCreatedInInspectionAreaWhenSchedulingACustomMissionRun() + public async Task CheckThatMissionDefinitionIsCreatedInInspectionAreaUponCreation() { // Arrange var installation = await DatabaseUtilities.NewInstallation(); @@ -107,27 +107,24 @@ public async Task CheckThatMissionDefinitionIsCreatedInInspectionAreaWhenSchedul inspectionArea.Id ); - var inspection = new CustomInspectionQuery - { - InspectionTarget = new Position(), - InspectionType = InspectionType.Image, - }; - var tasks = new List + var testName = Guid.NewGuid().ToString(); + + var tasks = new List { new() { - Inspection = inspection, TagId = "test", RobotPose = new Pose(), - TaskOrder = 0, + TargetPosition = new Position(), + SensorType = SensorType.Image, + AnalysisTypes = [AnalysisType.Fencilla], + Description = "Test description", }, }; - var missionQuery = new CustomMissionQuery + var missionQuery = new CreateMissionQuery { - RobotId = robot.Id, - CreationTime = DateTime.UtcNow, InstallationCode = installation.InstallationCode, - Name = "TestMission", + Name = testName, Tasks = tasks, }; @@ -139,31 +136,23 @@ public async Task CheckThatMissionDefinitionIsCreatedInInspectionAreaWhenSchedul // Act var missionResponse = await Client.PostAsync( - "/missions/custom", + "/missions/definitions", missionContent, TestContext.Current.CancellationToken ); - var userMissionResponse = await missionResponse.Content.ReadFromJsonAsync( - SerializerOptions, - cancellationToken: TestContext.Current.CancellationToken - ); var inspectionAreaMissionResponse = await Client.GetAsync( $"/inspectionAreas/{inspectionArea.Id}/mission-definitions", TestContext.Current.CancellationToken ); // Assert - var mission = await MissionRunService.ReadById(userMissionResponse!.Id); var missionDefinitions = await MissionDefinitionService.ReadByInspectionAreaId( inspectionArea.Id ); Assert.True(missionResponse.IsSuccessStatusCode); + Assert.NotNull(missionDefinitions.Find((m) => m.Name == testName)); Assert.True(inspectionAreaMissionResponse.IsSuccessStatusCode); - Assert.Single( - missionDefinitions, - m => m.Id.Equals(mission!.MissionId, StringComparison.Ordinal) - ); } [Fact] @@ -214,7 +203,7 @@ public async Task TestUpdatingInspectionAreaPolygon() } [Fact] - public async Task ScheduleMissionOutsideInspectionAreaPolygonFails() + public async Task CreateMissionDefinitionOutsideInspectionAreaPolygonFails() { // Arrange var installation = await DatabaseUtilities.NewInstallation(); @@ -246,25 +235,19 @@ public async Task ScheduleMissionOutsideInspectionAreaPolygonFails() var robot = await DatabaseUtilities.NewRobot(RobotStatus.Available, installation); - var inspection = new CustomInspectionQuery - { - InspectionTarget = new Position(), - InspectionType = InspectionType.Image, - }; - var tasks = new List + var tasks = new List { new() { - Inspection = inspection, TagId = "test", + TargetPosition = new Position(), + SensorType = SensorType.Image, + AnalysisTypes = [AnalysisType.Fencilla], RobotPose = new Pose(11, 11, 11, 0, 0, 0, 1), // Position outside polygon - TaskOrder = 0, }, }; - var missionQuery = new CustomMissionQuery + var missionQuery = new CreateMissionQuery { - RobotId = robot.Id, - CreationTime = DateTime.UtcNow, InstallationCode = installation.InstallationCode, Name = "TestMission", Tasks = tasks, @@ -278,7 +261,7 @@ public async Task ScheduleMissionOutsideInspectionAreaPolygonFails() // Act var missionResponse = await Client.PostAsync( - "/missions/custom", + "/missions/definitions", missionContent, TestContext.Current.CancellationToken ); diff --git a/backend/api.test/Controllers/MissionDefinitionControllerTests.cs b/backend/api.test/Controllers/MissionDefinitionControllerTests.cs index 867214bfa..c2d435c31 100644 --- a/backend/api.test/Controllers/MissionDefinitionControllerTests.cs +++ b/backend/api.test/Controllers/MissionDefinitionControllerTests.cs @@ -22,7 +22,6 @@ public class MissionDefinitionControllerTests : IAsyncLifetime public required JsonSerializerOptions SerializerOptions; public required IMissionDefinitionService MissionDefinitionService; - public required ISourceService SourceService; public async ValueTask InitializeAsync() { @@ -40,7 +39,6 @@ public async ValueTask InitializeAsync() MissionDefinitionService = serviceProvider.GetRequiredService(); - SourceService = serviceProvider.GetRequiredService(); } public async ValueTask DisposeAsync() @@ -60,11 +58,20 @@ public async Task CheckThatListAllMissionDefinitionsEndpointReturnsSuccess() plant.PlantCode ); - var source = await SourceService.CreateSourceIfDoesNotExist([]); - + var task = new TaskDefinition( + new TaskQuery + { + TagId = "test", + TargetPosition = new Position(), + SensorType = SensorType.Image, + AnalysisTypes = [AnalysisType.Fencilla], + RobotPose = new Pose(11, 11, 11, 0, 0, 0, 1), + }, + 1 + ); var missionDefinition = new MissionDefinition { - Source = source, + Tasks = [task], InstallationCode = installation.InstallationCode, Name = "Test Mission Definition", InspectionArea = inspectionArea, @@ -81,7 +88,7 @@ public async Task CheckThatListAllMissionDefinitionsEndpointReturnsSuccess() // Assert var missionDefinitions = await response.Content.ReadFromJsonAsync< - List + IEnumerable >(SerializerOptions, cancellationToken: TestContext.Current.CancellationToken); Assert.Single(missionDefinitions!); diff --git a/backend/api.test/Controllers/MissionLoaderControllerTests.cs b/backend/api.test/Controllers/MissionLoaderControllerTests.cs deleted file mode 100644 index f17808d15..000000000 --- a/backend/api.test/Controllers/MissionLoaderControllerTests.cs +++ /dev/null @@ -1,107 +0,0 @@ -using System.Collections.Generic; -using System.Net; -using System.Net.Http; -using System.Net.Http.Json; -using System.Text.Json; -using System.Threading.Tasks; -using Api.Services.MissionLoaders; -using Testcontainers.PostgreSql; -using Xunit; - -namespace Api.Test.Controllers; - -public class MissionLoaderControllerTests : IAsyncLifetime -{ - public required PostgreSqlContainer Container; - public required HttpClient Client; - public required HttpClient UnauthenticatedClient; - public required JsonSerializerOptions SerializerOptions; - - public async ValueTask InitializeAsync() - { - (Container, var connectionString, _) = await TestSetupHelpers.ConfigurePostgreSqlDatabase(); - - var factory = TestSetupHelpers.ConfigureWebApplicationFactory( - postgreSqlConnectionString: connectionString - ); - var unauthFactory = TestSetupHelpers.ConfigureUnauthenticatedWebApplicationFactory( - postgreSqlConnectionString: connectionString - ); - - Client = TestSetupHelpers.ConfigureHttpClient(factory); - UnauthenticatedClient = TestSetupHelpers.ConfigureUnauthenticatedHttpClient(unauthFactory); - SerializerOptions = TestSetupHelpers.ConfigureJsonSerializerOptions(); - } - - public ValueTask DisposeAsync() - { - return ValueTask.CompletedTask; - } - - [Fact] - public async Task GetAvailableMissionsReturnsOkWithMissionsList() - { - // Act - var response = await Client.GetAsync( - "/mission-loader/available-missions/TTT", - TestContext.Current.CancellationToken - ); - var missions = await response.Content.ReadFromJsonAsync>( - SerializerOptions, - cancellationToken: TestContext.Current.CancellationToken - ); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(missions); - Assert.Single(missions); - Assert.Equal("TTT", missions[0].InstallationCode); - Assert.Equal("test", missions[0].Name); - } - - [Fact] - public async Task GetMissionByIdReturnsOkWithMission() - { - // Act - var response = await Client.GetAsync( - "/mission-loader/missions/test-mission-id", - TestContext.Current.CancellationToken - ); - var mission = await response.Content.ReadFromJsonAsync( - SerializerOptions, - cancellationToken: TestContext.Current.CancellationToken - ); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(mission); - Assert.Equal("TTT", mission.InstallationCode); - Assert.Equal("test", mission.Name); - } - - [Fact] - public async Task GetAvailableMissionsUnauthenticatedUserReturnsUnauthorized() - { - // Act - var response = await UnauthenticatedClient.GetAsync( - "/mission-loader/available-missions/TTT", - TestContext.Current.CancellationToken - ); - - // Assert - Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); - } - - [Fact] - public async Task GetMissionByIdUnauthenticatedUserReturnsUnauthorized() - { - // Act - var response = await UnauthenticatedClient.GetAsync( - "/mission-loader/missions/test-mission-id", - TestContext.Current.CancellationToken - ); - - // Assert - Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); - } -} diff --git a/backend/api.test/Controllers/MissionSchedulingControllerTests.cs b/backend/api.test/Controllers/MissionSchedulingControllerTests.cs index bdaf32934..4e6479201 100644 --- a/backend/api.test/Controllers/MissionSchedulingControllerTests.cs +++ b/backend/api.test/Controllers/MissionSchedulingControllerTests.cs @@ -64,16 +64,30 @@ public async Task CheckThatSchedulingAMissionToBusyRobotSetsMissionToQueued() installation, inspectionArea.Id ); - string missionsUrl = "/missions"; - - // Act - var query = new ScheduledMissionQuery + TaskDefinition task = new() { - RobotId = robot.Id, - InstallationCode = installation.InstallationCode, - MissionSourceId = "95", - CreationTime = DateTime.UtcNow, + TagId = "dummy tag id 1", + Description = "dummy task 1", + RobotPose = new Pose(), + AnalysisTypes = [AnalysisType.Fencilla], + TargetPosition = new Position + { + X = 0, + Y = 0, + Z = 0, + }, }; + var missionDefinition = await DatabaseUtilities.NewMissionDefinition( + null, + installation.InstallationCode, + inspectionArea, + [task], + writeToDatabase: true + ); + string missionsUrl = $"/missions/schedule/{missionDefinition.Id}"; + + // Act + var query = new ScheduledMissionQuery { RobotId = robot.Id }; var content = new StringContent( JsonSerializer.Serialize(query), null, @@ -111,6 +125,26 @@ public async Task CheckThatSchedulingThreeAdditionalMissionsToTheQueueWorksAsExp installation, inspectionArea.Id ); + TaskDefinition task = new() + { + TagId = "dummy tag id 1", + Description = "dummy task 1", + RobotPose = new Pose(), + AnalysisTypes = [AnalysisType.Fencilla], + TargetPosition = new Position + { + X = 0, + Y = 0, + Z = 0, + }, + }; + var missionDefinition = await DatabaseUtilities.NewMissionDefinition( + null, + installation.InstallationCode, + inspectionArea, + [task], + writeToDatabase: true + ); // Act var query = new ScheduledMissionQuery @@ -126,11 +160,11 @@ public async Task CheckThatSchedulingThreeAdditionalMissionsToTheQueueWorksAsExp "application/json" ); - const string MissionsUrl = "/missions"; - _ = await Client.PostAsync(MissionsUrl, content, TestContext.Current.CancellationToken); + string missionsUrl = $"/missions/schedule/{missionDefinition.Id}"; + _ = await Client.PostAsync(missionsUrl, content, TestContext.Current.CancellationToken); var responseMissionOne = await Client.PostAsync( - MissionsUrl, + missionsUrl, content, TestContext.Current.CancellationToken ); @@ -139,7 +173,7 @@ public async Task CheckThatSchedulingThreeAdditionalMissionsToTheQueueWorksAsExp cancellationToken: TestContext.Current.CancellationToken ); var responseMissionTwo = await Client.PostAsync( - MissionsUrl, + missionsUrl, content, TestContext.Current.CancellationToken ); @@ -148,7 +182,7 @@ public async Task CheckThatSchedulingThreeAdditionalMissionsToTheQueueWorksAsExp cancellationToken: TestContext.Current.CancellationToken ); var responseMissionThree = await Client.PostAsync( - MissionsUrl, + missionsUrl, content, TestContext.Current.CancellationToken ); @@ -194,7 +228,7 @@ public async Task CheckThatDeletingMissionRunThatDoesNotExistReturnsNotFound() } [Fact] - public async Task ScheduleDuplicateCustomMissionDefinitions() + public async Task CreateDuplicateMissionDefinitions() { // Arrange var installation = await DatabaseUtilities.NewInstallation(); @@ -209,7 +243,28 @@ public async Task ScheduleDuplicateCustomMissionDefinitions() inspectionArea.Id ); - var query = CreateDefaultCustomMissionQuery(robot.Id, installation.InstallationCode); + TaskDefinition task = new() + { + TagId = "dummy tag id 1", + Description = "dummy task 1", + RobotPose = new Pose(), + AnalysisTypes = [AnalysisType.Fencilla], + TargetPosition = new Position + { + X = 0, + Y = 0, + Z = 0, + }, + }; + var missionDefinition = await DatabaseUtilities.NewMissionDefinition( + null, + installation.InstallationCode, + inspectionArea, + [task], + writeToDatabase: true + ); + + var query = new ScheduledMissionQuery { RobotId = robot.Id }; var content = new StringContent( JsonSerializer.Serialize(query), null, @@ -217,9 +272,9 @@ public async Task ScheduleDuplicateCustomMissionDefinitions() ); // Act - const string CustomMissionsUrl = "/missions/custom"; + string customMissionsUrl = $"/missions/schedule/{missionDefinition.Id}"; var responseMissionOne = await Client.PostAsync( - CustomMissionsUrl, + customMissionsUrl, content, TestContext.Current.CancellationToken ); @@ -229,7 +284,7 @@ public async Task ScheduleDuplicateCustomMissionDefinitions() ); var responseMissionTwo = await Client.PostAsync( - CustomMissionsUrl, + customMissionsUrl, content, TestContext.Current.CancellationToken ); @@ -248,7 +303,7 @@ public async Task ScheduleDuplicateCustomMissionDefinitions() } [Fact] - public async Task CheckThatNextRunIsCorrectlySelectedWhenSchedulingMultipleMissions() + public async Task CheckThatMissionDoesNotStartIfRobotIsNotInSameInstallationAsMission() { // Arrange var installation = await DatabaseUtilities.NewInstallation(); @@ -257,137 +312,51 @@ public async Task CheckThatNextRunIsCorrectlySelectedWhenSchedulingMultipleMissi installation.InstallationCode, plant.PlantCode ); + + var otherInstallation = await DatabaseUtilities.NewInstallation("OtherCode"); var robot = await DatabaseUtilities.NewRobot( RobotStatus.Available, - installation, + otherInstallation, inspectionArea.Id ); - var query = CreateDefaultCustomMissionQuery(robot.Id, installation.InstallationCode); - var content = new StringContent( - JsonSerializer.Serialize(query), - null, - "application/json" - ); - - const string CustomMissionsUrl = "/missions/custom"; - var response = await Client.PostAsync( - CustomMissionsUrl, - content, - TestContext.Current.CancellationToken - ); - var activeMissionRun = await response.Content.ReadFromJsonAsync( - SerializerOptions, - cancellationToken: TestContext.Current.CancellationToken - ); - - var scheduleQuery = new ScheduleMissionQuery + var missionId = Guid.NewGuid().ToString(); + TaskDefinition task1 = new() { - RobotId = robot.Id, - CreationTime = DateTime.SpecifyKind(new DateTime(2050, 1, 1), DateTimeKind.Utc), + TagId = "dummy tag id 1", + Description = "dummy task 1", + RobotPose = new Pose(), + AnalysisTypes = [AnalysisType.Fencilla], + TargetPosition = new Position + { + X = 0, + Y = 0, + Z = 0, + }, }; + var missionDefinition = await DatabaseUtilities.NewMissionDefinition( + missionId, + installation.InstallationCode, + inspectionArea, + [task1], + writeToDatabase: true + ); + + var scheduleQuery = new ScheduleMissionQuery { RobotId = robot.Id }; var scheduleContent = new StringContent( JsonSerializer.Serialize(scheduleQuery), null, "application/json" ); - string scheduleMissionsUrl = $"/missions/schedule/{activeMissionRun!.MissionId}"; + string scheduleUrl = $"/missions/schedule/{missionDefinition.Id}"; - var missionRunOneResponse = await Client.PostAsync( - scheduleMissionsUrl, + var scheduleMissionResponse = await Client.PostAsync( + scheduleUrl, scheduleContent, TestContext.Current.CancellationToken ); - var missionRunOne = await missionRunOneResponse.Content.ReadFromJsonAsync( - SerializerOptions, - cancellationToken: TestContext.Current.CancellationToken - ); - - var missionRunTwoResponse = await Client.PostAsync( - scheduleMissionsUrl, - scheduleContent, - TestContext.Current.CancellationToken - ); - var missionRunTwo = await missionRunTwoResponse.Content.ReadFromJsonAsync( - SerializerOptions, - cancellationToken: TestContext.Current.CancellationToken - ); - - var missionRunThreeResponse = await Client.PostAsync( - scheduleMissionsUrl, - scheduleContent, - TestContext.Current.CancellationToken - ); - var missionRunThree = - await missionRunThreeResponse.Content.ReadFromJsonAsync( - SerializerOptions, - cancellationToken: TestContext.Current.CancellationToken - ); - - Thread.Sleep(1000); - // Act - string nextMissionUrl = $"missions/definitions/{activeMissionRun.MissionId}/next-run"; - var nextMissionResponse = await Client.GetAsync( - nextMissionUrl, - TestContext.Current.CancellationToken - ); - - // Assert - var nextMissionRun = await nextMissionResponse.Content.ReadFromJsonAsync( - SerializerOptions, - cancellationToken: TestContext.Current.CancellationToken - ); - - // Next mission can be any of these three missions due to timing - var possibleNextMissionRuns = new List - { - missionRunOne!.Id, - missionRunTwo!.Id, - missionRunThree!.Id, - }; - - Assert.True(nextMissionResponse.IsSuccessStatusCode); - Assert.NotNull(nextMissionRun); - Assert.Equal(missionRunOne!.MissionId, activeMissionRun.MissionId); - Assert.Equal(missionRunTwo!.MissionId, activeMissionRun.MissionId); - Assert.Equal(missionRunThree!.MissionId, activeMissionRun.MissionId); - Assert.Contains(nextMissionRun.Id, possibleNextMissionRuns); - } - - [Fact] - public async Task CheckThatMissionDoesNotStartIfRobotIsNotInSameInstallationAsMission() - { - // Arrange - var installation = await DatabaseUtilities.NewInstallation(); - var plant = await DatabaseUtilities.NewPlant(installation.InstallationCode); - var inspectionArea = await DatabaseUtilities.NewInspectionArea( - installation.InstallationCode, - plant.PlantCode - ); - - var otherInstallation = await DatabaseUtilities.NewInstallation("OtherCode"); - var robot = await DatabaseUtilities.NewRobot( - RobotStatus.Available, - otherInstallation, - inspectionArea.Id - ); - - var query = CreateDefaultCustomMissionQuery(robot.Id, installation.InstallationCode); - var content = new StringContent( - JsonSerializer.Serialize(query), - null, - "application/json" - ); - - // Act - const string CustomMissionsUrl = "/missions/custom"; - var response = await Client.PostAsync( - CustomMissionsUrl, - content, - TestContext.Current.CancellationToken - ); - Assert.Equal(HttpStatusCode.Conflict, response.StatusCode); + Assert.Equal(HttpStatusCode.Conflict, scheduleMissionResponse.StatusCode); } [Fact] @@ -442,59 +411,45 @@ public async Task CheckThatMissionFailsIfRobotIsNotInSameInspectionAreaAsMission inspectionAreaRobot.Id ); - var query = CreateDefaultCustomMissionQuery(robot.Id, installation.InstallationCode); - var content = new StringContent( - JsonSerializer.Serialize(query), + var missionId = Guid.NewGuid().ToString(); + TaskDefinition task1 = new() + { + TagId = "dummy tag id 1", + Description = "dummy task 1", + RobotPose = new Pose(), + AnalysisTypes = [AnalysisType.Fencilla], + TargetPosition = new Position + { + X = 0, + Y = 0, + Z = 0, + }, + }; + var missionDefinition = await DatabaseUtilities.NewMissionDefinition( + missionId, + installation.InstallationCode, + _inspectionAreaMission, + [task1], + writeToDatabase: true + ); + + var scheduleQuery = new ScheduleMissionQuery { RobotId = robot.Id }; + var scheduleContent = new StringContent( + JsonSerializer.Serialize(scheduleQuery), null, "application/json" ); - // Act - const string CustomMissionsUrl = "/missions/custom"; - var missionResponse = await Client.PostAsync( - CustomMissionsUrl, - content, + string scheduleUrl = $"/missions/schedule/{missionDefinition.Id}"; + + var scheduleMissionResponse = await Client.PostAsync( + scheduleUrl, + scheduleContent, TestContext.Current.CancellationToken ); - Assert.Equal(HttpStatusCode.BadRequest, missionResponse.StatusCode); - } - private static CustomMissionQuery CreateDefaultCustomMissionQuery( - string robotId, - string installationCode - ) - { - return new CustomMissionQuery - { - RobotId = robotId, - InstallationCode = installationCode, - CreationTime = DateTime.SpecifyKind(new DateTime(3050, 1, 1), DateTimeKind.Utc), - InspectionFrequency = new TimeSpan(14, 0, 0, 0), - Name = "TestMission", - Tasks = - [ - new CustomTaskQuery - { - RobotPose = new Pose(), - Inspection = new CustomInspectionQuery - { - InspectionTarget = new Position(), - InspectionType = InspectionType.Image, - }, - TaskOrder = 0, - }, - new CustomTaskQuery - { - RobotPose = new Pose(1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f), - Inspection = new CustomInspectionQuery - { - InspectionTarget = new Position(), - InspectionType = InspectionType.Image, - }, - TaskOrder = 1, - }, - ], - }; + // Act + Assert.Equal(HttpStatusCode.BadRequest, scheduleMissionResponse.StatusCode); } } } diff --git a/backend/api.test/Controllers/SourceControllerTests.cs b/backend/api.test/Controllers/SourceControllerTests.cs deleted file mode 100644 index c963f76db..000000000 --- a/backend/api.test/Controllers/SourceControllerTests.cs +++ /dev/null @@ -1,78 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Net.Http; -using System.Net.Http.Json; -using System.Text.Json; -using System.Threading.Tasks; -using Api.Database.Models; -using Api.Services; -using Api.Test.Database; -using Microsoft.Extensions.DependencyInjection; -using Testcontainers.PostgreSql; -using Xunit; - -namespace Api.Test.Controllers; - -public class SourceControllerTests : IAsyncLifetime -{ - public required DatabaseUtilities DatabaseUtilities; - public required PostgreSqlContainer Container; - public required HttpClient Client; - public required JsonSerializerOptions SerializerOptions; - - public required ISourceService SourceService; - - public async ValueTask InitializeAsync() - { - (Container, var connectionString, var connection) = - await TestSetupHelpers.ConfigurePostgreSqlDatabase(); - var factory = TestSetupHelpers.ConfigureWebApplicationFactory( - postgreSqlConnectionString: connectionString - ); - - Client = TestSetupHelpers.ConfigureHttpClient(factory); - SerializerOptions = TestSetupHelpers.ConfigureJsonSerializerOptions(); - DatabaseUtilities = factory.Services.GetRequiredService(); - } - - public ValueTask DisposeAsync() - { - GC.SuppressFinalize(this); - return ValueTask.CompletedTask; - } - - [Fact] - public async Task CheckThatListAllSourcesWorksAsExpected() - { - // Arrange - var sourceOne = await DatabaseUtilities.NewSource(sourceId: "TestIdOne"); - var sourceTwo = await DatabaseUtilities.NewSource(sourceId: "TestIdTwo"); - - // Act - var response = await Client.GetAsync("/sources", TestContext.Current.CancellationToken); - var sources = await response.Content.ReadFromJsonAsync>( - SerializerOptions, - cancellationToken: TestContext.Current.CancellationToken - ); - - // Assert - Assert.Equal(2, sources!.Count); - Assert.Equal(sourceOne.Id, sources[0].Id); - Assert.Equal(sourceTwo.Id, sources[1].Id); - } - - [Fact] - public async Task CheckThatLookupSourceByIdWorksAsExpected() - { - var source = await DatabaseUtilities.NewSource(); - var response = await Client.GetAsync( - $"/sources/{source.Id}", - TestContext.Current.CancellationToken - ); - var sourceFromResponse = await response.Content.ReadFromJsonAsync( - SerializerOptions, - cancellationToken: TestContext.Current.CancellationToken - ); - Assert.Equal(source.Id, sourceFromResponse!.Id); - } -} diff --git a/backend/api.test/Database/DatabaseUtilities.cs b/backend/api.test/Database/DatabaseUtilities.cs index 0ad4f32f2..f8fae01eb 100644 --- a/backend/api.test/Database/DatabaseUtilities.cs +++ b/backend/api.test/Database/DatabaseUtilities.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Threading.Tasks; using Api.Controllers.Models; using Api.Database.Models; @@ -8,7 +9,6 @@ namespace Api.Test.Database { public class DatabaseUtilities( IMissionRunService _missionRunService, - ISourceService _sourceService, IMissionDefinitionService _missionDefinitionService, IInstallationService _installationService, IPlantService _plantService, @@ -37,6 +37,7 @@ public async Task NewMissionRun( installationCode, inspectionArea, null, + null, writeToDatabase ); @@ -52,11 +53,14 @@ public async Task NewMissionRun( InstallationCode = installationCode, }; - missionRun.Tasks = [new(new Pose())]; if (writeToDatabase) { missionRun = await _missionRunService.Create(missionRun); - await _robotService.UpdateCurrentMissionId(robot.Id, missionRun.Id); + if ( + missionStatus != MissionStatus.Successful + && missionStatus != MissionStatus.Failed + ) + await _robotService.UpdateCurrentMissionId(robot.Id, missionRun.Id); } return missionRun; } @@ -65,6 +69,7 @@ public async Task NewMissionDefinition( string? id, string installationCode, InspectionArea inspectionArea, + List? tasks = null, MissionRun? lastSuccessfulRun = null, bool writeToDatabase = false ) @@ -75,14 +80,13 @@ public async Task NewMissionDefinition( if (string.IsNullOrEmpty(id)) id = Guid.NewGuid().ToString(); - var source = await _sourceService.Create(new Source { SourceId = $"{id}" }); var missionDefinition = new MissionDefinition { Id = id, Name = "testMissionDefinition", InspectionArea = inspectionArea, InstallationCode = installationCode, - Source = source, + Tasks = tasks ?? [], InspectionFrequency = new DateTime().AddDays(7) - new DateTime(), LastSuccessfulRun = lastSuccessfulRun, AutoScheduleFrequency = new AutoScheduleFrequency @@ -165,10 +169,5 @@ public async Task NewRobot( var robot = new Robot(createRobotQuery, installation, inspectionAreaId); return await _robotService.Create(robot); } - - public async Task NewSource(string sourceId = "TestId") - { - return await _sourceService.Create(new Source { SourceId = sourceId }); - } } } diff --git a/backend/api.test/EventHandlers/TestMissionEventHandler.cs b/backend/api.test/EventHandlers/TestMissionEventHandler.cs index edc40032c..9eb06d220 100644 --- a/backend/api.test/EventHandlers/TestMissionEventHandler.cs +++ b/backend/api.test/EventHandlers/TestMissionEventHandler.cs @@ -97,7 +97,8 @@ public async Task ScheduledMissionStartedWhenSystemIsAvailable() var missionRun = await DatabaseUtilities.NewMissionRun( installation.InstallationCode, robot, - inspectionArea + inspectionArea, + tasks: [new() { RobotPose = new Pose { } }] ); // Act @@ -129,7 +130,8 @@ public async Task SecondScheduledMissionQueuedIfRobotIsBusy() installation.InstallationCode, robot, inspectionArea, - missionStatus: MissionStatus.Ongoing + missionStatus: MissionStatus.Ongoing, + tasks: [new() { RobotPose = new Pose { } }] ); await MissionRunService.Create(missionRunOne); await MissionSchedulingService.StartNextMissionRunIfSystemIsAvailable(robot); @@ -138,7 +140,8 @@ public async Task SecondScheduledMissionQueuedIfRobotIsBusy() var missionRunTwo = await DatabaseUtilities.NewMissionRun( installation.InstallationCode, robot, - inspectionArea + inspectionArea, + tasks: [new() { RobotPose = new Pose { } }] ); await MissionRunService.Create(missionRunTwo); await MissionSchedulingService.StartNextMissionRunIfSystemIsAvailable(robot); @@ -175,7 +178,8 @@ public async Task NewMissionIsStartedWhenRobotBecomesAvailable() var missionRun = await DatabaseUtilities.NewMissionRun( installation.InstallationCode, robot, - inspectionArea + inspectionArea, + tasks: [new() { RobotPose = new Pose { } }] ); await MissionRunService.Create(missionRun); @@ -226,12 +230,14 @@ public async Task MissionRunIsStartedForOtherAvailableRobotIfOneRobotHasAnOngoin var missionRunOne = await DatabaseUtilities.NewMissionRun( installation.InstallationCode, robotOne, - inspectionArea + inspectionArea, + tasks: [new() { RobotPose = new Pose { } }] ); var missionRunTwo = await DatabaseUtilities.NewMissionRun( installation.InstallationCode, robotTwo, - inspectionArea + inspectionArea, + tasks: [new() { RobotPose = new Pose { } }] ); // Act (Ensure first mission is started) @@ -355,13 +361,15 @@ public async Task IsarStatusTriggersNextMissionEvenIfOtherMissionIsOngoing() robot, inspectionArea, writeToDatabase: true, - missionStatus: MissionStatus.Ongoing + missionStatus: MissionStatus.Ongoing, + tasks: [new() { RobotPose = new Pose { } }] ); var missionRun2 = await DatabaseUtilities.NewMissionRun( installation.InstallationCode, robot, inspectionArea, - writeToDatabase: true + writeToDatabase: true, + tasks: [new() { RobotPose = new Pose { } }] ); Assert.False(IsarService.isStartCalled); Assert.False(IsarService.isStarted); diff --git a/backend/api.test/HostedServices/TestAutoSchedulingHostedService.cs b/backend/api.test/HostedServices/TestAutoSchedulingHostedService.cs index 6b8594b36..5de487199 100644 --- a/backend/api.test/HostedServices/TestAutoSchedulingHostedService.cs +++ b/backend/api.test/HostedServices/TestAutoSchedulingHostedService.cs @@ -103,6 +103,7 @@ await DatabaseUtilities.NewMissionDefinition( "1", installation.InstallationCode, inspectionArea, + null, missionRun, writeToDatabase: true ); diff --git a/backend/api.test/MQTT/TestMqttEvents.cs b/backend/api.test/MQTT/TestMqttEvents.cs index aae95166a..46de7e771 100644 --- a/backend/api.test/MQTT/TestMqttEvents.cs +++ b/backend/api.test/MQTT/TestMqttEvents.cs @@ -2,6 +2,7 @@ using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using Api.Controllers.Models; using Api.Database.Context; using Api.Database.Models; using Api.Mqtt; @@ -146,12 +147,24 @@ public async Task TestMQTTMissionAborted() installation, inspectionArea.Id ); + var task = new TaskDefinition( + new TaskQuery + { + TagId = "test", + TargetPosition = new Position(), + SensorType = SensorType.Image, + AnalysisTypes = [AnalysisType.Fencilla], + RobotPose = new Pose(11, 11, 11, 0, 0, 0, 1), + }, + 1 + ).ToMissionRunTask(); var missionRun = await DatabaseUtilities.NewMissionRun( installation.InstallationCode, robot, inspectionArea, writeToDatabase: true, - missionStatus: MissionStatus.Ongoing + missionStatus: MissionStatus.Ongoing, + tasks: [task] ); var message = new IsarMissionAbortedMessage @@ -196,7 +209,8 @@ public async Task TestMQTTMissionStatus() robot, inspectionArea, writeToDatabase: true, - missionStatus: MissionStatus.Ongoing + missionStatus: MissionStatus.Ongoing, + tasks: [new() { RobotPose = new Pose { } }] ); var message = new IsarMissionMessage @@ -467,12 +481,24 @@ public async Task TestMQTTStartup() installation, inspectionArea.Id ); + var task = new TaskDefinition( + new TaskQuery + { + TagId = "test", + TargetPosition = new Position(), + SensorType = SensorType.Image, + AnalysisTypes = [AnalysisType.Fencilla], + RobotPose = new Pose(11, 11, 11, 0, 0, 0, 1), + }, + 1 + ).ToMissionRunTask(); var missionRun = await DatabaseUtilities.NewMissionRun( installation.InstallationCode, robot, inspectionArea, writeToDatabase: true, - missionStatus: MissionStatus.Ongoing + missionStatus: MissionStatus.Ongoing, + tasks: [task] ); var message = new IsarStartupMessage @@ -542,14 +568,24 @@ public async Task TestMQTTSaraAnalysisResult() installation, inspectionArea.Id ); - MissionTask task = new MissionTask { RobotPose = new Pose { } }; + var task = new TaskDefinition( + new TaskQuery + { + TagId = "test", + TargetPosition = new Position(), + SensorType = SensorType.Image, + AnalysisTypes = [AnalysisType.Fencilla], + RobotPose = new Pose(11, 11, 11, 0, 0, 0, 1), + }, + 1 + ).ToMissionRunTask(); var missionRun = await DatabaseUtilities.NewMissionRun( installation.InstallationCode, robot, inspectionArea, writeToDatabase: true, missionStatus: MissionStatus.Ongoing, - tasks: new MissionTask[] { task } + tasks: [task] ); var isarInspectionId = missionRun.Tasks[0].Inspection!.IsarInspectionId; diff --git a/backend/api.test/Mocks/MissionLoaderMock.cs b/backend/api.test/Mocks/MissionLoaderMock.cs deleted file mode 100644 index 6efdb6969..000000000 --- a/backend/api.test/Mocks/MissionLoaderMock.cs +++ /dev/null @@ -1,98 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Api.Database.Models; -using Api.Services.MissionLoaders; - -namespace Api.Test.Mocks -{ - public class MockMissionLoader : IMissionLoader - { - private readonly List _mockMissionTasks = - [ - new MissionTask( - inspection: new Inspection(), - taskOrder: 0, - tagId: "1", - tagLink: new Uri("https://testurl.com"), - poseId: 1, - taskDescription: "description", - robotPose: new Pose - { - Position = new Position - { - X = 0, - Y = 0, - Z = 0, - }, - Orientation = new Orientation - { - X = 0, - Y = 0, - Z = 0, - W = 1, - }, - } - ), - new MissionTask( - inspection: new Inspection(), - taskOrder: 0, - tagId: "2", - tagLink: new Uri("https://testurl.com"), - poseId: 1, - taskDescription: "description", - robotPose: new Pose - { - Position = new Position - { - X = 0, - Y = 0, - Z = 0, - }, - Orientation = new Orientation - { - X = 0, - Y = 0, - Z = 0, - W = 1, - }, - } - ), - ]; - - private readonly MissionDefinition _mockMissionDefinition = new() - { - InspectionArea = new InspectionArea(), - Comment = "", - Id = "", - InstallationCode = "TTT", - IsDeprecated = false, - Name = "test", - Source = new Source { Id = "", SourceId = "" }, - }; - - public async Task GetMissionById(string sourceMissionId) - { - await Task.Run(() => Thread.Sleep(1)); - return new CondensedMissionDefinition(_mockMissionDefinition); - } - - public async Task> GetAvailableMissions( - string? installationCode - ) - { - await Task.Run(() => Thread.Sleep(1)); - return new List([ - new CondensedMissionDefinition(_mockMissionDefinition), - ]).AsQueryable(); - } - - public async Task?> GetTasksForMission(string sourceMissionId) - { - await Task.Run(() => Thread.Sleep(1)); - return _mockMissionTasks; - } - } -} diff --git a/backend/api.test/Services/EchoService.cs b/backend/api.test/Services/EchoService.cs deleted file mode 100644 index 75fbd7198..000000000 --- a/backend/api.test/Services/EchoService.cs +++ /dev/null @@ -1,120 +0,0 @@ -using System; -using System.Net; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using Api.Database.Context; -using Api.Services; -using Api.Test.Database; -using Api.Utilities; -using Microsoft.Extensions.Logging; -using Microsoft.Identity.Abstractions; -using Moq; -using Testcontainers.PostgreSql; -using Xunit; - -namespace Api.Test.Services -{ - public class EchoServiceTest : IAsyncLifetime - { - public required DatabaseUtilities DatabaseUtilities; - public required PostgreSqlContainer Container; - public required FlotillaDbContext Context; - - public async ValueTask InitializeAsync() - { - (Container, string connectionString, var connection) = - await TestSetupHelpers.ConfigurePostgreSqlDatabase(); - - Context = TestSetupHelpers.ConfigurePostgreSqlContext(connectionString); - - DatabaseUtilities = TestSetupHelpers.CreateIsolatedDatabaseUtilities(Context); - } - - public ValueTask DisposeAsync() - { - GC.SuppressFinalize(this); - return ValueTask.CompletedTask; - } - - [Fact] - public async Task TestGetAvailableMissions_WhenServerReturns500_ThrowsException() - { - //Arrange - var echoApiMock = new Mock(); - var logger = new Mock>(); - var sourceService = new Mock(); - var inspectionService = new Mock(); - - var httpResponse = new HttpResponseMessage(HttpStatusCode.InternalServerError); - - echoApiMock - .Setup(a => - a.CallApiForAppAsync( - It.IsAny(), - It.IsAny?>(), - It.IsAny(), - It.IsAny() - ) - ) - .ReturnsAsync(httpResponse); - - var echoService = new EchoService( - logger.Object, - echoApiMock.Object, - sourceService.Object, - inspectionService.Object - ); - var installation = await DatabaseUtilities.NewInstallation(); - - //Act & Assert - var exception = await Assert.ThrowsAsync(async () => - await echoService.GetAvailableMissions(installation.InstallationCode) - ); - Assert.Equal( - "Echo API unavailable. Status code: InternalServerError", - exception.Message - ); - } - - [Fact] - public async Task TestGetEchoMission_WhenServerReturns500_ThrowsException() - { - //Arrange - var echoApiMock = new Mock(); - var logger = new Mock>(); - var sourceService = new Mock(); - var inspectionService = new Mock(); - - var httpResponse = new HttpResponseMessage(HttpStatusCode.InternalServerError); - - echoApiMock - .Setup(a => - a.CallApiForAppAsync( - It.IsAny(), - It.IsAny?>(), - It.IsAny(), - It.IsAny() - ) - ) - .ReturnsAsync(httpResponse); - - var echoService = new EchoService( - logger.Object, - echoApiMock.Object, - sourceService.Object, - inspectionService.Object - ); - var dummyEchoMissionId = "1"; - - //Act and Assert - var exception = await Assert.ThrowsAsync(async () => - await echoService.GetMissionById(dummyEchoMissionId) - ); - Assert.Equal( - "Echo API unavailable. Status code: InternalServerError", - exception.Message - ); - } - } -} diff --git a/backend/api.test/Services/InspectionAreaService.cs b/backend/api.test/Services/InspectionAreaService.cs index cb832335c..495b8c849 100644 --- a/backend/api.test/Services/InspectionAreaService.cs +++ b/backend/api.test/Services/InspectionAreaService.cs @@ -55,10 +55,34 @@ public void TestTasksInsidePolygon() ], }; - List missionTasks = + List missionTasks = [ - new(new Pose(1, 1, 1, 0, 0, 0, 1)), - new(new Pose(2, 2, 2, 0, 0, 0, 1)), + new TaskDefinition + { + TagId = "dummy tag id 1", + Description = "dummy task 1", + RobotPose = new Pose(1, 1, 1, 0, 0, 0, 1), + AnalysisTypes = [AnalysisType.Fencilla], + TargetPosition = new Position + { + X = 0, + Y = 0, + Z = 0, + }, + }, + new TaskDefinition + { + TagId = "dummy tag id 2", + Description = "dummy task 2", + RobotPose = new Pose(2, 2, 2, 0, 0, 0, 1), + AnalysisTypes = [AnalysisType.Fencilla], + TargetPosition = new Position + { + X = 0, + Y = 0, + Z = 0, + }, + }, ]; var testBool = AreaPolygonService.MissionTasksAreInsideAreaPolygon( @@ -83,10 +107,34 @@ public void TestTasksOutsidePolygon() new PolygonPoint { X = 10, Y = 0 }, ], }; - List missionTasks = + List missionTasks = [ - new(new Pose(1, 1, 1, 0, 0, 0, 1)), - new(new Pose(11, 11, 11, 0, 0, 0, 1)), + new TaskDefinition + { + TagId = "dummy tag id 1", + Description = "dummy task 1", + RobotPose = new Pose(1, 1, 1, 0, 0, 0, 1), + AnalysisTypes = [AnalysisType.Fencilla], + TargetPosition = new Position + { + X = 0, + Y = 0, + Z = 0, + }, + }, + new TaskDefinition + { + TagId = "dummy tag id 2", + Description = "dummy task 2", + RobotPose = new Pose(11, 11, 11, 0, 0, 0, 1), + AnalysisTypes = [AnalysisType.Fencilla], + TargetPosition = new Position + { + X = 0, + Y = 0, + Z = 0, + }, + }, ]; var testBool = AreaPolygonService.MissionTasksAreInsideAreaPolygon( diff --git a/backend/api.test/Services/Models/IsarMissionDefinitionTests.cs b/backend/api.test/Services/Models/IsarMissionDefinitionTests.cs new file mode 100644 index 000000000..5820ff0a9 --- /dev/null +++ b/backend/api.test/Services/Models/IsarMissionDefinitionTests.cs @@ -0,0 +1,58 @@ +using System.Text.Json; +using Api.Database.Models; +using Api.Services.Models; +using Xunit; + +namespace Api.Test.Services.Models +{ + public class IsarMissionDefinitionTests + { + [Fact] + public void AnalysisTypesSerialiseAsSnakeCaseSaraKeys() + { + var inspection = new Inspection( + SensorType.Image, + new Position(0, 0, 0), + [ + AnalysisType.Fencilla, + AnalysisType.CLOE, + AnalysisType.ThermalReading, + AnalysisType.CO2, + ], + videoDuration: null + ); + var task = new MissionTask + { + TaskOrder = 0, + RobotPose = new Pose(), + Status = TaskStatus.NotStarted, + Inspection = inspection, + }; + + var json = JsonSerializer.Serialize(new IsarInspectionDefinition(task)); + + Assert.Contains( + "\"analysis_types\":[\"fencilla\",\"cloe\",\"thermal-reading\",\"co2\"]", + json + ); + } + + [Fact] + public void ToMissionRunTaskPopulatesAnalysisTypesOnBothMissionTaskAndInspection() + { + var def = new TaskDefinition + { + Index = 0, + RobotPose = new Pose(), + TargetPosition = new Position(0, 0, 0), + SensorType = SensorType.Image, + AnalysisTypes = [AnalysisType.Fencilla], + }; + + var task = def.ToMissionRunTask(); + + Assert.Equal([AnalysisType.Fencilla], task.AnalysisTypes); + Assert.Equal([AnalysisType.Fencilla], task.Inspection!.AnalysisTypes); + } + } +} diff --git a/backend/api.test/TestSetupHelpers.cs b/backend/api.test/TestSetupHelpers.cs index bf9735026..2cb16d4e2 100644 --- a/backend/api.test/TestSetupHelpers.cs +++ b/backend/api.test/TestSetupHelpers.cs @@ -225,27 +225,23 @@ public static DatabaseUtilities CreateIsolatedDatabaseUtilities(FlotillaDbContex _areaPolygonService, _exclusionAreaService ); - var _sourceService = new SourceService(context, new Mock>().Object); var _missionDefinitionService = new MissionDefinitionService( context, signalRService, _accessRoleService, new Mock>().Object, - _missionRunService, - _sourceService + _missionRunService ); var _autoScheduleService = new AutoScheduleService( new Mock>().Object, _missionDefinitionService, _robotService, - new MockMissionLoader(), _missionRunService, _missionSchedulingService, signalRService ); var databaseUtilities = new DatabaseUtilities( _missionRunService, - _sourceService, _missionDefinitionService, _installationService, _plantService, diff --git a/backend/api.test/TestWebApplicationFactory.cs b/backend/api.test/TestWebApplicationFactory.cs index 87882c8eb..d4f2da37e 100644 --- a/backend/api.test/TestWebApplicationFactory.cs +++ b/backend/api.test/TestWebApplicationFactory.cs @@ -1,7 +1,6 @@ using System.IO; using Api.Database.Context; using Api.Services; -using Api.Services.MissionLoaders; using Api.Test.Database; using Api.Test.Mocks; using Microsoft.AspNetCore.Authentication; @@ -83,7 +82,6 @@ protected override void ConfigureWebHost(IWebHostBuilder builder) services.AddScoped(); services.AddSingleton(); services.AddScoped(); - services.AddScoped(); services .AddAuthorizationBuilder() .AddFallbackPolicy( diff --git a/backend/api/Configurations/CustomServiceConfigurations.cs b/backend/api/Configurations/CustomServiceConfigurations.cs index a58d3d404..18e69bc3d 100644 --- a/backend/api/Configurations/CustomServiceConfigurations.cs +++ b/backend/api/Configurations/CustomServiceConfigurations.cs @@ -1,9 +1,7 @@ using System.Reflection; using Api.Database.Context; -using Api.Services.MissionLoaders; using Azure.Core; using Azure.Identity; -using Microsoft.Azure.StackExchangeRedis; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Microsoft.OpenApi; @@ -299,30 +297,6 @@ [new OpenApiSecuritySchemeReference("oauth2", document)] = [], return services; } - public static IServiceCollection ConfigureMissionLoader( - this IServiceCollection services, - IConfiguration configuration - ) - { - string? missionLoaderFileName = configuration["MissionLoader:FileName"]; - if (missionLoaderFileName == null) - return services; - - var loaderType = Type.GetType(missionLoaderFileName); - if (loaderType != null && typeof(IMissionLoader).IsAssignableFrom(loaderType)) - { - services.AddScoped(typeof(IMissionLoader), loaderType); - } - else - { - throw new InvalidOperationException( - "The specified class does not implement IMissionLoader or could not be found." - ); - } - - return services; - } - public static IServiceCollection ConfigureRedisCache( this IServiceCollection services, IConfiguration configuration diff --git a/backend/api/Controllers/InspectionController.cs b/backend/api/Controllers/InspectionController.cs index 2d5765c98..d14e14c7f 100644 --- a/backend/api/Controllers/InspectionController.cs +++ b/backend/api/Controllers/InspectionController.cs @@ -1,7 +1,6 @@ using Api.Controllers.Models; using Api.Database.Models; using Api.Services; -using Api.Services.MissionLoaders; using Api.Services.Models; using Api.Utilities; using Microsoft.AspNetCore.Authorization; @@ -177,7 +176,7 @@ [FromRoute] string isarInspectionId return NotFound(errorMessage); } - if (task.Inspection?.InspectionType != InspectionType.CO2Measurement) + if (task.Inspection?.InspectionType != SensorType.CO2Measurement) { string errorMessage = $"Inspection with ISAR Inspection ID {isarInspectionId} is of type {task.Inspection?.InspectionType}. Fetching of inspection value is not supported for this inspection type."; diff --git a/backend/api/Controllers/MissionDefinitionController.cs b/backend/api/Controllers/MissionDefinitionController.cs index 891696477..b5062ce6c 100644 --- a/backend/api/Controllers/MissionDefinitionController.cs +++ b/backend/api/Controllers/MissionDefinitionController.cs @@ -13,9 +13,9 @@ namespace Api.Controllers public class MissionDefinitionController( ILogger logger, IMissionDefinitionService missionDefinitionService, - IMissionDefinitionTaskService missionDefinitionTaskService, - IMissionRunService missionRunService, - IAutoScheduleService autoScheduleService + IInstallationService installationService, + IAutoScheduleService autoScheduleService, + IInspectionAreaService inspectionAreaService ) : ControllerBase { /// @@ -26,14 +26,17 @@ IAutoScheduleService autoScheduleService /// [HttpGet("")] [Authorize(Roles = Role.Any)] - [ProducesResponseType(typeof(IList), StatusCodes.Status200OK)] + [ProducesResponseType( + typeof(IEnumerable), + StatusCodes.Status200OK + )] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task>> GetMissionDefinitions( - [FromQuery] MissionDefinitionQueryStringParameters parameters - ) + public async Task< + ActionResult> + > GetMissionDefinitions([FromQuery] MissionDefinitionQueryStringParameters parameters) { PagedList missionDefinitions; try @@ -90,36 +93,121 @@ public async Task< { return NotFound($"Could not find mission definition with id {id}"); } - var missionDefinitionResponse = new MissionDefinitionWithTasksResponse( - missionDefinitionTaskService, - missionDefinition - ); + var missionDefinitionResponse = new MissionDefinitionResponse(missionDefinition); return Ok(missionDefinitionResponse); } /// - /// Lookup which mission run is scheduled next for the given mission definition + /// List all available missions for the installation /// + /// + /// These missions are fetched based on your mission loader + /// [HttpGet] + [Route("installation/{installationCode}")] [Authorize(Roles = Role.Any)] - [Route("{id}/next-run")] - [ProducesResponseType(typeof(MissionRun), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(IList), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task> GetNextMissionRun([FromRoute] string id) + [ProducesResponseType(StatusCodes.Status502BadGateway)] + [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] + public async Task>> GetAvailableMissions( + [FromRoute] string installationCode + ) { - var missionDefinition = await missionDefinitionService.ReadById(id, readOnly: true); - if (missionDefinition == null) + IQueryable missionDefinitions; + try { - return NotFound($"Could not find mission definition with id {id}"); + missionDefinitions = await missionDefinitionService.ReadByInstallationCode( + installationCode + ); + } + catch (InvalidDataException e) + { + logger.LogError(e, "{ErrorMessage}", e.Message); + return BadRequest(e.Message); + } + catch (HttpRequestException e) + { + logger.LogError(e, "Error retrieving missions from Mission Loader"); + return StatusCode(StatusCodes.Status502BadGateway); + } + catch (JsonException e) + { + logger.LogError(e, "Error retrieving missions from database"); + return StatusCode(StatusCodes.Status500InternalServerError); } - var nextRun = await missionRunService.ReadNextScheduledRunByMissionId( - id, + + return Ok(missionDefinitions.Select((m) => new MissionDefinitionResponse(m))); + } + + /// + /// Create a new mission definition + /// + /// + /// This query creates a new mission definition + /// + [HttpPost] + [Authorize(Roles = Role.User)] + [ProducesResponseType(typeof(MissionDefinition), StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task> Create( + [FromBody] CreateMissionQuery customMissionQuery + ) + { + customMissionQuery = Sanitize.SanitizeUserInput(customMissionQuery); + + customMissionQuery.InstallationCode = customMissionQuery.InstallationCode.ToUpper(); + + var installation = await installationService.ReadByInstallationCode( + customMissionQuery.InstallationCode, readOnly: true ); - return Ok(nextRun); + if (installation == null) + { + return NotFound( + $"Could not find installation with name {customMissionQuery.InstallationCode}" + ); + } + + var missionTasks = customMissionQuery + .Tasks.Select((task, index) => new TaskDefinition(task, index)) + .ToList(); + + try + { + var inspectionAreaForMission = + inspectionAreaService.TryFindInspectionAreaForMissionTasks( + missionTasks, + customMissionQuery.InstallationCode + ); + if (inspectionAreaForMission == null) + { + return BadRequest("No inspection area found for the mission tasks"); + } + + var newMissionDefinition = new MissionDefinition + { + Id = Guid.NewGuid().ToString(), + Tasks = missionTasks, + Name = customMissionQuery.Name, + InstallationCode = customMissionQuery.InstallationCode, + InspectionArea = inspectionAreaForMission, + }; + await missionDefinitionService.Create(newMissionDefinition); + return Ok(newMissionDefinition); + } + catch (MultipleInspectionAreasFoundException e) + { + return BadRequest(e.Message); + } } /// @@ -128,8 +216,8 @@ public async Task> GetNextMissionRun([FromRoute] string /// The mission definition was successfully updated /// The mission definition data is invalid /// There was no mission definition with the given ID in the database - [HttpPut] - [Authorize(Roles = Role.Any)] + [HttpPatch] + [Authorize(Roles = Role.User)] [Route("{id}")] [ProducesResponseType(typeof(MissionDefinitionResponse), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] @@ -147,24 +235,31 @@ [FromBody] UpdateMissionDefinitionQuery missionDefinitionQuery logger.LogInformation("Updating mission definition with id '{Id}'", id); if (!ModelState.IsValid) - { return BadRequest("Invalid data."); - } var missionDefinition = await missionDefinitionService.ReadById(id, readOnly: false); if (missionDefinition == null) - { return NotFound($"Could not find mission definition with id '{id}'"); - } - if (missionDefinitionQuery.Name == null) + if (missionDefinitionQuery.Name != null) + missionDefinition.Name = missionDefinitionQuery.Name; + + if (missionDefinitionQuery.Comment != null) + missionDefinition.Comment = missionDefinitionQuery.Comment; + + if (missionDefinitionQuery.InspectionFrequency != null) + missionDefinition.InspectionFrequency = missionDefinitionQuery.InspectionFrequency; + + if (missionDefinitionQuery.Tasks != null) { - return BadRequest("Name cannot be null."); + missionDefinition.Tasks = + [ + .. missionDefinitionQuery.Tasks.Select( + (taskQuery, index) => new TaskDefinition(taskQuery, index + 1) + ), + ]; } - missionDefinition.Name = missionDefinitionQuery.Name; - missionDefinition.Comment = missionDefinitionQuery.Comment; - missionDefinition.InspectionFrequency = missionDefinitionQuery.InspectionFrequency; if (missionDefinitionQuery.SchedulingTimesCETperWeek != null) { var schedulingTimesCETperWeek = missionDefinitionQuery diff --git a/backend/api/Controllers/MissionLoaderController.cs b/backend/api/Controllers/MissionLoaderController.cs deleted file mode 100644 index d7408d4b3..000000000 --- a/backend/api/Controllers/MissionLoaderController.cs +++ /dev/null @@ -1,128 +0,0 @@ -using System.Text.Json; -using Api.Controllers.Models; -using Api.Services; -using Api.Services.MissionLoaders; -using Api.Utilities; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; - -namespace Api.Controllers -{ - [ApiController] - [Route("mission-loader")] - [Authorize(Roles = Role.Any)] - public class MissionLoaderController( - ILogger logger, - IMissionLoader missionLoader - ) : ControllerBase - { - /// - /// List all available missions for the installation - /// - /// - /// These missions are fetched based on your mission loader - /// - [HttpGet] - [Route("available-missions/{installationCode}")] - [ProducesResponseType(typeof(IList), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - [ProducesResponseType(StatusCodes.Status502BadGateway)] - [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] - public async Task>> GetAvailableMissions( - [FromRoute] string? installationCode - ) - { - IQueryable condensedMissionDefinitions; - try - { - condensedMissionDefinitions = await missionLoader.GetAvailableMissions( - installationCode - ); - } - catch (InvalidDataException e) - { - logger.LogError(e, "{ErrorMessage}", e.Message); - return BadRequest(e.Message); - } - catch (HttpRequestException e) - { - logger.LogError(e, "Error retrieving missions from Mission Loader"); - return StatusCode(StatusCodes.Status502BadGateway); - } - catch (MissionLoaderUnavailableException e) - { - logger.LogError(e, "Mission loader unavailable: {message}", e.Message); - return StatusCode( - StatusCodes.Status503ServiceUnavailable, - "External API is unavailable" - ); - } - catch (JsonException e) - { - logger.LogError(e, "Error retrieving missions from MissionLoader"); - return StatusCode(StatusCodes.Status500InternalServerError); - } - - return Ok(condensedMissionDefinitions); - } - - /// - /// Lookup mission by Id - /// - /// - /// This mission is loaded from the mission loader - /// - [HttpGet] - [Route("missions/{missionSourceId}")] - [ProducesResponseType(typeof(CondensedMissionDefinition), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - [ProducesResponseType(StatusCodes.Status502BadGateway)] - public async Task> GetMissionById( - [FromRoute] string missionSourceId - ) - { - missionSourceId = Sanitize.SanitizeUserInput(missionSourceId); - - try - { - var mission = await missionLoader.GetMissionById(missionSourceId); - return Ok(mission); - } - catch (HttpRequestException e) - { - if (e.StatusCode.HasValue && (int)e.StatusCode.Value == 404) - { - logger.LogWarning("Could not find mission with id={id}", missionSourceId); - return NotFound("Mission not found"); - } - - logger.LogError(e, "Error getting mission from mission loader"); - return StatusCode(StatusCodes.Status502BadGateway); - } - catch (JsonException e) - { - logger.LogError(e, "Error deserializing mission from mission loader"); - return StatusCode(StatusCodes.Status500InternalServerError); - } - catch (InvalidDataException e) - { - string message = - "Mission invalid: One or more tags are missing associated robot poses."; - logger.LogError(e, message); - return StatusCode(StatusCodes.Status502BadGateway, message); - } - catch (Exception e) - { - logger.LogError(e, "Unexpected error while getting mission definition"); - return StatusCode(StatusCodes.Status500InternalServerError); - } - } - } -} diff --git a/backend/api/Controllers/MissionSchedulingController.cs b/backend/api/Controllers/MissionSchedulingController.cs index 7916353a6..539316221 100644 --- a/backend/api/Controllers/MissionSchedulingController.cs +++ b/backend/api/Controllers/MissionSchedulingController.cs @@ -1,9 +1,6 @@ -using System.Globalization; -using System.Text.Json; -using Api.Controllers.Models; +using Api.Controllers.Models; using Api.Database.Models; using Api.Services; -using Api.Services.MissionLoaders; using Api.Utilities; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -17,235 +14,10 @@ public class MissionSchedulingController( IMissionRunService missionRunService, IMissionSchedulingService missionSchedulingService, IInstallationService installationService, - IMissionLoader missionLoader, ILogger logger, - IRobotService robotService, - ISourceService sourceService, - IInspectionAreaService inspectionAreaService + IRobotService robotService ) : ControllerBase { - /// - /// Schedule a mission based on mission loader - /// - /// - /// This query schedules a new mission and adds it to the database - /// - [HttpPost] - [Authorize(Roles = Role.User)] - [ProducesResponseType(typeof(MissionRun), StatusCodes.Status201Created)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - [ProducesResponseType(StatusCodes.Status409Conflict)] - [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] - public async Task> Create( - [FromBody] ScheduledMissionQuery scheduledMissionQuery - ) - { - scheduledMissionQuery = Sanitize.SanitizeUserInput(scheduledMissionQuery); - - Robot robot; - try - { - robot = await robotService.GetRobotWithSchedulingPreCheck( - scheduledMissionQuery.RobotId, - readOnly: true - ); - } - catch (RobotNotFoundException e) - { - return NotFound(e.Message); - } - catch (RobotPreCheckFailedException e) - { - return BadRequest(e.Message); - } - string missionSourceId = scheduledMissionQuery.MissionSourceId.ToString( - CultureInfo.CurrentCulture - ); - - CondensedMissionDefinition? condensedMissionDefinition; - try - { - condensedMissionDefinition = await missionLoader.GetMissionById(missionSourceId); - if (condensedMissionDefinition == null) - { - return NotFound("Mission not found"); - } - } - catch (HttpRequestException e) - { - if (e.StatusCode.HasValue && (int)e.StatusCode.Value == 404) - { - logger.LogWarning("Could not find mission with id={Id}", missionSourceId); - return NotFound("Mission not found"); - } - - logger.LogError(e, "Error getting mission from mission loader"); - return StatusCode(StatusCodes.Status502BadGateway, $"{e.Message}"); - } - catch (MissionLoaderUnavailableException e) - { - logger.LogError(e, "Mission loader unavailable: {message}", e.Message); - return StatusCode( - StatusCodes.Status503ServiceUnavailable, - "External API is unavailable" - ); - } - catch (JsonException e) - { - const string Message = "Error deserializing mission"; - logger.LogError(e, "{Message}", Message); - return StatusCode(StatusCodes.Status500InternalServerError, Message); - } - catch (InvalidDataException e) - { - const string Message = - "Can not schedule mission because Mission is invalid. One or more tasks does not contain a robot pose"; - logger.LogError(e, "Message: {errorMessage}", Message); - return StatusCode(StatusCodes.Status502BadGateway, Message); - } - catch (Exception e) - { - logger.LogError(e, "Error getting mission from mission loader"); - return StatusCode(StatusCodes.Status500InternalServerError, $"{e.Message}"); - } - - var missionTasks = await missionLoader.GetTasksForMission(missionSourceId); - - if (missionTasks == null) - { - return NotFound("No mission tasks were found for the requested mission"); - } - - InspectionArea? inspectionAreaForMission = null; - try - { - inspectionAreaForMission = - inspectionAreaService.TryFindInspectionAreaForMissionTasks( - missionTasks, - scheduledMissionQuery.InstallationCode - ); - } - catch (MultipleInspectionAreasFoundException e) - { - return BadRequest(e.Message); - } - - if (inspectionAreaForMission == null) - { - return BadRequest("No inspection area found for the mission tasks"); - } - - if (robot.CurrentInspectionAreaId == null) - { - return BadRequest("Robot does not have an inspection area"); - } - - if (inspectionAreaForMission.Id != robot.CurrentInspectionAreaId) - { - return BadRequest( - "The tasks of the mission are not inside the inspection area of the robot" - ); - } - - var source = await sourceService.CheckForExistingSource( - scheduledMissionQuery.MissionSourceId - ); - MissionDefinition? existingMissionDefinition = null; - if (source == null) - { - source = await sourceService.Create( - new Source { SourceId = $"{condensedMissionDefinition.Id}" } - ); - } - else - { - existingMissionDefinition = await missionDefinitionService.ReadBySourceId( - source.SourceId, - readOnly: true - ); - } - - var scheduledMissionDefinition = - existingMissionDefinition - ?? new MissionDefinition - { - Id = Guid.NewGuid().ToString(), - Source = source, - Name = condensedMissionDefinition.Name, - InspectionFrequency = scheduledMissionQuery.InspectionFrequency, - InstallationCode = scheduledMissionQuery.InstallationCode, - InspectionArea = inspectionAreaForMission, - }; - - if (scheduledMissionDefinition.InspectionArea.Id != inspectionAreaForMission.Id) - { - logger.LogWarning( - "Inspection area for mission definition {Id} was changed from {OldInspectionAreaId} to {NewInspectionAreaId}", - scheduledMissionDefinition.Id, - scheduledMissionDefinition.InspectionArea.Id, - inspectionAreaForMission.Id - ); - scheduledMissionDefinition.InspectionArea = inspectionAreaForMission; - } - - var missionRun = new MissionRun - { - Name = condensedMissionDefinition.Name, - Robot = robot, - MissionId = scheduledMissionDefinition.Id, - Status = MissionStatus.Queued, - CreationTime = scheduledMissionQuery.CreationTime ?? DateTime.UtcNow, - Tasks = missionTasks, - InstallationCode = scheduledMissionQuery.InstallationCode, - InspectionArea = scheduledMissionDefinition.InspectionArea, - }; - - if (missionRun.Tasks.Any()) - { - missionRun.SetEstimatedTaskDuration(); - } - - if (existingMissionDefinition == null) - { - await missionDefinitionService.Create(scheduledMissionDefinition); - } - - MissionRun newMissionRun; - try - { - newMissionRun = await missionRunService.Create(missionRun); - } - catch (UnsupportedRobotCapabilityException) - { - return BadRequest( - $"The robot {robot.Name} does not have the necessary sensors to run the mission." - ); - } - - try - { - await missionSchedulingService.StartNextMissionRunIfSystemIsAvailable( - newMissionRun.Robot - ); - } - catch (MissionRunNotFoundException e) - { - logger.LogError( - $"Mission run created but then not found for robot ID: {newMissionRun.Robot.Id}. Exception: {e.Message}" - ); - return StatusCode( - StatusCodes.Status500InternalServerError, - "Not able to create mission run. " - ); - } - - return CreatedAtAction(nameof(Create), new { id = newMissionRun.Id }, newMissionRun); - } - /// /// Rerun a mission run, running only the parts that did not previously complete /// @@ -313,7 +85,7 @@ [FromBody] ScheduleMissionQuery scheduledMissionQuery MissionId = missionRun.MissionId, Status = MissionStatus.Queued, Tasks = missionTasks, - CreationTime = scheduledMissionQuery.CreationTime ?? DateTime.UtcNow, + CreationTime = DateTime.UtcNow, InstallationCode = missionRun.InstallationCode, InspectionArea = missionRun.InspectionArea, }; @@ -403,6 +175,9 @@ [FromBody] ScheduleMissionQuery scheduledMissionQuery return NotFound("Mission definition not found"); } + if (missionDefinition.InspectionArea.Id != robot.CurrentInspectionAreaId) + return BadRequest("Robot is not in the same inspection area as the mission."); + try { await installationService.AssertRobotIsOnSameInstallationAsMission( @@ -419,10 +194,7 @@ await installationService.AssertRobotIsOnSameInstallationAsMission( return Conflict(e.Message); } - var missionTasks = await missionLoader.GetTasksForMission( - missionDefinition.Source.SourceId - ); - if (missionTasks == null) + if (missionDefinition.Tasks == null) return NotFound("No mission tasks were found for the requested mission"); var missionRun = new MissionRun @@ -431,8 +203,8 @@ await installationService.AssertRobotIsOnSameInstallationAsMission( Robot = robot, MissionId = missionDefinition.Id, Status = MissionStatus.Queued, - CreationTime = scheduledMissionQuery.CreationTime ?? DateTime.UtcNow, - Tasks = missionTasks, + CreationTime = DateTime.UtcNow, + Tasks = [.. missionDefinition.Tasks.Select((t) => t.ToMissionRunTask())], InstallationCode = missionDefinition.InstallationCode, InspectionArea = missionDefinition.InspectionArea, }; @@ -473,205 +245,5 @@ await missionSchedulingService.StartNextMissionRunIfSystemIsAvailable( return CreatedAtAction(nameof(Schedule), new { id = newMissionRun.Id }, newMissionRun); } - - /// - /// Schedule a custom mission - /// - /// - /// This query schedules a custom mission defined in the incoming json - /// - [HttpPost] - [Authorize(Roles = Role.User)] - [Route("custom")] - [ProducesResponseType(typeof(MissionRun), StatusCodes.Status201Created)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task> Create( - [FromBody] CustomMissionQuery customMissionQuery - ) - { - customMissionQuery = Sanitize.SanitizeUserInput(customMissionQuery); - - customMissionQuery.InstallationCode = customMissionQuery.InstallationCode.ToUpper(); - - Robot robot; - try - { - robot = await robotService.GetRobotWithSchedulingPreCheck( - customMissionQuery.RobotId, - readOnly: true - ); - } - catch (RobotNotFoundException e) - { - return NotFound(e.Message); - } - catch (RobotPreCheckFailedException e) - { - return BadRequest(e.Message); - } - - var installation = await installationService.ReadByInstallationCode( - customMissionQuery.InstallationCode, - readOnly: true - ); - if (installation == null) - { - return NotFound( - $"Could not find installation with name {customMissionQuery.InstallationCode}" - ); - } - - var missionTasks = customMissionQuery - .Tasks.Select(task => new MissionTask(task)) - .ToList(); - - MissionDefinition? customMissionDefinition; - try - { - var inspectionAreaForMission = - inspectionAreaService.TryFindInspectionAreaForMissionTasks( - missionTasks, - customMissionQuery.InstallationCode - ); - if (inspectionAreaForMission == null) - { - return BadRequest("No inspection area found for the mission tasks"); - } - - if (robot.CurrentInspectionAreaId == null) - { - return BadRequest("Robot does not have an inspection area"); - } - - if (inspectionAreaForMission.Id != robot.CurrentInspectionAreaId) - { - return BadRequest( - "The tasks of the mission are not inside the inspection area of the robot" - ); - } - - var source = await sourceService.CheckForExistingSourceFromTasks(missionTasks); - - MissionDefinition? existingMissionDefinition = null; - if (source == null) - { - source = await sourceService.CreateSourceIfDoesNotExist(missionTasks); - } - else - { - var missionDefinition = await missionDefinitionService.ReadBySourceId( - source.SourceId, - readOnly: true - ); - if (missionDefinition != null) - { - existingMissionDefinition = missionDefinition; - } - } - - customMissionDefinition = - existingMissionDefinition - ?? new MissionDefinition - { - Id = Guid.NewGuid().ToString(), - Source = source, - Name = customMissionQuery.Name, - InspectionFrequency = customMissionQuery.InspectionFrequency, - InstallationCode = customMissionQuery.InstallationCode, - InspectionArea = inspectionAreaForMission, - }; - - if (existingMissionDefinition == null) - { - await missionDefinitionService.Create(customMissionDefinition); - } - } - catch (MultipleInspectionAreasFoundException e) - { - return BadRequest(e.Message); - } - catch (SourceException e) - { - return StatusCode(StatusCodes.Status502BadGateway, e.Message); - } - - try - { - await installationService.AssertRobotIsOnSameInstallationAsMission( - robot, - customMissionDefinition - ); - } - catch (InstallationNotFoundException e) - { - return NotFound(e.Message); - } - catch (RobotNotInSameInstallationAsMissionException e) - { - return Conflict(e.Message); - } - - MissionRun? newMissionRun; - try - { - var scheduledMission = new MissionRun - { - Name = customMissionQuery.Name, - Description = customMissionQuery.Description, - MissionId = customMissionDefinition.Id, - Comment = customMissionQuery.Comment, - Robot = robot, - Status = MissionStatus.Queued, - CreationTime = customMissionQuery.CreationTime ?? DateTime.UtcNow, - Tasks = missionTasks, - InstallationCode = customMissionQuery.InstallationCode, - InspectionArea = customMissionDefinition.InspectionArea, - }; - - if (scheduledMission.Tasks.Any()) - { - scheduledMission.SetEstimatedTaskDuration(); - } - - newMissionRun = await missionRunService.Create(scheduledMission); - } - catch (MissionNotFoundException e) - { - return NotFound(e.Message); - } - catch (RobotNotFoundException e) - { - return NotFound(e.Message); - } - catch (UnsupportedRobotCapabilityException) - { - return BadRequest( - $"The robot {robot.Name} does not have the necessary sensors to run the mission." - ); - } - - try - { - await missionSchedulingService.StartNextMissionRunIfSystemIsAvailable( - newMissionRun.Robot - ); - } - catch (MissionRunNotFoundException e) - { - logger.LogError( - $"Mission run created but then not found for robot ID: {newMissionRun.Robot.Id}. Exception: {e.Message}" - ); - return StatusCode( - StatusCodes.Status500InternalServerError, - "Not able to create mission run. " - ); - } - - return CreatedAtAction(nameof(Create), new { id = newMissionRun.Id }, newMissionRun); - } } } diff --git a/backend/api/Controllers/Models/CustomMissionQuery.cs b/backend/api/Controllers/Models/CustomMissionQuery.cs index 4b0256f8f..014e25887 100644 --- a/backend/api/Controllers/Models/CustomMissionQuery.cs +++ b/backend/api/Controllers/Models/CustomMissionQuery.cs @@ -3,18 +3,23 @@ namespace Api.Controllers.Models { - public struct CustomInspectionQuery + public struct TaskQuery { - public InspectionType InspectionType { get; set; } - - public Position InspectionTarget { get; set; } - - public float? VideoDuration { get; set; } - } - - public struct CustomTaskQuery - { - public int TaskOrder { get; set; } +#nullable disable + public TaskQuery() { } + +#nullable enable + public TaskQuery(TaskDefinition def) + { + TagId = def.TagId; + Description = def.Description; + RobotPose = def.RobotPose; + TargetPosition = def.TargetPosition; + ZoomDescription = def.ZoomDescription; + SensorType = def.SensorType; + AnalysisTypes = def.AnalysisTypes; + VideoDuration = def.VideoDuration; + } public string? TagId { get; set; } @@ -22,29 +27,25 @@ public struct CustomTaskQuery public Pose RobotPose { get; set; } - public IsarZoomDescription? IsarZoomDescription { get; set; } + public Position TargetPosition { get; set; } - public CustomInspectionQuery Inspection { get; set; } - } + public IsarZoomDescription? ZoomDescription { get; set; } - public struct CustomMissionQuery - { - public string RobotId { get; set; } + public SensorType SensorType { get; set; } - public DateTime? CreationTime { get; set; } + public IList AnalysisTypes { get; set; } - public string InstallationCode { get; set; } + public float? VideoDuration { get; set; } + } - public TimeSpan? InspectionFrequency { get; set; } + public struct CreateMissionQuery + { + public string InstallationCode { get; set; } public string Name { get; set; } public string? Description { get; set; } - public string? Comment { get; set; } - - public List Tasks { get; set; } - - public IsarZoomDescription? IsarZoomDescription { get; set; } + public List Tasks { get; set; } } } diff --git a/backend/api/Controllers/Models/MissionDefinitionQueryStringParameters.cs b/backend/api/Controllers/Models/MissionDefinitionQueryStringParameters.cs index c61e03714..11ce53e92 100644 --- a/backend/api/Controllers/Models/MissionDefinitionQueryStringParameters.cs +++ b/backend/api/Controllers/Models/MissionDefinitionQueryStringParameters.cs @@ -22,10 +22,5 @@ public MissionDefinitionQueryStringParameters() /// The search parameter for the mission name /// public string? NameSearch { get; set; } - - /// - /// The search parameter for the mission source id - /// - public string? SourceId { get; set; } } } diff --git a/backend/api/Controllers/Models/MissionDefinitionResponse.cs b/backend/api/Controllers/Models/MissionDefinitionResponse.cs index ddc2e6529..48ee2ccf8 100644 --- a/backend/api/Controllers/Models/MissionDefinitionResponse.cs +++ b/backend/api/Controllers/Models/MissionDefinitionResponse.cs @@ -1,41 +1,32 @@ -using System.Text.Json.Serialization; +using System.Text.Json.Serialization; using Api.Database.Models; -using Api.Services; namespace Api.Controllers.Models { public class MissionDefinitionResponse { - public string Id { get; set; } = string.Empty; - - public string Name { get; set; } = string.Empty; - - public string InstallationCode { get; set; } = string.Empty; - + public string Id { get; set; } + public List Tasks { get; set; } + public string Name { get; set; } + public string InstallationCode { get; set; } public string? Comment { get; set; } - - public TimeSpan? InspectionFrequency { get; set; } - public AutoScheduleFrequency? AutoScheduleFrequency { get; set; } - public virtual MissionRun? LastSuccessfulRun { get; set; } - - public InspectionAreaResponse? InspectionArea { get; set; } - - public bool IsDeprecated { get; set; } - - public string SourceId { get; set; } = string.Empty; + public InspectionAreaResponse InspectionArea { get; set; } [JsonConstructor] +#nullable disable public MissionDefinitionResponse() { } +#nullable enable + public MissionDefinitionResponse(MissionDefinition missionDefinition) { - Id = missionDefinition.Id; - Name = missionDefinition.Name; - InstallationCode = missionDefinition.InstallationCode; + Id = missionDefinition.Id ?? string.Empty; + Tasks = missionDefinition.Tasks; + Name = missionDefinition.Name ?? string.Empty; + InstallationCode = missionDefinition.InstallationCode ?? string.Empty; Comment = missionDefinition.Comment; - InspectionFrequency = missionDefinition.InspectionFrequency; AutoScheduleFrequency = ( missionDefinition.AutoScheduleFrequency is not null @@ -43,43 +34,8 @@ missionDefinition.AutoScheduleFrequency is not null ) ? missionDefinition.AutoScheduleFrequency : null; - InspectionArea = new InspectionAreaResponse(missionDefinition.InspectionArea); LastSuccessfulRun = missionDefinition.LastSuccessfulRun; - IsDeprecated = missionDefinition.IsDeprecated; - SourceId = missionDefinition.Source.SourceId; + InspectionArea = new InspectionAreaResponse(missionDefinition.InspectionArea); } } - - public class MissionDefinitionWithTasksResponse( - IMissionDefinitionTaskService service, - MissionDefinition missionDefinition - ) - { - public string Id { get; } = missionDefinition.Id; - - public List Tasks { get; } = - service.GetTasksFromSource(missionDefinition.Source).Result!; - - public string Name { get; } = missionDefinition.Name; - - public string InstallationCode { get; } = missionDefinition.InstallationCode; - - public string? Comment { get; } = missionDefinition.Comment; - - public TimeSpan? InspectionFrequency { get; } = missionDefinition.InspectionFrequency; - - public AutoScheduleFrequency? AutoScheduleFrequency { get; } = - ( - missionDefinition.AutoScheduleFrequency is not null - && missionDefinition.AutoScheduleFrequency.HasValidValue() - ) - ? missionDefinition.AutoScheduleFrequency - : null; - - public virtual MissionRun? LastSuccessfulRun { get; } = missionDefinition.LastSuccessfulRun; - - public InspectionArea InspectionArea { get; } = missionDefinition.InspectionArea; - - public bool IsDeprecated { get; } = missionDefinition.IsDeprecated; - } } diff --git a/backend/api/Controllers/Models/MissionRunQueryStringParameters.cs b/backend/api/Controllers/Models/MissionRunQueryStringParameters.cs index f4ca14ce8..ea017a65d 100644 --- a/backend/api/Controllers/Models/MissionRunQueryStringParameters.cs +++ b/backend/api/Controllers/Models/MissionRunQueryStringParameters.cs @@ -56,9 +56,9 @@ public MissionRunQueryStringParameters() public string? TagSearch { get; set; } /// - /// Filter for an inspection type in the mission equal to any of InspectionTypes + /// Filter for an inspection type in the mission equal to any of SensorTypes /// - public List? InspectionTypes { get; set; } + public List? InspectionTypes { get; set; } #region Time Filters diff --git a/backend/api/Controllers/Models/PlantInfo.cs b/backend/api/Controllers/Models/PlantInfo.cs deleted file mode 100644 index e2d5803e5..000000000 --- a/backend/api/Controllers/Models/PlantInfo.cs +++ /dev/null @@ -1,25 +0,0 @@ -#nullable disable -using Api.Database.Models; - -namespace Api.Controllers.Models -{ - public class PlantInfo - { - public string PlantCode { get; set; } - public string ProjectDescription { get; set; } - - public PlantInfo() { } - - public PlantInfo(string plantCode, string projectDescription) - { - PlantCode = plantCode; - ProjectDescription = projectDescription; - } - - public PlantInfo(Installation installation) - { - PlantCode = installation.InstallationCode; - ProjectDescription = installation.Name; - } - } -} diff --git a/backend/api/Controllers/Models/ScheduleMissionQuery.cs b/backend/api/Controllers/Models/ScheduleMissionQuery.cs index 7f0b2f86a..be82eda5a 100644 --- a/backend/api/Controllers/Models/ScheduleMissionQuery.cs +++ b/backend/api/Controllers/Models/ScheduleMissionQuery.cs @@ -3,6 +3,5 @@ public class ScheduleMissionQuery { public string RobotId { get; set; } = string.Empty; - public DateTime? CreationTime { get; set; } } } diff --git a/backend/api/Controllers/Models/ScheduledMissionQuery.cs b/backend/api/Controllers/Models/ScheduledMissionQuery.cs index 943c1bd57..d0d54db57 100644 --- a/backend/api/Controllers/Models/ScheduledMissionQuery.cs +++ b/backend/api/Controllers/Models/ScheduledMissionQuery.cs @@ -6,6 +6,5 @@ public struct ScheduledMissionQuery public string MissionSourceId { get; set; } public DateTime? CreationTime { get; set; } public string InstallationCode { get; set; } - public TimeSpan? InspectionFrequency { get; set; } } } diff --git a/backend/api/Controllers/Models/SourceResponse.cs b/backend/api/Controllers/Models/SourceResponse.cs deleted file mode 100644 index d828781c3..000000000 --- a/backend/api/Controllers/Models/SourceResponse.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Api.Database.Models -{ - public class SourceResponse(Source source, IList tasks) - { - public string Id { get; } = source.Id; - - public string SourceId { get; } = source.SourceId; - - public IList Tasks = tasks; - } -} diff --git a/backend/api/Controllers/Models/UpdateMissionDefinitionQuery.cs b/backend/api/Controllers/Models/UpdateMissionDefinitionQuery.cs index 9231e9276..74f350439 100644 --- a/backend/api/Controllers/Models/UpdateMissionDefinitionQuery.cs +++ b/backend/api/Controllers/Models/UpdateMissionDefinitionQuery.cs @@ -24,6 +24,8 @@ public struct UpdateMissionDefinitionQuery /// Will be unchanged if null. Use an empty list to remove all scheduled times. /// public IList? SchedulingTimesCETperWeek { get; set; } + + public IList? Tasks { get; set; } } public struct TimeAndDayQuery diff --git a/backend/api/Controllers/SourceController.cs b/backend/api/Controllers/SourceController.cs deleted file mode 100644 index 839a61021..000000000 --- a/backend/api/Controllers/SourceController.cs +++ /dev/null @@ -1,61 +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("sources")] -public class SourceController(ISourceService sourceService, ILogger logger) - : ControllerBase -{ - /// - /// List all sources in the Flotilla database - /// - /// - /// This query gets all sources - /// - [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>> GetAllSources() - { - List sources; - try - { - sources = await sourceService.ReadAll(readOnly: true); - } - catch (InvalidDataException e) - { - logger.LogError(e, "{Message}", e.Message); - return BadRequest(e.Message); - } - - return Ok(sources); - } - - /// - /// Lookup a custom source by specified id. - /// - [HttpGet] - [Authorize(Roles = Role.Any)] - [Route("{id}")] - [ProducesResponseType(typeof(SourceResponse), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task> GetSourceById([FromRoute] string id) - { - var source = await sourceService.ReadById(id); - if (source == null) - return NotFound($"Could not find source with id {id}"); - return Ok(source); - } -} diff --git a/backend/api/Database/Context/FlotillaDbContext.cs b/backend/api/Database/Context/FlotillaDbContext.cs index 78c254f5b..06289801f 100644 --- a/backend/api/Database/Context/FlotillaDbContext.cs +++ b/backend/api/Database/Context/FlotillaDbContext.cs @@ -1,6 +1,5 @@ using System.Text.Json; using Api.Database.Models; -using Api.Services.MissionLoaders; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.ChangeTracking; using Microsoft.EntityFrameworkCore.Metadata.Builders; @@ -26,7 +25,6 @@ public FlotillaDbContext(DbContextOptions options) public DbSet Installations => Set(); public DbSet InspectionAreas => Set(); public DbSet ExclusionAreas => Set(); - public DbSet Sources => Set(); public DbSet AccessRoles => Set(); public DbSet UserInfos => Set(); public DbSet TagInspectionMetadata => Set(); @@ -61,10 +59,30 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) ); }); - AddConverterForListOfEnums( + AddConverterForNullableListOfEnums( modelBuilder.Entity().Property(r => r.RobotCapabilities) ); + AddConverterForNullableListOfEnums( + modelBuilder.Entity().Property(r => r.AnalysisTypes) + ); + + AddConverterForNullableListOfEnums( + modelBuilder.Entity().Property(r => r.AnalysisTypes) + ); + + modelBuilder + .Entity() + .OwnsMany( + p => p.Tasks, + tasks => + { + tasks.WithOwner(); + tasks.HasKey("MissionDefinitionId", "Index"); + AddConverterForListOfEnums(tasks.Property(t => t.AnalysisTypes)); + } + ); + modelBuilder .Entity() .Property(m => m.InspectionFrequency) @@ -177,7 +195,7 @@ private static void AddConverterForDateTimeOffsets(ref EntityTypeBuilder e } } - private static void AddConverterForListOfEnums( + private static void AddConverterForNullableListOfEnums( PropertyBuilder?> propertyBuilder ) where T : Enum @@ -202,5 +220,29 @@ private static void AddConverterForListOfEnums( ) .Metadata.SetValueComparer(valueComparer); } + + private static void AddConverterForListOfEnums(PropertyBuilder> propertyBuilder) + where T : Enum + { +#pragma warning disable IDE0305 + var valueComparer = new ValueComparer?>( + (c1, c2) => + (c1 == null && c2 == null) + || ((c1 != null == (c2 != null)) && c1!.SequenceEqual(c2!)), + c => c == null ? 0 : c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())), + c => c == null ? null : (IList?)c.ToList() + ); +#pragma warning restore IDE0305 + + propertyBuilder + .HasConversion( + r => r != null ? string.Join(';', r) : "", + r => + r.Split(';', StringSplitOptions.RemoveEmptyEntries) + .Select(r => (T)Enum.Parse(typeof(T), r)) + .ToList() + ) + .Metadata.SetValueComparer(valueComparer); + } } } diff --git a/backend/api/Database/Context/InitDb.cs b/backend/api/Database/Context/InitDb.cs index e8d136868..1f908d550 100644 --- a/backend/api/Database/Context/InitDb.cs +++ b/backend/api/Database/Context/InitDb.cs @@ -1,5 +1,4 @@ using Api.Database.Models; -using Microsoft.EntityFrameworkCore; using TaskStatus = Api.Database.Models.TaskStatus; namespace Api.Database.Context @@ -11,7 +10,6 @@ public static class InitDb private static readonly List plants = GetPlants(); private static readonly List inspectionAreas = GetInspectionAreas(); private static readonly List robots = GetRobots(); - private static readonly List sources = GetSources(); private static readonly List tasks = GetMissionTasks(); private static readonly List missionDefinitions = GetMissionDefinitions(); @@ -20,9 +18,9 @@ public static class InitDb private static List GetInspections() { - var inspection1 = new Inspection { InspectionType = InspectionType.Image }; + var inspection1 = new Inspection { InspectionType = SensorType.Image }; - var inspection2 = new Inspection { InspectionType = InspectionType.ThermalImage }; + var inspection2 = new Inspection { InspectionType = SensorType.ThermalImage }; return new List([inspection1, inspection2]); } @@ -146,17 +144,6 @@ private static List GetInspectionAreas() ]); } - private static List GetSources() - { - var source1 = new Source { SourceId = "986" }; - - var source2 = new Source { SourceId = "990" }; - - var source3 = new Source { SourceId = "991" }; - - return new List([source1, source2, source3]); - } - private static List GetRobots() { var robot1 = new Robot @@ -216,6 +203,56 @@ private static List GetRobots() return new List([robot1, robot2, robot3, robot4]); } + private static List GetMissionTaskDefinitions() + { + var task1 = new TaskDefinition + { + Index = 1, + TagId = "dummy tag id 1", + Description = "dummy task 1", + RobotPose = new Pose(), + AnalysisTypes = [AnalysisType.Fencilla], + TargetPosition = new Position + { + X = 0, + Y = 0, + Z = 0, + }, + }; + + var task2 = new TaskDefinition + { + Index = 2, + TagId = "dummy tag id 1", + Description = "dummy task 2", + RobotPose = new Pose(), + AnalysisTypes = [AnalysisType.CLOE], + TargetPosition = new Position + { + X = 0, + Y = 0, + Z = 0, + }, + }; + + var task3 = new TaskDefinition + { + Index = 3, + TagId = "dummy tag id 2", + Description = "dummy task 3", + RobotPose = new Pose(), + AnalysisTypes = [AnalysisType.CO2], + TargetPosition = new Position + { + X = 0, + Y = 0, + Z = 0, + }, + }; + + return new List([task1, task2, task3]); + } + private static List GetMissionDefinitions() { var missionDefinition1 = new MissionDefinition @@ -224,7 +261,12 @@ private static List GetMissionDefinitions() Name = "Placeholder Mission 1", InstallationCode = inspectionAreas[0].Installation!.InstallationCode, InspectionArea = inspectionAreas[0], - Source = sources[0], + Tasks = + [ + GetMissionTaskDefinitions()[0], + GetMissionTaskDefinitions()[1], + GetMissionTaskDefinitions()[2], + ], Comment = "Interesting comment", InspectionFrequency = new DateTime().AddDays(12) - new DateTime(), LastSuccessfulRun = null, @@ -236,7 +278,7 @@ private static List GetMissionDefinitions() Name = "Placeholder Mission 2", InstallationCode = inspectionAreas[1].Installation!.InstallationCode, InspectionArea = inspectionAreas[1], - Source = sources[1], + Tasks = [GetMissionTaskDefinitions()[0], GetMissionTaskDefinitions()[2]], InspectionFrequency = new DateTime().AddDays(7) - new DateTime(), LastSuccessfulRun = null, }; @@ -247,7 +289,7 @@ private static List GetMissionDefinitions() Name = "Placeholder Mission 3", InstallationCode = inspectionAreas[1].Installation!.InstallationCode, InspectionArea = inspectionAreas[1], - Source = sources[2], + Tasks = [GetMissionTaskDefinitions()[1], GetMissionTaskDefinitions()[2]], LastSuccessfulRun = null, }; @@ -258,7 +300,7 @@ private static List GetMissionDefinitions() InstallationCode = inspectionAreas[2].Installation.InstallationCode, InspectionFrequency = new DateTime().AddDays(90) - new DateTime(), InspectionArea = inspectionAreas[2], - Source = sources[2], + Tasks = [GetMissionTaskDefinitions()[0]], LastSuccessfulRun = null, }; @@ -269,7 +311,7 @@ private static List GetMissionDefinitions() InstallationCode = inspectionAreas[2].Installation.InstallationCode, InspectionFrequency = new DateTime().AddDays(35) - new DateTime(), InspectionArea = inspectionAreas[2], - Source = sources[2], + Tasks = [GetMissionTaskDefinitions()[1]], LastSuccessfulRun = null, }; @@ -280,16 +322,7 @@ private static List GetMissionDefinitions() InstallationCode = inspectionAreas[3].Installation.InstallationCode, InspectionFrequency = new DateTime().AddDays(4) - new DateTime(), InspectionArea = inspectionAreas[3], - Source = sources[2], - LastSuccessfulRun = null, - }; - _ = new MissionDefinition - { - Id = Guid.NewGuid().ToString(), - Name = "Placeholder Mission 7", - InstallationCode = inspectionAreas[3].Installation.InstallationCode, - InspectionArea = inspectionAreas[4], - Source = sources[2], + Tasks = [GetMissionTaskDefinitions()[2]], LastSuccessfulRun = null, }; @@ -305,83 +338,68 @@ private static List GetMissionDefinitions() private static List GetMissionTasks() { - var url = new Uri("https://dummyurl/tag?tagNo=ABCD"); - var task1 = new MissionTask( - inspection: new Inspection(), - robotPose: new Pose(300.0f, 50.0f, 200.0f, 0.0f, 0.0f, 0.0f, 1.0f), - taskOrder: 0, - tagLink: url, - tagId: "ABCD", - taskDescription: "Task description", - poseId: 2, - status: TaskStatus.Successful - ); - - var task2 = new MissionTask( - inspection: new Inspection(), - robotPose: new Pose(300.0f, 50.0f, 200.0f, 0.0f, 0.0f, 0.0f, 1.0f), - taskOrder: 0, - tagLink: url, - tagId: "ABCDE", - taskDescription: "Task description", - poseId: 2, - status: TaskStatus.Failed - ); - - var task3 = new MissionTask( - inspection: new Inspection(), - robotPose: new Pose(300.0f, 50.0f, 200.0f, 0.0f, 0.0f, 0.0f, 1.0f), - taskOrder: 0, - tagLink: url, - tagId: "ABCDEF", - taskDescription: "Task description", - poseId: 2, - status: TaskStatus.PartiallySuccessful - ); - - var task4 = new MissionTask( - inspection: new Inspection(), - robotPose: new Pose(300.0f, 50.0f, 200.0f, 0.0f, 0.0f, 0.0f, 1.0f), - taskOrder: 0, - tagLink: url, - tagId: "ABCDEFG", - taskDescription: "Task description", - poseId: 2, - status: TaskStatus.Cancelled - ); - - var task5 = new MissionTask( - inspection: new Inspection(), - robotPose: new Pose(300.0f, 50.0f, 200.0f, 0.0f, 0.0f, 0.0f, 1.0f), - taskOrder: 0, - tagLink: url, - tagId: "ABCDEFGH", - taskDescription: "Task description", - poseId: 2, - status: TaskStatus.Failed - ); - - var task6 = new MissionTask( - inspection: new Inspection(), - robotPose: new Pose(300.0f, 50.0f, 200.0f, 0.0f, 0.0f, 0.0f, 1.0f), - taskOrder: 0, - tagLink: url, - tagId: "ABCDEFGHI", - taskDescription: "Task description", - poseId: 2, - status: TaskStatus.Failed - ); - - var task7 = new MissionTask( - inspection: new Inspection(), - robotPose: new Pose(300.0f, 50.0f, 200.0f, 0.0f, 0.0f, 0.0f, 1.0f), - taskOrder: 0, - tagLink: url, - tagId: "ABCDEFGHIJ", - taskDescription: "Task description", - poseId: 2, - status: TaskStatus.Failed - ); + var task1 = new MissionTask + { + Inspection = new Inspection(), + RobotPose = new Pose(300.0f, 50.0f, 200.0f, 0.0f, 0.0f, 0.0f, 1.0f), + TagId = "ABCD", + Description = "Task description", + Status = TaskStatus.Successful, + }; + + var task2 = new MissionTask + { + Inspection = new Inspection(), + RobotPose = new Pose(300.0f, 50.0f, 200.0f, 0.0f, 0.0f, 0.0f, 1.0f), + TagId = "ABCDE", + Description = "Task description", + Status = TaskStatus.Failed, + }; + + var task3 = new MissionTask + { + Inspection = new Inspection(), + RobotPose = new Pose(300.0f, 50.0f, 200.0f, 0.0f, 0.0f, 0.0f, 1.0f), + TagId = "ABCDEF", + Description = "Task description", + Status = TaskStatus.PartiallySuccessful, + }; + + var task4 = new MissionTask + { + Inspection = new Inspection(), + RobotPose = new Pose(300.0f, 50.0f, 200.0f, 0.0f, 0.0f, 0.0f, 1.0f), + TagId = "ABCDEFG", + Description = "Task description", + Status = TaskStatus.Cancelled, + }; + + var task5 = new MissionTask + { + Inspection = new Inspection(), + RobotPose = new Pose(300.0f, 50.0f, 200.0f, 0.0f, 0.0f, 0.0f, 1.0f), + TagId = "ABCDEFGH", + Description = "Task description", + Status = TaskStatus.Failed, + }; + + var task6 = new MissionTask + { + Inspection = new Inspection(), + RobotPose = new Pose(300.0f, 50.0f, 200.0f, 0.0f, 0.0f, 0.0f, 1.0f), + TagId = "ABCDEFGHI", + Description = "Task description", + Status = TaskStatus.Failed, + }; + + var task7 = new MissionTask + { + Inspection = new Inspection(), + RobotPose = new Pose(300.0f, 50.0f, 200.0f, 0.0f, 0.0f, 0.0f, 1.0f), + TagId = "ABCDEFGHIJ", + Description = "Task description", + Status = TaskStatus.Failed, + }; return [task1, task2, task3, task4, task5, task6, task7]; } @@ -501,7 +519,6 @@ public static void PopulateDb(FlotillaDbContext context) context.AddRange(robots); context.AddRange(plants); context.AddRange(inspectionAreas); - context.AddRange(sources); var tasks = GetMissionTasks(); foreach (var task in tasks) diff --git a/backend/api/Database/Models/Inspection.cs b/backend/api/Database/Models/Inspection.cs index a0e4e15f0..2212deba6 100644 --- a/backend/api/Database/Models/Inspection.cs +++ b/backend/api/Database/Models/Inspection.cs @@ -1,6 +1,5 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; -using Api.Controllers.Models; using Api.Services.Models; #pragma warning disable CS8618 namespace Api.Database.Models @@ -9,28 +8,24 @@ public class Inspection { public Inspection() { - InspectionType = InspectionType.Image; + InspectionType = SensorType.Image; InspectionTarget = new Position(); + AnalysisTypes = []; } public Inspection( - InspectionType inspectionType, + SensorType sensorType, + Position target, + IList analysisTypes, float? videoDuration, - Position inspectionTarget, - string? inspectionTargetName + string? taskDescription = null ) { - InspectionType = inspectionType; + InspectionType = sensorType; + InspectionTarget = target; VideoDuration = videoDuration; - InspectionTarget = inspectionTarget; - InspectionTargetName = inspectionTargetName; - } - - public Inspection(CustomInspectionQuery inspectionQuery) - { - InspectionType = inspectionQuery.InspectionType; - InspectionTarget = inspectionQuery.InspectionTarget; - VideoDuration = inspectionQuery.VideoDuration; + AnalysisTypes = analysisTypes; + TaskDescription = taskDescription; } // Creates a blank deepcopy of the provided inspection @@ -42,6 +37,8 @@ public Inspection(Inspection copy, bool useEmptyID = false) VideoDuration = copy.VideoDuration; InspectionUrl = copy.InspectionUrl; InspectionTarget = new Position(copy.InspectionTarget); + TaskDescription = copy.TaskDescription; + AnalysisTypes = copy.AnalysisTypes; } [Key] @@ -55,10 +52,12 @@ public Inspection(Inspection copy, bool useEmptyID = false) [Required] public Position InspectionTarget { get; set; } - public string? InspectionTargetName { get; set; } + public IList? AnalysisTypes { get; set; } [Required] - public InspectionType InspectionType { get; set; } + public SensorType InspectionType { get; set; } + + public string? TaskDescription { get; set; } public AnalysisResult AnalysisResult { get; set; } @@ -75,28 +74,28 @@ public void UpdateWithIsarInfo(IsarTask isarTask) } } - public bool IsSupportedInspectionType(IList capabilities) + public bool IsSupportedSensorType(IList capabilities) { return InspectionType switch { - InspectionType.Image => capabilities.Contains(RobotCapabilitiesEnum.take_image), - InspectionType.ThermalImage => capabilities.Contains( + SensorType.Image => capabilities.Contains(RobotCapabilitiesEnum.take_image), + SensorType.ThermalImage => capabilities.Contains( RobotCapabilitiesEnum.take_thermal_image ), - InspectionType.Video => capabilities.Contains(RobotCapabilitiesEnum.take_video), - InspectionType.ThermalVideo => capabilities.Contains( + SensorType.Video => capabilities.Contains(RobotCapabilitiesEnum.take_video), + SensorType.ThermalVideo => capabilities.Contains( RobotCapabilitiesEnum.take_thermal_video ), - InspectionType.CO2Measurement => capabilities.Contains( + SensorType.CO2Measurement => capabilities.Contains( RobotCapabilitiesEnum.take_co2_measurement ), - InspectionType.Audio => capabilities.Contains(RobotCapabilitiesEnum.record_audio), + SensorType.Audio => capabilities.Contains(RobotCapabilitiesEnum.record_audio), _ => false, }; } } - public enum InspectionType + public enum SensorType { Image, ThermalImage, diff --git a/backend/api/Database/Models/MissionDefinition.cs b/backend/api/Database/Models/MissionDefinition.cs index 65f0c77c0..3a276074e 100644 --- a/backend/api/Database/Models/MissionDefinition.cs +++ b/backend/api/Database/Models/MissionDefinition.cs @@ -10,8 +10,14 @@ public class MissionDefinition : SortableRecord [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public string Id { get; set; } + private IList _tasks; + [Required] - public Source Source { get; set; } + public List Tasks + { + get => _tasks != null ? [.. _tasks.OrderBy(t => t.Index)] : []; + set => _tasks = value; + } [Required] [MaxLength(200)] diff --git a/backend/api/Database/Models/MissionRun.cs b/backend/api/Database/Models/MissionRun.cs index 9d4343b4c..7574157fa 100644 --- a/backend/api/Database/Models/MissionRun.cs +++ b/backend/api/Database/Models/MissionRun.cs @@ -1,6 +1,5 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; -using Api.Services.Models; #pragma warning disable CS8618 namespace Api.Database.Models { @@ -55,10 +54,7 @@ public MissionStatus Status [Required] public IList Tasks { - get => - _tasks != null - ? _tasks.OrderBy(t => t.TaskOrder).ToList() - : new List(); + get => _tasks != null ? _tasks.OrderBy(t => t.TaskOrder).ToList() : []; set => _tasks = value; } diff --git a/backend/api/Database/Models/MissionTask.cs b/backend/api/Database/Models/MissionTask.cs index 2519be7ec..9b0ca8152 100644 --- a/backend/api/Database/Models/MissionTask.cs +++ b/backend/api/Database/Models/MissionTask.cs @@ -1,11 +1,6 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; -using System.Security.Cryptography; -using System.Text; -using System.Text.Json; -using Api.Controllers.Models; using Api.Services.Models; -using Api.Utilities; #pragma warning disable CS8618 namespace Api.Database.Models { @@ -16,62 +11,16 @@ public class MissionTask // ReSharper disable once NotNullOrRequiredMemberIsNotInitialized public MissionTask() { } - // ReSharper disable once NotNullOrRequiredMemberIsNotInitialized - public MissionTask( - Inspection? inspection, - Pose robotPose, - int taskOrder, - Uri? tagLink, - string? tagId, - int? poseId, - string? taskDescription, - IsarZoomDescription? zoomDescription = null, - TaskStatus status = TaskStatus.NotStarted - ) - { - TagLink = tagLink; - TagId = tagId; - RobotPose = robotPose; - PoseId = poseId; - TaskOrder = taskOrder; - Description = taskDescription; - Status = status; - IsarZoomDescription = zoomDescription; - if (inspection != null) - Inspection = new Inspection(inspection); - } - - public MissionTask(CustomTaskQuery taskQuery) - { - TagId = taskQuery.TagId; - Description = taskQuery.Description; - RobotPose = taskQuery.RobotPose; - TaskOrder = taskQuery.TaskOrder; - Status = TaskStatus.NotStarted; - IsarZoomDescription = taskQuery.IsarZoomDescription; - Inspection = new Inspection(taskQuery.Inspection); - } - - public MissionTask(Pose robotPose) - { - Description = "Inspection"; - RobotPose = robotPose; - TaskOrder = 0; - Status = TaskStatus.NotStarted; - Inspection = new Inspection(); - } - // Creates a copy of the provided task public MissionTask(MissionTask copy) { TaskOrder = copy.TaskOrder; TagId = copy.TagId; Description = copy.Description; - TagLink = copy.TagLink; RobotPose = new Pose(copy.RobotPose); - PoseId = copy.PoseId; Status = TaskStatus.NotStarted; IsarZoomDescription = copy.IsarZoomDescription; + AnalysisTypes = copy.AnalysisTypes; if (copy.Inspection is not null) { Inspection = new Inspection(copy.Inspection); @@ -91,13 +40,10 @@ public MissionTask(MissionTask copy) [MaxLength(500)] public string? Description { get; set; } - [MaxLength(200)] - public Uri? TagLink { get; set; } - [Required] public Pose RobotPose { get; set; } - public int? PoseId { get; set; } + public IList? AnalysisTypes { get; set; } = []; [Required] public TaskStatus Status @@ -163,24 +109,17 @@ public static string GetIsarInspectionTaskType() return "inspection"; } - public static string CalculateHashFromTasks(IList tasks) + public TaskDefinition ToMissionTaskDefinition() { - var genericTasks = new List(); - foreach (var task in tasks) + return new TaskDefinition { - var taskCopy = new MissionTask(task) { Id = "" }; - if (taskCopy.Inspection is not null) - taskCopy.Inspection = new Inspection(taskCopy.Inspection, useEmptyID: true); - - genericTasks.Add(taskCopy); - } - - string json = JsonSerializer.Serialize(genericTasks); - byte[] hash = SHA256.HashData(Encoding.UTF8.GetBytes(json)); - return BitConverter - .ToString(hash) - .Replace("-", "", StringComparison.CurrentCulture) - .ToUpperInvariant(); + Index = this.TaskOrder, + TagId = this.TagId, + Description = this.Description, + RobotPose = this.RobotPose, + ZoomDescription = this.IsarZoomDescription, + AnalysisTypes = this.AnalysisTypes ?? [], + }; } } diff --git a/backend/api/Database/Models/Source.cs b/backend/api/Database/Models/Source.cs deleted file mode 100644 index a06a1856a..000000000 --- a/backend/api/Database/Models/Source.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -#pragma warning disable CS8618 -namespace Api.Database.Models -{ - public class Source - { - [Key] - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public string Id { get; set; } - - [Required] - public string SourceId { get; set; } - - public string? CustomMissionTasks { get; set; } - } -} diff --git a/backend/api/Database/Models/TagInspectionMetadata.cs b/backend/api/Database/Models/TagInspectionMetadata.cs index 8b651c0b6..638a94cd4 100644 --- a/backend/api/Database/Models/TagInspectionMetadata.cs +++ b/backend/api/Database/Models/TagInspectionMetadata.cs @@ -1,8 +1,8 @@ -#nullable disable +#nullable disable using System.ComponentModel.DataAnnotations; using Api.Services.Models; -namespace Api.Services.MissionLoaders +namespace Api.Database.Models { public class TagInspectionMetadata { diff --git a/backend/api/Database/Models/TaskDefinition.cs b/backend/api/Database/Models/TaskDefinition.cs new file mode 100644 index 000000000..591710779 --- /dev/null +++ b/backend/api/Database/Models/TaskDefinition.cs @@ -0,0 +1,77 @@ +using System.ComponentModel.DataAnnotations; +using Api.Controllers.Models; +using Api.Services.Models; +using Microsoft.EntityFrameworkCore; +#pragma warning disable CS8618 +namespace Api.Database.Models +{ + public enum AnalysisType + { + Fencilla, + CLOE, + ThermalReading, + CO2, + } + + [Owned] + public class TaskDefinition + { + public TaskDefinition() { } + + public TaskDefinition(TaskQuery taskQuery, int index) + { + Index = index; + TagId = taskQuery.TagId; + Description = taskQuery.Description; + RobotPose = taskQuery.RobotPose; + ZoomDescription = taskQuery.ZoomDescription; + AnalysisTypes = taskQuery.AnalysisTypes; + SensorType = taskQuery.SensorType; + TargetPosition = taskQuery.TargetPosition; + VideoDuration = taskQuery.VideoDuration; + } + + public int Index { get; set; } + + [MaxLength(200)] + public string? TagId { get; set; } + + [MaxLength(500)] + public string? Description { get; set; } + + [Required] + public Pose RobotPose { get; set; } + + [Required] + public Position TargetPosition { get; set; } + + public IsarZoomDescription? ZoomDescription { get; set; } + + public IList AnalysisTypes { get; set; } = []; + + public SensorType SensorType { get; set; } + + public float? VideoDuration { get; set; } + + public MissionTask ToMissionRunTask() + { + return new MissionTask + { + TaskOrder = this.Index, + TagId = this.TagId, + Description = this.Description, + RobotPose = this.RobotPose, + Status = TaskStatus.NotStarted, + IsarZoomDescription = this.ZoomDescription, + AnalysisTypes = this.AnalysisTypes, + Inspection = new Inspection( + this.SensorType, + this.TargetPosition, + this.AnalysisTypes, + this.VideoDuration, + Description + ), + }; + } + } +} diff --git a/backend/api/HostedServices/AutoSchedulingHostedService.cs b/backend/api/HostedServices/AutoSchedulingHostedService.cs index f4ecddaa6..1f8424321 100644 --- a/backend/api/HostedServices/AutoSchedulingHostedService.cs +++ b/backend/api/HostedServices/AutoSchedulingHostedService.cs @@ -1,7 +1,6 @@ using Api.Controllers.Models; using Api.Database.Models; using Api.Services; -using Api.Services.MissionLoaders; namespace Api.HostedServices { @@ -22,9 +21,6 @@ IServiceScopeFactory scopeFactory private IMissionRunService MissionRunService => _scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); - private IMissionLoader MissionLoader => - _scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); - private IRobotService RobotService => _scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); diff --git a/backend/api/Migrations/20260513110434_AddTaskDefinitionRefactor.Designer.cs b/backend/api/Migrations/20260513110434_AddTaskDefinitionRefactor.Designer.cs new file mode 100644 index 000000000..e62e5bb9e --- /dev/null +++ b/backend/api/Migrations/20260513110434_AddTaskDefinitionRefactor.Designer.cs @@ -0,0 +1,1081 @@ +// +using System; +using Api.Database.Context; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Api.Migrations +{ + [DbContext(typeof(FlotillaDbContext))] + [Migration("20260513110434_AddTaskDefinitionRefactor")] + partial class AddTaskDefinitionRefactor + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Api.Database.Models.AccessRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("AccessLevel") + .IsRequired() + .HasColumnType("text"); + + b.Property("InstallationId") + .HasColumnType("text"); + + b.Property("RoleName") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("InstallationId"); + + b.ToTable("AccessRoles"); + }); + + modelBuilder.Entity("Api.Database.Models.AnalysisResult", b => + { + b.Property("InspectionId") + .HasColumnType("text"); + + b.Property("AnalysisType") + .IsRequired() + .HasColumnType("text"); + + b.Property("BlobContainer") + .HasColumnType("text"); + + b.Property("BlobName") + .HasColumnType("text"); + + b.Property("Confidence") + .HasColumnType("real"); + + b.Property("StorageAccount") + .HasColumnType("text"); + + b.Property("Unit") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.Property("Warning") + .HasColumnType("text"); + + b.HasKey("InspectionId"); + + b.ToTable("AnalysisResults"); + }); + + modelBuilder.Entity("Api.Database.Models.AutoScheduleFrequency", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("AutoScheduledJobs") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("AutoScheduleFrequency"); + }); + + modelBuilder.Entity("Api.Database.Models.ExclusionArea", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("InstallationId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("PlantId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("InstallationId"); + + b.HasIndex("PlantId"); + + b.ToTable("ExclusionAreas"); + }); + + modelBuilder.Entity("Api.Database.Models.Inspection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("AnalysisTypes") + .HasColumnType("text"); + + b.Property("InspectionType") + .IsRequired() + .HasColumnType("text"); + + b.Property("InspectionUrl") + .HasMaxLength(250) + .HasColumnType("character varying(250)"); + + b.Property("IsarInspectionId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("TaskDescription") + .HasColumnType("text"); + + b.Property("VideoDuration") + .HasColumnType("real"); + + b.HasKey("Id"); + + b.ToTable("Inspections"); + }); + + modelBuilder.Entity("Api.Database.Models.InspectionArea", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("InstallationId") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsDeprecated") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("PlantId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("InstallationId"); + + b.HasIndex("PlantId"); + + b.ToTable("InspectionAreas"); + }); + + modelBuilder.Entity("Api.Database.Models.Installation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("InstallationCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.HasKey("Id"); + + b.HasIndex("InstallationCode") + .IsUnique(); + + b.ToTable("Installations"); + }); + + modelBuilder.Entity("Api.Database.Models.MissionDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("AutoScheduleFrequencyId") + .HasColumnType("text"); + + b.Property("Comment") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("InspectionAreaId") + .IsRequired() + .HasColumnType("text"); + + b.Property("InspectionFrequency") + .HasColumnType("bigint"); + + b.Property("InstallationCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsDeprecated") + .HasColumnType("boolean"); + + b.Property("LastSuccessfulRunId") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.HasKey("Id"); + + b.HasIndex("AutoScheduleFrequencyId"); + + b.HasIndex("InspectionAreaId"); + + b.HasIndex("LastSuccessfulRunId"); + + b.ToTable("MissionDefinitions"); + }); + + modelBuilder.Entity("Api.Database.Models.MissionRun", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("Comment") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("CreationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.Property("EndTime") + .HasColumnType("timestamp with time zone"); + + b.Property("EstimatedTaskDuration") + .HasColumnType("bigint"); + + b.Property("InspectionAreaId") + .IsRequired() + .HasColumnType("text"); + + b.Property("InstallationCode") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("IsDeprecated") + .HasColumnType("boolean"); + + b.Property("MissionId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RobotId") + .IsRequired() + .HasColumnType("text"); + + b.Property("StartTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("StatusReason") + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.HasKey("Id"); + + b.HasIndex("InspectionAreaId"); + + b.HasIndex("RobotId"); + + b.ToTable("MissionRuns"); + }); + + modelBuilder.Entity("Api.Database.Models.MissionTask", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("AnalysisTypes") + .HasColumnType("text"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("EndTime") + .HasColumnType("timestamp with time zone"); + + b.Property("ErrorDescription") + .HasColumnType("text"); + + b.Property("InspectionId") + .HasColumnType("text"); + + b.Property("MissionRunId") + .HasColumnType("text"); + + b.Property("StartTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("TagId") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("TaskOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("InspectionId"); + + b.HasIndex("MissionRunId"); + + b.ToTable("MissionTasks"); + }); + + modelBuilder.Entity("Api.Database.Models.Plant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("InstallationId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("PlantCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.HasKey("Id"); + + b.HasIndex("InstallationId"); + + b.HasIndex("PlantCode") + .IsUnique(); + + b.ToTable("Plants"); + }); + + modelBuilder.Entity("Api.Database.Models.Robot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("AverageDurationPerTag") + .HasColumnType("real"); + + b.Property("CurrentInspectionAreaId") + .HasColumnType("text"); + + b.Property("CurrentInstallationId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CurrentMissionId") + .HasColumnType("text"); + + b.Property("Deprecated") + .HasColumnType("boolean"); + + b.Property("Host") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("IsarId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Port") + .HasColumnType("integer"); + + b.Property("RobotCapabilities") + .HasColumnType("text"); + + b.Property("SerialNumber") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CurrentInstallationId"); + + b.ToTable("Robots"); + }); + + modelBuilder.Entity("Api.Database.Models.TagInspectionMetadata", b => + { + b.Property("TagId") + .HasColumnType("text"); + + b.HasKey("TagId"); + + b.ToTable("TagInspectionMetadata"); + }); + + modelBuilder.Entity("Api.Database.Models.UserInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("Oid") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("UserInfos"); + }); + + modelBuilder.Entity("Api.Database.Models.AccessRole", b => + { + b.HasOne("Api.Database.Models.Installation", "Installation") + .WithMany() + .HasForeignKey("InstallationId"); + + b.Navigation("Installation"); + }); + + modelBuilder.Entity("Api.Database.Models.AnalysisResult", b => + { + b.HasOne("Api.Database.Models.Inspection", null) + .WithOne("AnalysisResult") + .HasForeignKey("Api.Database.Models.AnalysisResult", "InspectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Api.Database.Models.AutoScheduleFrequency", b => + { + b.OwnsMany("Api.Database.Models.TimeAndDay", "SchedulingTimesCETperWeek", b1 => + { + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b1.Property("AutoScheduleFrequencyId") + .IsRequired() + .HasColumnType("text"); + + b1.Property("DayOfWeek") + .IsRequired() + .HasColumnType("text"); + + b1.Property("TimeOfDay") + .HasColumnType("time without time zone"); + + b1.HasKey("Id"); + + b1.HasIndex("AutoScheduleFrequencyId"); + + b1.ToTable("TimeAndDay"); + + b1.WithOwner() + .HasForeignKey("AutoScheduleFrequencyId"); + }); + + b.Navigation("SchedulingTimesCETperWeek"); + }); + + modelBuilder.Entity("Api.Database.Models.ExclusionArea", b => + { + b.HasOne("Api.Database.Models.Installation", "Installation") + .WithMany() + .HasForeignKey("InstallationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Api.Database.Models.Plant", "Plant") + .WithMany() + .HasForeignKey("PlantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsOne("Api.Database.Models.AreaPolygon", "AreaPolygon", b1 => + { + b1.Property("ExclusionAreaId") + .HasColumnType("text"); + + b1.Property("Positions") + .IsRequired() + .HasColumnType("text") + .HasJsonPropertyName("positions"); + + b1.Property("ZMax") + .HasColumnType("double precision") + .HasJsonPropertyName("zmax"); + + b1.Property("ZMin") + .HasColumnType("double precision") + .HasJsonPropertyName("zmin"); + + b1.HasKey("ExclusionAreaId"); + + b1.ToTable("ExclusionAreas"); + + b1.WithOwner() + .HasForeignKey("ExclusionAreaId"); + }); + + b.Navigation("AreaPolygon") + .IsRequired(); + + b.Navigation("Installation"); + + b.Navigation("Plant"); + }); + + modelBuilder.Entity("Api.Database.Models.Inspection", b => + { + b.OwnsOne("Api.Database.Models.Position", "InspectionTarget", b1 => + { + b1.Property("InspectionId") + .HasColumnType("text"); + + b1.Property("X") + .HasColumnType("real"); + + b1.Property("Y") + .HasColumnType("real"); + + b1.Property("Z") + .HasColumnType("real"); + + b1.HasKey("InspectionId"); + + b1.ToTable("Inspections"); + + b1.WithOwner() + .HasForeignKey("InspectionId"); + }); + + b.Navigation("InspectionTarget") + .IsRequired(); + }); + + modelBuilder.Entity("Api.Database.Models.InspectionArea", b => + { + b.HasOne("Api.Database.Models.Installation", "Installation") + .WithMany() + .HasForeignKey("InstallationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Api.Database.Models.Plant", "Plant") + .WithMany() + .HasForeignKey("PlantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.OwnsOne("Api.Database.Models.AreaPolygon", "AreaPolygon", b1 => + { + b1.Property("InspectionAreaId") + .HasColumnType("text"); + + b1.Property("Positions") + .IsRequired() + .HasColumnType("text") + .HasJsonPropertyName("positions"); + + b1.Property("ZMax") + .HasColumnType("double precision") + .HasJsonPropertyName("zmax"); + + b1.Property("ZMin") + .HasColumnType("double precision") + .HasJsonPropertyName("zmin"); + + b1.HasKey("InspectionAreaId"); + + b1.ToTable("InspectionAreas"); + + b1.WithOwner() + .HasForeignKey("InspectionAreaId"); + }); + + b.Navigation("AreaPolygon"); + + b.Navigation("Installation"); + + b.Navigation("Plant"); + }); + + modelBuilder.Entity("Api.Database.Models.MissionDefinition", b => + { + b.HasOne("Api.Database.Models.AutoScheduleFrequency", "AutoScheduleFrequency") + .WithMany() + .HasForeignKey("AutoScheduleFrequencyId"); + + b.HasOne("Api.Database.Models.InspectionArea", "InspectionArea") + .WithMany() + .HasForeignKey("InspectionAreaId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Api.Database.Models.MissionRun", "LastSuccessfulRun") + .WithMany() + .HasForeignKey("LastSuccessfulRunId"); + + b.OwnsMany("Api.Database.Models.TaskDefinition", "Tasks", b1 => + { + b1.Property("MissionDefinitionId") + .HasColumnType("text"); + + b1.Property("Index") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Index")); + + b1.Property("AnalysisTypes") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b1.Property("SensorType") + .IsRequired() + .HasColumnType("text"); + + b1.Property("TagId") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b1.Property("VideoDuration") + .HasColumnType("real"); + + b1.HasKey("MissionDefinitionId", "Index"); + + b1.ToTable("TaskDefinition"); + + b1.WithOwner() + .HasForeignKey("MissionDefinitionId"); + + b1.OwnsOne("Api.Database.Models.Pose", "RobotPose", b2 => + { + b2.Property("TaskDefinitionMissionDefinitionId") + .HasColumnType("text"); + + b2.Property("TaskDefinitionIndex") + .HasColumnType("integer"); + + b2.HasKey("TaskDefinitionMissionDefinitionId", "TaskDefinitionIndex"); + + b2.ToTable("TaskDefinition"); + + b2.WithOwner() + .HasForeignKey("TaskDefinitionMissionDefinitionId", "TaskDefinitionIndex"); + + b2.OwnsOne("Api.Database.Models.Orientation", "Orientation", b3 => + { + b3.Property("PoseTaskDefinitionMissionDefinitionId") + .HasColumnType("text"); + + b3.Property("PoseTaskDefinitionIndex") + .HasColumnType("integer"); + + b3.Property("W") + .HasColumnType("real"); + + b3.Property("X") + .HasColumnType("real"); + + b3.Property("Y") + .HasColumnType("real"); + + b3.Property("Z") + .HasColumnType("real"); + + b3.HasKey("PoseTaskDefinitionMissionDefinitionId", "PoseTaskDefinitionIndex"); + + b3.ToTable("TaskDefinition"); + + b3.WithOwner() + .HasForeignKey("PoseTaskDefinitionMissionDefinitionId", "PoseTaskDefinitionIndex"); + }); + + b2.OwnsOne("Api.Database.Models.Position", "Position", b3 => + { + b3.Property("PoseTaskDefinitionMissionDefinitionId") + .HasColumnType("text"); + + b3.Property("PoseTaskDefinitionIndex") + .HasColumnType("integer"); + + b3.Property("X") + .HasColumnType("real"); + + b3.Property("Y") + .HasColumnType("real"); + + b3.Property("Z") + .HasColumnType("real"); + + b3.HasKey("PoseTaskDefinitionMissionDefinitionId", "PoseTaskDefinitionIndex"); + + b3.ToTable("TaskDefinition"); + + b3.WithOwner() + .HasForeignKey("PoseTaskDefinitionMissionDefinitionId", "PoseTaskDefinitionIndex"); + }); + + b2.Navigation("Orientation") + .IsRequired(); + + b2.Navigation("Position") + .IsRequired(); + }); + + b1.OwnsOne("Api.Database.Models.Position", "TargetPosition", b2 => + { + b2.Property("TaskDefinitionMissionDefinitionId") + .HasColumnType("text"); + + b2.Property("TaskDefinitionIndex") + .HasColumnType("integer"); + + b2.Property("X") + .HasColumnType("real"); + + b2.Property("Y") + .HasColumnType("real"); + + b2.Property("Z") + .HasColumnType("real"); + + b2.HasKey("TaskDefinitionMissionDefinitionId", "TaskDefinitionIndex"); + + b2.ToTable("TaskDefinition"); + + b2.WithOwner() + .HasForeignKey("TaskDefinitionMissionDefinitionId", "TaskDefinitionIndex"); + }); + + b1.OwnsOne("Api.Services.Models.IsarZoomDescription", "ZoomDescription", b2 => + { + b2.Property("TaskDefinitionMissionDefinitionId") + .HasColumnType("text"); + + b2.Property("TaskDefinitionIndex") + .HasColumnType("integer"); + + b2.Property("ObjectHeight") + .HasColumnType("double precision") + .HasJsonPropertyName("objectHeight"); + + b2.Property("ObjectWidth") + .HasColumnType("double precision") + .HasJsonPropertyName("objectWidth"); + + b2.HasKey("TaskDefinitionMissionDefinitionId", "TaskDefinitionIndex"); + + b2.ToTable("TaskDefinition"); + + b2.WithOwner() + .HasForeignKey("TaskDefinitionMissionDefinitionId", "TaskDefinitionIndex"); + }); + + b1.Navigation("RobotPose") + .IsRequired(); + + b1.Navigation("TargetPosition") + .IsRequired(); + + b1.Navigation("ZoomDescription"); + }); + + b.Navigation("AutoScheduleFrequency"); + + b.Navigation("InspectionArea"); + + b.Navigation("LastSuccessfulRun"); + + b.Navigation("Tasks"); + }); + + modelBuilder.Entity("Api.Database.Models.MissionRun", b => + { + b.HasOne("Api.Database.Models.InspectionArea", "InspectionArea") + .WithMany() + .HasForeignKey("InspectionAreaId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Api.Database.Models.Robot", "Robot") + .WithMany() + .HasForeignKey("RobotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("InspectionArea"); + + b.Navigation("Robot"); + }); + + modelBuilder.Entity("Api.Database.Models.MissionTask", b => + { + b.HasOne("Api.Database.Models.Inspection", "Inspection") + .WithMany() + .HasForeignKey("InspectionId"); + + b.HasOne("Api.Database.Models.MissionRun", null) + .WithMany("Tasks") + .HasForeignKey("MissionRunId"); + + b.OwnsOne("Api.Services.Models.IsarZoomDescription", "IsarZoomDescription", b1 => + { + b1.Property("MissionTaskId") + .HasColumnType("text"); + + b1.Property("ObjectHeight") + .HasColumnType("double precision") + .HasJsonPropertyName("objectHeight"); + + b1.Property("ObjectWidth") + .HasColumnType("double precision") + .HasJsonPropertyName("objectWidth"); + + b1.HasKey("MissionTaskId"); + + b1.ToTable("MissionTasks"); + + b1.WithOwner() + .HasForeignKey("MissionTaskId"); + }); + + b.OwnsOne("Api.Database.Models.Pose", "RobotPose", b1 => + { + b1.Property("MissionTaskId") + .HasColumnType("text"); + + b1.HasKey("MissionTaskId"); + + b1.ToTable("MissionTasks"); + + b1.WithOwner() + .HasForeignKey("MissionTaskId"); + + b1.OwnsOne("Api.Database.Models.Orientation", "Orientation", b2 => + { + b2.Property("PoseMissionTaskId") + .HasColumnType("text"); + + b2.Property("W") + .HasColumnType("real"); + + b2.Property("X") + .HasColumnType("real"); + + b2.Property("Y") + .HasColumnType("real"); + + b2.Property("Z") + .HasColumnType("real"); + + b2.HasKey("PoseMissionTaskId"); + + b2.ToTable("MissionTasks"); + + b2.WithOwner() + .HasForeignKey("PoseMissionTaskId"); + }); + + b1.OwnsOne("Api.Database.Models.Position", "Position", b2 => + { + b2.Property("PoseMissionTaskId") + .HasColumnType("text"); + + b2.Property("X") + .HasColumnType("real"); + + b2.Property("Y") + .HasColumnType("real"); + + b2.Property("Z") + .HasColumnType("real"); + + b2.HasKey("PoseMissionTaskId"); + + b2.ToTable("MissionTasks"); + + b2.WithOwner() + .HasForeignKey("PoseMissionTaskId"); + }); + + b1.Navigation("Orientation") + .IsRequired(); + + b1.Navigation("Position") + .IsRequired(); + }); + + b.Navigation("Inspection"); + + b.Navigation("IsarZoomDescription"); + + b.Navigation("RobotPose") + .IsRequired(); + }); + + modelBuilder.Entity("Api.Database.Models.Plant", b => + { + b.HasOne("Api.Database.Models.Installation", "Installation") + .WithMany() + .HasForeignKey("InstallationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Installation"); + }); + + modelBuilder.Entity("Api.Database.Models.Robot", b => + { + b.HasOne("Api.Database.Models.Installation", "CurrentInstallation") + .WithMany() + .HasForeignKey("CurrentInstallationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsMany("Api.Database.Models.DocumentInfo", "Documentation", b1 => + { + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b1.Property("RobotId") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Url") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b1.HasKey("Id"); + + b1.HasIndex("RobotId"); + + b1.ToTable("DocumentInfo"); + + b1.WithOwner() + .HasForeignKey("RobotId"); + }); + + b.Navigation("CurrentInstallation"); + + b.Navigation("Documentation"); + }); + + modelBuilder.Entity("Api.Database.Models.TagInspectionMetadata", b => + { + b.OwnsOne("Api.Services.Models.IsarZoomDescription", "ZoomDescription", b1 => + { + b1.Property("TagInspectionMetadataTagId") + .HasColumnType("text"); + + b1.Property("ObjectHeight") + .HasColumnType("double precision") + .HasJsonPropertyName("objectHeight"); + + b1.Property("ObjectWidth") + .HasColumnType("double precision") + .HasJsonPropertyName("objectWidth"); + + b1.HasKey("TagInspectionMetadataTagId"); + + b1.ToTable("TagInspectionMetadata"); + + b1.WithOwner() + .HasForeignKey("TagInspectionMetadataTagId"); + }); + + b.Navigation("ZoomDescription"); + }); + + modelBuilder.Entity("Api.Database.Models.Inspection", b => + { + b.Navigation("AnalysisResult") + .IsRequired(); + }); + + modelBuilder.Entity("Api.Database.Models.MissionRun", b => + { + b.Navigation("Tasks"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/api/Migrations/20260513110434_AddTaskDefinitionRefactor.cs b/backend/api/Migrations/20260513110434_AddTaskDefinitionRefactor.cs new file mode 100644 index 000000000..0fceb57a3 --- /dev/null +++ b/backend/api/Migrations/20260513110434_AddTaskDefinitionRefactor.cs @@ -0,0 +1,157 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Api.Migrations +{ + /// + public partial class AddTaskDefinitionRefactor : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_MissionDefinitions_Sources_SourceId", + table: "MissionDefinitions"); + + migrationBuilder.DropTable( + name: "Sources"); + + migrationBuilder.DropIndex( + name: "IX_MissionDefinitions_SourceId", + table: "MissionDefinitions"); + + migrationBuilder.DropColumn( + name: "PoseId", + table: "MissionTasks"); + + migrationBuilder.DropColumn( + name: "TagLink", + table: "MissionTasks"); + + migrationBuilder.DropColumn( + name: "SourceId", + table: "MissionDefinitions"); + + migrationBuilder.RenameColumn( + name: "InspectionTargetName", + table: "Inspections", + newName: "TaskDescription"); + + migrationBuilder.AddColumn( + name: "AnalysisTypes", + table: "MissionTasks", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "AnalysisTypes", + table: "Inspections", + type: "text", + nullable: true); + + migrationBuilder.CreateTable( + name: "TaskDefinition", + columns: table => new + { + Index = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + MissionDefinitionId = table.Column(type: "text", nullable: false), + TagId = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), + Description = table.Column(type: "character varying(500)", maxLength: 500, nullable: true), + RobotPose_Position_X = table.Column(type: "real", nullable: false), + RobotPose_Position_Y = table.Column(type: "real", nullable: false), + RobotPose_Position_Z = table.Column(type: "real", nullable: false), + RobotPose_Orientation_X = table.Column(type: "real", nullable: false), + RobotPose_Orientation_Y = table.Column(type: "real", nullable: false), + RobotPose_Orientation_Z = table.Column(type: "real", nullable: false), + RobotPose_Orientation_W = table.Column(type: "real", nullable: false), + TargetPosition_X = table.Column(type: "real", nullable: false), + TargetPosition_Y = table.Column(type: "real", nullable: false), + TargetPosition_Z = table.Column(type: "real", nullable: false), + ZoomDescription_ObjectWidth = table.Column(type: "double precision", nullable: true), + ZoomDescription_ObjectHeight = table.Column(type: "double precision", nullable: true), + AnalysisTypes = table.Column(type: "text", nullable: false), + SensorType = table.Column(type: "text", nullable: false), + VideoDuration = table.Column(type: "real", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_TaskDefinition", x => new { x.MissionDefinitionId, x.Index }); + table.ForeignKey( + name: "FK_TaskDefinition_MissionDefinitions_MissionDefinitionId", + column: x => x.MissionDefinitionId, + principalTable: "MissionDefinitions", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "TaskDefinition"); + + migrationBuilder.DropColumn( + name: "AnalysisTypes", + table: "MissionTasks"); + + migrationBuilder.DropColumn( + name: "AnalysisTypes", + table: "Inspections"); + + migrationBuilder.RenameColumn( + name: "TaskDescription", + table: "Inspections", + newName: "InspectionTargetName"); + + migrationBuilder.AddColumn( + name: "PoseId", + table: "MissionTasks", + type: "integer", + nullable: true); + + migrationBuilder.AddColumn( + name: "TagLink", + table: "MissionTasks", + type: "character varying(200)", + maxLength: 200, + nullable: true); + + migrationBuilder.AddColumn( + name: "SourceId", + table: "MissionDefinitions", + type: "text", + nullable: false, + defaultValue: ""); + + migrationBuilder.CreateTable( + name: "Sources", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + CustomMissionTasks = table.Column(type: "text", nullable: true), + SourceId = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Sources", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_MissionDefinitions_SourceId", + table: "MissionDefinitions", + column: "SourceId"); + + migrationBuilder.AddForeignKey( + name: "FK_MissionDefinitions_Sources_SourceId", + table: "MissionDefinitions", + column: "SourceId", + principalTable: "Sources", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + } +} diff --git a/backend/api/Migrations/FlotillaDbContextModelSnapshot.cs b/backend/api/Migrations/FlotillaDbContextModelSnapshot.cs index ee7cbf69c..ab92296a9 100644 --- a/backend/api/Migrations/FlotillaDbContextModelSnapshot.cs +++ b/backend/api/Migrations/FlotillaDbContextModelSnapshot.cs @@ -17,7 +17,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "10.0.2") + .HasAnnotation("ProductVersion", "10.0.7") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); @@ -128,7 +128,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("text"); - b.Property("InspectionTargetName") + b.Property("AnalysisTypes") .HasColumnType("text"); b.Property("InspectionType") @@ -144,6 +144,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasMaxLength(200) .HasColumnType("character varying(200)"); + b.Property("TaskDescription") + .HasColumnType("text"); + b.Property("VideoDuration") .HasColumnType("real"); @@ -242,10 +245,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasMaxLength(200) .HasColumnType("character varying(200)"); - b.Property("SourceId") - .IsRequired() - .HasColumnType("text"); - b.HasKey("Id"); b.HasIndex("AutoScheduleFrequencyId"); @@ -254,8 +253,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("LastSuccessfulRunId"); - b.HasIndex("SourceId"); - b.ToTable("MissionDefinitions"); }); @@ -333,6 +330,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("text"); + b.Property("AnalysisTypes") + .HasColumnType("text"); + b.Property("Description") .HasMaxLength(500) .HasColumnType("character varying(500)"); @@ -349,9 +349,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("MissionRunId") .HasColumnType("text"); - b.Property("PoseId") - .HasColumnType("integer"); - b.Property("StartTime") .HasColumnType("timestamp with time zone"); @@ -363,10 +360,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasMaxLength(200) .HasColumnType("character varying(200)"); - b.Property("TagLink") - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - b.Property("TaskOrder") .HasColumnType("integer"); @@ -472,22 +465,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Robots"); }); - modelBuilder.Entity("Api.Database.Models.Source", b => + modelBuilder.Entity("Api.Database.Models.TagInspectionMetadata", b => { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("text"); - - b.Property("CustomMissionTasks") - .HasColumnType("text"); - - b.Property("SourceId") - .IsRequired() + b.Property("TagId") .HasColumnType("text"); - b.HasKey("Id"); + b.HasKey("TagId"); - b.ToTable("Sources"); + b.ToTable("TagInspectionMetadata"); }); modelBuilder.Entity("Api.Database.Models.UserInfo", b => @@ -505,16 +490,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("UserInfos"); }); - modelBuilder.Entity("Api.Services.MissionLoaders.TagInspectionMetadata", b => - { - b.Property("TagId") - .HasColumnType("text"); - - b.HasKey("TagId"); - - b.ToTable("TagInspectionMetadata"); - }); - modelBuilder.Entity("Api.Database.Models.AccessRole", b => { b.HasOne("Api.Database.Models.Installation", "Installation") @@ -704,11 +679,175 @@ protected override void BuildModel(ModelBuilder modelBuilder) .WithMany() .HasForeignKey("LastSuccessfulRunId"); - b.HasOne("Api.Database.Models.Source", "Source") - .WithMany() - .HasForeignKey("SourceId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); + b.OwnsMany("Api.Database.Models.TaskDefinition", "Tasks", b1 => + { + b1.Property("MissionDefinitionId") + .HasColumnType("text"); + + b1.Property("Index") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Index")); + + b1.Property("AnalysisTypes") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b1.Property("SensorType") + .IsRequired() + .HasColumnType("text"); + + b1.Property("TagId") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b1.Property("VideoDuration") + .HasColumnType("real"); + + b1.HasKey("MissionDefinitionId", "Index"); + + b1.ToTable("TaskDefinition"); + + b1.WithOwner() + .HasForeignKey("MissionDefinitionId"); + + b1.OwnsOne("Api.Database.Models.Pose", "RobotPose", b2 => + { + b2.Property("TaskDefinitionMissionDefinitionId") + .HasColumnType("text"); + + b2.Property("TaskDefinitionIndex") + .HasColumnType("integer"); + + b2.HasKey("TaskDefinitionMissionDefinitionId", "TaskDefinitionIndex"); + + b2.ToTable("TaskDefinition"); + + b2.WithOwner() + .HasForeignKey("TaskDefinitionMissionDefinitionId", "TaskDefinitionIndex"); + + b2.OwnsOne("Api.Database.Models.Orientation", "Orientation", b3 => + { + b3.Property("PoseTaskDefinitionMissionDefinitionId") + .HasColumnType("text"); + + b3.Property("PoseTaskDefinitionIndex") + .HasColumnType("integer"); + + b3.Property("W") + .HasColumnType("real"); + + b3.Property("X") + .HasColumnType("real"); + + b3.Property("Y") + .HasColumnType("real"); + + b3.Property("Z") + .HasColumnType("real"); + + b3.HasKey("PoseTaskDefinitionMissionDefinitionId", "PoseTaskDefinitionIndex"); + + b3.ToTable("TaskDefinition"); + + b3.WithOwner() + .HasForeignKey("PoseTaskDefinitionMissionDefinitionId", "PoseTaskDefinitionIndex"); + }); + + b2.OwnsOne("Api.Database.Models.Position", "Position", b3 => + { + b3.Property("PoseTaskDefinitionMissionDefinitionId") + .HasColumnType("text"); + + b3.Property("PoseTaskDefinitionIndex") + .HasColumnType("integer"); + + b3.Property("X") + .HasColumnType("real"); + + b3.Property("Y") + .HasColumnType("real"); + + b3.Property("Z") + .HasColumnType("real"); + + b3.HasKey("PoseTaskDefinitionMissionDefinitionId", "PoseTaskDefinitionIndex"); + + b3.ToTable("TaskDefinition"); + + b3.WithOwner() + .HasForeignKey("PoseTaskDefinitionMissionDefinitionId", "PoseTaskDefinitionIndex"); + }); + + b2.Navigation("Orientation") + .IsRequired(); + + b2.Navigation("Position") + .IsRequired(); + }); + + b1.OwnsOne("Api.Database.Models.Position", "TargetPosition", b2 => + { + b2.Property("TaskDefinitionMissionDefinitionId") + .HasColumnType("text"); + + b2.Property("TaskDefinitionIndex") + .HasColumnType("integer"); + + b2.Property("X") + .HasColumnType("real"); + + b2.Property("Y") + .HasColumnType("real"); + + b2.Property("Z") + .HasColumnType("real"); + + b2.HasKey("TaskDefinitionMissionDefinitionId", "TaskDefinitionIndex"); + + b2.ToTable("TaskDefinition"); + + b2.WithOwner() + .HasForeignKey("TaskDefinitionMissionDefinitionId", "TaskDefinitionIndex"); + }); + + b1.OwnsOne("Api.Services.Models.IsarZoomDescription", "ZoomDescription", b2 => + { + b2.Property("TaskDefinitionMissionDefinitionId") + .HasColumnType("text"); + + b2.Property("TaskDefinitionIndex") + .HasColumnType("integer"); + + b2.Property("ObjectHeight") + .HasColumnType("double precision") + .HasJsonPropertyName("objectHeight"); + + b2.Property("ObjectWidth") + .HasColumnType("double precision") + .HasJsonPropertyName("objectWidth"); + + b2.HasKey("TaskDefinitionMissionDefinitionId", "TaskDefinitionIndex"); + + b2.ToTable("TaskDefinition"); + + b2.WithOwner() + .HasForeignKey("TaskDefinitionMissionDefinitionId", "TaskDefinitionIndex"); + }); + + b1.Navigation("RobotPose") + .IsRequired(); + + b1.Navigation("TargetPosition") + .IsRequired(); + + b1.Navigation("ZoomDescription"); + }); b.Navigation("AutoScheduleFrequency"); @@ -716,7 +855,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("LastSuccessfulRun"); - b.Navigation("Source"); + b.Navigation("Tasks"); }); modelBuilder.Entity("Api.Database.Models.MissionRun", b => @@ -897,7 +1036,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Documentation"); }); - modelBuilder.Entity("Api.Services.MissionLoaders.TagInspectionMetadata", b => + modelBuilder.Entity("Api.Database.Models.TagInspectionMetadata", b => { b.OwnsOne("Api.Services.Models.IsarZoomDescription", "ZoomDescription", b1 => { diff --git a/backend/api/Program.cs b/backend/api/Program.cs index a115dab53..89d7f784d 100644 --- a/backend/api/Program.cs +++ b/backend/api/Program.cs @@ -52,8 +52,6 @@ builder.Services.AddMemoryCache(); builder.Services.ConfigureDatabase(builder.Configuration, builder.Environment.EnvironmentName); -builder.Services.ConfigureMissionLoader(builder.Configuration); - var otelMeter = new Meter($"{applicationName}.Metrics", "0.0.1"); builder.Services.AddSingleton(otelMeter); @@ -74,16 +72,13 @@ 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(); @@ -142,7 +137,6 @@ .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd")) .EnableTokenAcquisitionToCallDownstreamApi() .AddDistributedTokenCaches() - .AddDownstreamApi(EchoService.ServiceName, builder.Configuration.GetSection("Echo")) .AddDownstreamApi(InspectionService.ServiceName, builder.Configuration.GetSection("SARA")) .AddDownstreamApi(IsarService.ServiceName, builder.Configuration.GetSection("Isar")) .AddDownstreamApi( @@ -157,7 +151,6 @@ .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd")) .EnableTokenAcquisitionToCallDownstreamApi() .AddInMemoryTokenCaches() - .AddDownstreamApi(EchoService.ServiceName, builder.Configuration.GetSection("Echo")) .AddDownstreamApi(InspectionService.ServiceName, builder.Configuration.GetSection("SARA")) .AddDownstreamApi(IsarService.ServiceName, builder.Configuration.GetSection("Isar")) .AddDownstreamApi( diff --git a/backend/api/Services/AreaPolygonService.cs b/backend/api/Services/AreaPolygonService.cs index 111e8d8b4..f790b778a 100644 --- a/backend/api/Services/AreaPolygonService.cs +++ b/backend/api/Services/AreaPolygonService.cs @@ -6,7 +6,7 @@ namespace Api.Services public interface IAreaPolygonService { public bool MissionTasksAreInsideAreaPolygon( - List missionTasks, + List missionTasks, AreaPolygon? areaPolygon ); @@ -21,7 +21,7 @@ double zMax public class AreaPolygonService(ILogger logger) : IAreaPolygonService { public bool MissionTasksAreInsideAreaPolygon( - List missionTasks, + List missionTasks, AreaPolygon? areaPolygon ) { @@ -41,11 +41,11 @@ public bool MissionTasksAreInsideAreaPolygon( ) { logger.LogWarning( - "Robot position (X={X}, Y={Y}, Z={Z}) is outside the inspection area polygon for task {taskId}", + "Robot position (X={X}, Y={Y}, Z={Z}) is outside the inspection area polygon for task with description {descr}", robotPosition.X, robotPosition.Y, robotPosition.Z, - Sanitize.SanitizeUserInput(missionTask.Id) + Sanitize.SanitizeUserInput(missionTask.Description) ); return false; } diff --git a/backend/api/Services/AutoScheduleService.cs b/backend/api/Services/AutoScheduleService.cs index 6b545875c..9be31f58f 100644 --- a/backend/api/Services/AutoScheduleService.cs +++ b/backend/api/Services/AutoScheduleService.cs @@ -1,6 +1,5 @@ using System.Text.Json; using Api.Database.Models; -using Api.Services.MissionLoaders; using Api.Utilities; using Hangfire; @@ -46,7 +45,6 @@ public class AutoScheduleService( ILogger logger, IMissionDefinitionService missionDefinitionService, IRobotService robotService, - IMissionLoader missionLoader, IMissionRunService missionRunService, IMissionSchedulingService missionSchedulingService, ISignalRService signalRService @@ -246,17 +244,7 @@ public async Task AutoScheduleMissionRun(string missionDefinitionId, TimeOnly ti try { - var missionTasks = await missionLoader.GetTasksForMission( - missionDefinition.Source.SourceId - ); - if (missionTasks == null) - { - logger.LogError( - "No mission tasks were found for mission definition {MissionDefinitionId}.", - missionDefinition.Id - ); - return; - } + var missionTasks = missionDefinition.Tasks; var missionRun = new MissionRun { @@ -265,7 +253,7 @@ public async Task AutoScheduleMissionRun(string missionDefinitionId, TimeOnly ti MissionId = missionDefinition.Id, Status = MissionStatus.Queued, CreationTime = DateTime.UtcNow, - Tasks = missionTasks, + Tasks = [.. missionDefinition.Tasks.Select((t) => t.ToMissionRunTask())], InstallationCode = missionDefinition.InstallationCode, InspectionArea = missionDefinition.InspectionArea, }; diff --git a/backend/api/Services/EchoService.cs b/backend/api/Services/EchoService.cs deleted file mode 100644 index 44f5d768e..000000000 --- a/backend/api/Services/EchoService.cs +++ /dev/null @@ -1,278 +0,0 @@ -using System.Globalization; -using System.Text.Json; -using Api.Controllers.Models; -using Api.Database.Context; -using Api.Database.Models; -using Api.Services.MissionLoaders; -using Api.Services.Models; -using Api.Utilities; -using Microsoft.EntityFrameworkCore; -using Microsoft.Identity.Abstractions; - -namespace Api.Services -{ - public interface IEchoService - { - public Task> GetAvailableMissions( - string? installationCode - ); - public Task GetMissionById(string sourceMissionId); - public Task?> GetTasksForMission(string missionSourceId); - } - - public class EchoService( - ILogger logger, - IDownstreamApi echoApi, - ISourceService sourceService, - IInspectionService inspectionService - ) : IEchoService - { - public const string ServiceName = "EchoApi"; - - public async Task> GetAvailableMissions( - string? installationCode - ) - { - string relativePath = string.IsNullOrEmpty(installationCode) - ? "robots/robot-plan?Status=Ready" - : $"robots/robot-plan?InstallationCode={installationCode}&&Status=Ready"; - - var response = await echoApi.CallApiForAppAsync( - ServiceName, - options => - { - options.HttpMethod = HttpMethod.Get.Method; - options.RelativePath = relativePath; - } - ); - - if (!response.IsSuccessStatusCode) - { - throw new MissionLoaderUnavailableException( - $"Echo API unavailable. Status code: {response.StatusCode}" - ); - } - - var echoMissions = - await response.Content.ReadFromJsonAsync>() - ?? throw new JsonException("Failed to deserialize missions from Echo"); - - var availableMissions = new List(); - - foreach (var echoMissionResponse in echoMissions) - { - var echoMission = ProcessEchoMission(echoMissionResponse); - if (echoMission == null) - { - continue; - } - var missionDefinitionResponse = await EchoMissionToCondensedMissionDefinition( - echoMission - ); - if (missionDefinitionResponse == null) - { - continue; - } - availableMissions.Add(missionDefinitionResponse); - } - - return availableMissions.AsQueryable(); - } - - public async Task GetMissionById(string sourceMissionId) - { - var echoMission = await GetEchoMission(sourceMissionId); - var mission = await EchoMissionToCondensedMissionDefinition(echoMission); - return mission; - } - - private async Task GetEchoMission(string echoMissionId) - { - string relativePath = $"robots/robot-plan/{echoMissionId}"; - - var response = await echoApi.CallApiForAppAsync( - ServiceName, - options => - { - options.HttpMethod = HttpMethod.Get.Method; - options.RelativePath = relativePath; - } - ); - - if (!response.IsSuccessStatusCode) - { - throw new MissionLoaderUnavailableException( - $"Echo API unavailable. Status code: {response.StatusCode}" - ); - } - - var echoMission = - await response.Content.ReadFromJsonAsync() - ?? throw new JsonException("Failed to deserialize mission from Echo"); - var processedEchoMission = - ProcessEchoMission(echoMission) - ?? throw new InvalidDataException( - $"EchoMission with id: {echoMissionId} is invalid" - ); - return processedEchoMission; - } - - public async Task?> GetTasksForMission(string missionSourceId) - { - var echoMission = await GetEchoMission(missionSourceId); - var missionTasks = echoMission - .Tags.Select(t => MissionTasksFromEchoTag(t)) - .SelectMany(task => task.Result) - .ToList(); - return missionTasks; - } - - private async Task EchoMissionToCondensedMissionDefinition( - EchoMission echoMission - ) - { - var source = - await sourceService.CheckForExistingSource(echoMission.Id) - ?? await sourceService.Create(new Source { SourceId = $"{echoMission.Id}" }); - - var missionDefinition = new CondensedMissionDefinition - { - Id = Guid.NewGuid().ToString(), - SourceId = source.SourceId, - Name = echoMission.Name, - InstallationCode = echoMission.InstallationCode, - }; - return missionDefinition; - } - - private static List ProcessPlanItems( - List planItems, - string installationCode - ) - { - var tags = new List(); - - var indices = new HashSet(); - bool inconsistentIndices = false; - - for (int i = 0; i < planItems.Count; i++) - { - var planItem = planItems[i]; - if ( - planItem.SortingOrder < 0 - || planItem.SortingOrder >= planItems.Count - || indices.Contains(planItem.SortingOrder) - ) - inconsistentIndices = true; - indices.Add(planItem.SortingOrder); - - if (planItem.PoseId is null) - { - string message = - $"Invalid EchoMission {planItem.Tag} has no associated pose id"; - throw new InvalidDataException(message); - } - - var tag = new EchoTag - { - Id = planItem.Id, - TagId = planItem.Tag, - PoseId = planItem.PoseId.Value, - PlanOrder = planItem.SortingOrder, - Pose = new Pose( - planItem.EchoPose.Position, - planItem.EchoPose.RobotBodyDirectionDegrees * MathF.PI / 180 - ), - URL = new Uri( - $"https://stid.equinor.com/{installationCode}/tag?tagNo={planItem.Tag}" - ), - Inspections = - [ - .. planItem - .SensorTypes.Select(sensor => new EchoInspection( - sensor, - planItem.InspectionPoint.EnuPosition.ToPosition(), - planItem.InspectionPoint.Name - )) - .Distinct(new EchoInspectionComparer()), - ], - }; - - if (tag.Inspections.Count < 1) - { - tag.Inspections.Add(new EchoInspection()); - } - - tags.Add(tag); - } - - if (inconsistentIndices) - for (int i = 0; i < tags.Count; i++) - tags[i].PlanOrder = i; - - return tags; - } - - private EchoMission? ProcessEchoMission(EchoMissionResponse echoMission) - { - if (echoMission.PlanItems is null) - { - throw new MissionNotFoundException("Mission has no tags"); - } - try - { - var mission = new EchoMission - { - Id = echoMission.Id.ToString(CultureInfo.CurrentCulture), - Name = echoMission.Name, - InstallationCode = echoMission.InstallationCode, - URL = new Uri($"https://echo.equinor.com/mp?editId={echoMission.Id}"), - Tags = ProcessPlanItems(echoMission.PlanItems, echoMission.InstallationCode), - }; - return mission; - } - catch (InvalidDataException e) - { - logger.LogWarning( - "Echo mission with ID '{Id}' is invalid: '{Message}'", - echoMission.Id, - e.Message - ); - return null; - } - } - - private async Task> MissionTasksFromEchoTag(EchoTag echoTag) - { - var inspections = echoTag - .Inspections.Select(inspection => new Inspection( - inspectionType: inspection.InspectionType, - videoDuration: inspection.TimeInSeconds, - inspectionTarget: inspection.InspectionPoint, - inspectionTargetName: inspection.InspectionPointName - )) - .ToList(); - - var missionTasks = new List(); - - foreach (var inspection in inspections) - { - missionTasks.Add( - new MissionTask( - inspection: inspection, - tagLink: echoTag.URL, - tagId: echoTag.TagId, - robotPose: new Pose(echoTag.Pose), - poseId: echoTag.PoseId, - taskOrder: echoTag.PlanOrder, - taskDescription: inspection.InspectionTargetName, - zoomDescription: await inspectionService.FindInspectionZoom(echoTag), - status: Database.Models.TaskStatus.NotStarted - ) - ); - } - - return missionTasks; - } - } -} diff --git a/backend/api/Services/InspectionAreaService.cs b/backend/api/Services/InspectionAreaService.cs index 15ead51e0..458e29d13 100644 --- a/backend/api/Services/InspectionAreaService.cs +++ b/backend/api/Services/InspectionAreaService.cs @@ -40,7 +40,7 @@ public Task> ReadInspectionAreasByInstallation( ); public InspectionArea? TryFindInspectionAreaForMissionTasks( - List missionTasks, + List missionTasks, string installationCode ); @@ -142,7 +142,7 @@ public async Task> ReadByInstallation( } public InspectionArea? TryFindInspectionAreaForMissionTasks( - List missionTasks, + List missionTasks, string installationCode ) { diff --git a/backend/api/Services/InspectionService.cs b/backend/api/Services/InspectionService.cs index 9b23827df..590b81a1a 100644 --- a/backend/api/Services/InspectionService.cs +++ b/backend/api/Services/InspectionService.cs @@ -1,13 +1,11 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Net; -using System.Net.Http.Json; using System.Text; using System.Text.Json; using Api.Controllers.Models; using Api.Database.Context; using Api.Database.Models; -using Api.Services.MissionLoaders; using Api.Services.Models; using Api.Utilities; using Microsoft.EntityFrameworkCore; @@ -28,7 +26,7 @@ AnalysisResult analysisResult public Task CreateOrUpdateTagInspectionMetadata( TagInspectionMetadata metadata ); - public Task FindInspectionZoom(EchoTag echoTag); + public Task FindInspectionZoom(string tagId); public string GetInspectionName( string installationCode, Position position, @@ -285,11 +283,11 @@ TagInspectionMetadata metadata return metadata; } - public async Task FindInspectionZoom(EchoTag echoTag) + public async Task FindInspectionZoom(string tagId) { return ( await context - .TagInspectionMetadata.Where(e => e.TagId == echoTag.TagId) + .TagInspectionMetadata.Where(e => e.TagId == tagId) .FirstOrDefaultAsync() )?.ZoomDescription; } diff --git a/backend/api/Services/MissionDefinitionService.cs b/backend/api/Services/MissionDefinitionService.cs index 642116e2d..a8ba4a4a7 100644 --- a/backend/api/Services/MissionDefinitionService.cs +++ b/backend/api/Services/MissionDefinitionService.cs @@ -32,8 +32,6 @@ public Task> ReadByInspectionAreaId( public Task?> ReadByHasAutoScheduleFrequency(bool readOnly = true); - public Task ReadBySourceId(string sourceId, bool readOnly = true); - public Task UpdateLastSuccessfulMissionRun( string missionRunId, string missionDefinitionId @@ -61,8 +59,7 @@ public class MissionDefinitionService( ISignalRService signalRService, IAccessRoleService accessRoleService, ILogger logger, - IMissionRunService missionRunService, - ISourceService sourceService + IMissionRunService missionRunService ) : IMissionDefinitionService { public async Task Create(MissionDefinition missionDefinition) @@ -71,10 +68,6 @@ public async Task Create(MissionDefinition missionDefinition) { context.Entry(missionDefinition.LastSuccessfulRun).State = EntityState.Unchanged; } - if (missionDefinition.Source is not null) - { - context.Entry(missionDefinition.Source).State = EntityState.Unchanged; - } context.Entry(missionDefinition.InspectionArea).State = EntityState.Unchanged; await context.MissionDefinitions.AddAsync(missionDefinition); @@ -142,13 +135,6 @@ public async Task> ReadByInspectionAreaId( .ToListAsync(); } - public async Task ReadBySourceId(string sourceId, bool readOnly = true) - { - return await GetMissionDefinitionsWithSubModels(readOnly: readOnly) - .Where(m => m.IsDeprecated == false) - .FirstOrDefaultAsync(m => m.Source.SourceId.Equals(sourceId)); - } - public async Task?> ReadByHasAutoScheduleFrequency( bool readOnly = true ) @@ -265,8 +251,8 @@ private IQueryable GetMissionDefinitionsWithSubModels( .ThenInclude(plant => plant.Installation) .Include(missionDefinition => missionDefinition.InspectionArea) .ThenInclude(area => area!.Installation) - .Include(missionDefinition => missionDefinition.Source) .Include(missionDefinition => missionDefinition.LastSuccessfulRun) + .Include(missionDefinition => missionDefinition.Tasks) .Include(missionDefinition => missionDefinition.InspectionArea) .Where(m => accessibleInstallationCodes.Result.Contains( @@ -342,8 +328,6 @@ public void DetachTracking(FlotillaDbContext context, MissionDefinition missionD { if (missionDefinition.LastSuccessfulRun != null) missionRunService.DetachTracking(context, missionDefinition.LastSuccessfulRun); - if (missionDefinition.Source != null) - sourceService.DetachTracking(context, missionDefinition.Source); context.Entry(missionDefinition).State = EntityState.Detached; } } diff --git a/backend/api/Services/MissionDefinitionTaskService.cs b/backend/api/Services/MissionDefinitionTaskService.cs deleted file mode 100644 index 313a698e7..000000000 --- a/backend/api/Services/MissionDefinitionTaskService.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Api.Database.Models; -using Api.Services.MissionLoaders; - -namespace Api.Services -{ - public interface IMissionDefinitionTaskService - { - public Task?> GetTasksFromSource(Source source); - } - - public class MissionDefinitionTaskService(IMissionLoader missionLoader) - : IMissionDefinitionTaskService - { - public async Task?> GetTasksFromSource(Source source) - { - return await missionLoader.GetTasksForMission(source.SourceId); - } - } -} diff --git a/backend/api/Services/MissionLoaders/CondensedMissionDefinition.cs b/backend/api/Services/MissionLoaders/CondensedMissionDefinition.cs deleted file mode 100644 index 225365136..000000000 --- a/backend/api/Services/MissionLoaders/CondensedMissionDefinition.cs +++ /dev/null @@ -1,28 +0,0 @@ -#nullable disable -using System.Text.Json.Serialization; -using Api.Database.Models; - -namespace Api.Services.MissionLoaders -{ - public class CondensedMissionDefinition - { - public string Id { get; set; } - - public string Name { get; set; } - - public string InstallationCode { get; set; } - - public string SourceId { get; set; } - - [JsonConstructor] - public CondensedMissionDefinition() { } - - public CondensedMissionDefinition(MissionDefinition missionDefinition) - { - Id = missionDefinition.Id; - Name = missionDefinition.Name; - InstallationCode = missionDefinition.InstallationCode; - SourceId = missionDefinition.Source.SourceId; - } - } -} diff --git a/backend/api/Services/MissionLoaders/CustomMissionLoader.cs b/backend/api/Services/MissionLoaders/CustomMissionLoader.cs deleted file mode 100644 index 7b6f5cadf..000000000 --- a/backend/api/Services/MissionLoaders/CustomMissionLoader.cs +++ /dev/null @@ -1,41 +0,0 @@ -using Api.Controllers.Models; -using Api.Database.Models; - -namespace Api.Services.MissionLoaders -{ - public class CustomMissionLoader( - IInstallationService installationService, - IMissionDefinitionService missionDefinitionService, - ISourceService sourceService - ) : IMissionLoader - { - public async Task> GetAvailableMissions( - string? installationCode - ) - { - var missionDefinitions = await missionDefinitionService.ReadByInstallationCode( - installationCode ?? "" - ); - return missionDefinitions.Select(m => new CondensedMissionDefinition(m)); - } - - public async Task GetMissionById(string sourceMissionId) - { - var missionDefinition = await missionDefinitionService.ReadBySourceId(sourceMissionId); - return missionDefinition != null - ? new CondensedMissionDefinition(missionDefinition) - : null; - } - - public async Task?> GetTasksForMission(string missionSourceId) - { - return await sourceService.GetMissionTasksFromSourceId(missionSourceId); - } - - public async Task> GetPlantInfos() - { - var installations = await installationService.ReadAll(); - return installations.Select(i => new PlantInfo(i)).ToList(); - } - } -} diff --git a/backend/api/Services/MissionLoaders/EchoAndCustomMissionLoader.cs b/backend/api/Services/MissionLoaders/EchoAndCustomMissionLoader.cs deleted file mode 100644 index b21787540..000000000 --- a/backend/api/Services/MissionLoaders/EchoAndCustomMissionLoader.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Api.Database.Models; - -namespace Api.Services.MissionLoaders -{ - public class EchoAndCustomMissionLoader(IEchoService echoService, ISourceService sourceService) - : IMissionLoader - { - public async Task> GetAvailableMissions( - string? installationCode - ) - { - return await echoService.GetAvailableMissions(installationCode); - } - - public async Task GetMissionById(string sourceMissionId) - { - return await echoService.GetMissionById(sourceMissionId); - } - - public async Task?> GetTasksForMission(string missionSourceId) - { - var customMissionTasks = await sourceService.GetMissionTasksFromSourceId( - missionSourceId - ); - if (customMissionTasks != null) - { - return customMissionTasks; - } - - return await echoService.GetTasksForMission(missionSourceId); - } - } -} diff --git a/backend/api/Services/MissionLoaders/EchoInspection.cs b/backend/api/Services/MissionLoaders/EchoInspection.cs deleted file mode 100644 index cca9c9830..000000000 --- a/backend/api/Services/MissionLoaders/EchoInspection.cs +++ /dev/null @@ -1,80 +0,0 @@ -using Api.Controllers.Models; -using Api.Database.Models; - -namespace Api.Services.MissionLoaders -{ - public class EchoInspection - { - public EchoInspection() - { - InspectionType = InspectionType.Image; - InspectionPoint = new Position(); - } - - public EchoInspection( - SensorType echoSensorType, - Position inspectionPoint, - string? inspectionPointName - ) - { - InspectionType = InspectionTypeFromEchoSensorType(echoSensorType.Key); - TimeInSeconds = (float?)echoSensorType.TimeInSeconds; - InspectionPoint = inspectionPoint; - InspectionPointName = - inspectionPointName != "Stid Coordinate" ? inspectionPointName : null; - } - - public InspectionType InspectionType { get; set; } - - public Position InspectionPoint { get; set; } - - public string? InspectionPointName { get; set; } - - public float? TimeInSeconds { get; set; } - - private static InspectionType InspectionTypeFromEchoSensorType(string sensorType) - { - return sensorType switch - { - "Picture" => InspectionType.Image, - "ThermicPicture" => InspectionType.ThermalImage, - "ThermalPicture" => InspectionType.ThermalImage, - "Audio" => InspectionType.Audio, - "Video" => InspectionType.Video, - "ThermicVideo" => InspectionType.ThermalVideo, - "ThermalVideo" => InspectionType.ThermalVideo, - "CO2" => InspectionType.CO2Measurement, - _ => throw new InvalidDataException( - $"Echo sensor type '{sensorType}' not supported" - ), - }; - } - } - - public class EchoInspectionComparer : IEqualityComparer - { - public bool Equals(EchoInspection? e1, EchoInspection? e2) - { - if (ReferenceEquals(e1, e2)) - { - return true; - } - - if (e2 is null || e1 is null) - { - return false; - } - - return e1.InspectionType == e2.InspectionType - && e1.TimeInSeconds == e2.TimeInSeconds - && e1.InspectionPoint.Equals(e2.InspectionPoint); - } - - public int GetHashCode(EchoInspection e) - { - // We cannot incorporate TimeInSeconds here as SQL queries do not handle - // nullables even with short circuiting logic - return (int)e.InspectionType; - } - } -} diff --git a/backend/api/Services/MissionLoaders/EchoMission.cs b/backend/api/Services/MissionLoaders/EchoMission.cs deleted file mode 100644 index 978b9f330..000000000 --- a/backend/api/Services/MissionLoaders/EchoMission.cs +++ /dev/null @@ -1,16 +0,0 @@ -# nullable disable -namespace Api.Services.MissionLoaders -{ - public class EchoMission - { - public string Id { get; set; } - - public string Name { get; set; } - - public string InstallationCode { get; set; } - - public Uri URL { get; set; } - - public virtual IList Tags { get; set; } - } -} diff --git a/backend/api/Services/MissionLoaders/EchoMissionLoader.cs b/backend/api/Services/MissionLoaders/EchoMissionLoader.cs deleted file mode 100644 index 209e77c8f..000000000 --- a/backend/api/Services/MissionLoaders/EchoMissionLoader.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Api.Database.Models; - -namespace Api.Services.MissionLoaders -{ - public class EchoMissionLoader(IEchoService echoService) : IMissionLoader - { - public async Task> GetAvailableMissions( - string? installationCode - ) - { - return await echoService.GetAvailableMissions(installationCode); - } - - public async Task GetMissionById(string sourceMissionId) - { - return await echoService.GetMissionById(sourceMissionId); - } - - public async Task?> GetTasksForMission(string missionSourceId) - { - return await echoService.GetTasksForMission(missionSourceId); - } - } -} diff --git a/backend/api/Services/MissionLoaders/EchoMissionResponse.cs b/backend/api/Services/MissionLoaders/EchoMissionResponse.cs deleted file mode 100644 index e6e94f142..000000000 --- a/backend/api/Services/MissionLoaders/EchoMissionResponse.cs +++ /dev/null @@ -1,126 +0,0 @@ -# nullable disable -using System.Text.Json.Serialization; -using Api.Services.Models; - -namespace Api.Controllers.Models -{ - public class EchoMissionResponse - { - [JsonPropertyName("robotPlanId")] - public int Id { get; set; } - - [JsonPropertyName("installationCode")] - public string InstallationCode { get; set; } - - [JsonPropertyName("name")] - public string Name { get; set; } - - [JsonPropertyName("createdBy")] - public string CreatedBy { get; set; } - - [JsonPropertyName("status")] - public string Status { get; set; } - - [JsonPropertyName("robotOperator")] - public string RobotOperator { get; set; } - - [JsonPropertyName("createdAt")] - public string CreatedAt { get; set; } - - [JsonPropertyName("lastModifiedAt")] - public string LastModifiedAt { get; set; } - - [JsonPropertyName("inspectionDate")] - public string InspectionDate { get; set; } - - [JsonPropertyName("planItems")] - public List PlanItems { get; set; } - } - - public class PlanItem - { - [JsonPropertyName("planItemId")] - public int Id { get; set; } - - [JsonPropertyName("tag")] - public string Tag { get; set; } - - [JsonPropertyName("sortingOrder")] - public int SortingOrder { get; set; } - - [JsonPropertyName("robotPlanId")] - public int RobotPlanId { get; set; } - - [JsonPropertyName("sensorTypes")] - public List SensorTypes { get; set; } - - [JsonPropertyName("poseId")] - public int? PoseId { get; set; } - - [JsonPropertyName("pose")] - public EchoPose EchoPose { get; set; } - - [JsonPropertyName("inspectionPoint")] - public InspectionPoint InspectionPoint { get; set; } - } - - public class SensorType - { - [JsonPropertyName("planItemSensorTypeId")] - public int Id { get; set; } - - [JsonPropertyName("sensorTypeKey")] - public string Key { get; set; } - - [JsonPropertyName("timeInSeconds")] - public decimal? TimeInSeconds { get; set; } - - [JsonPropertyName("planItemId")] - public int PlanItemId { get; set; } - } - - public class EchoPose - { - [JsonPropertyName("poseId")] - public int? PoseId { get; set; } - - [JsonPropertyName("installationCode")] - public string InstallationCode { get; set; } - - [JsonPropertyName("tag")] - public string Tag { get; set; } - - [JsonPropertyName("name")] - public string Name { get; set; } - - [JsonPropertyName("position")] - public EnuPosition Position { get; set; } - - [JsonPropertyName("robotBodyDirectionDegrees")] - public float RobotBodyDirectionDegrees { get; set; } - - [JsonPropertyName("isDefault")] - public bool IsDefault { get; set; } - } - - public class InspectionPoint - { - [JsonPropertyName("inspectionPointId")] - public int Id { get; set; } - - [JsonPropertyName("installationCode")] - public string InstallationCode { get; set; } - - [JsonPropertyName("tag")] - public string Tag { get; set; } - - [JsonPropertyName("name")] - public string Name { get; set; } - - [JsonPropertyName("position")] - public EnuPosition EnuPosition { get; set; } - - [JsonPropertyName("isDefault")] - public bool IsDefault { get; set; } - } -} diff --git a/backend/api/Services/MissionLoaders/EchoPlantInfoResponse.cs b/backend/api/Services/MissionLoaders/EchoPlantInfoResponse.cs deleted file mode 100644 index 4bbc6adbd..000000000 --- a/backend/api/Services/MissionLoaders/EchoPlantInfoResponse.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Api.Services.MissionLoaders -{ - public class EchoPlantInfoResponse - { - [JsonPropertyName("plantCode")] - public string? PlantCode { get; set; } - - [JsonPropertyName("installationCode")] - public string? InstallationCode { get; set; } - - [JsonPropertyName("projectDescription")] - public string? ProjectDescription { get; set; } - - [JsonPropertyName("plantDirectory")] - public string? PlantDirectory { get; set; } - - [JsonPropertyName("availableInEcho3D")] - public bool AvailableInEcho3D { get; set; } - - [JsonPropertyName("availableInEcho3DWebReveal")] - public bool AvailableInEcho3DWebReveal { get; set; } - - [JsonPropertyName("sapId")] - public int? SapId { get; set; } - - [JsonPropertyName("ayelixSiteId")] - public int? AyelixSiteId { get; set; } - } -} diff --git a/backend/api/Services/MissionLoaders/EchoTag.cs b/backend/api/Services/MissionLoaders/EchoTag.cs deleted file mode 100644 index 18a207270..000000000 --- a/backend/api/Services/MissionLoaders/EchoTag.cs +++ /dev/null @@ -1,22 +0,0 @@ -#nullable disable -using Api.Database.Models; - -namespace Api.Services.MissionLoaders -{ - public class EchoTag - { - public int Id { get; set; } - - public string TagId { get; set; } - - public int PlanOrder { get; set; } - - public int? PoseId { get; set; } - - public Pose Pose { get; set; } - - public Uri URL { get; set; } - - public virtual IList Inspections { get; set; } - } -} diff --git a/backend/api/Services/MissionLoaders/MissionLoaderInterface.cs b/backend/api/Services/MissionLoaders/MissionLoaderInterface.cs deleted file mode 100644 index 54140f2d4..000000000 --- a/backend/api/Services/MissionLoaders/MissionLoaderInterface.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Api.Controllers.Models; -using Api.Database.Models; - -namespace Api.Services.MissionLoaders -{ - public interface IMissionLoader - { - public Task GetMissionById(string sourceMissionId); - - public Task> GetAvailableMissions( - string? installationCode - ); - - public Task?> GetTasksForMission(string sourceMissionId); - } -} diff --git a/backend/api/Services/MissionRunService.cs b/backend/api/Services/MissionRunService.cs index de8f455ef..c20b08b3e 100644 --- a/backend/api/Services/MissionRunService.cs +++ b/backend/api/Services/MissionRunService.cs @@ -272,7 +272,7 @@ public bool IncludesUnsupportedInspectionType(MissionRun missionRun) return missionRun.Tasks.Any(task => task.Inspection != null - && !task.Inspection.IsSupportedInspectionType(missionRun.Robot.RobotCapabilities) + && !task.Inspection.IsSupportedSensorType(missionRun.Robot.RobotCapabilities) ); } diff --git a/backend/api/Services/MissionSchedulingService.cs b/backend/api/Services/MissionSchedulingService.cs index 1005c53a3..26e067444 100644 --- a/backend/api/Services/MissionSchedulingService.cs +++ b/backend/api/Services/MissionSchedulingService.cs @@ -128,7 +128,7 @@ await missionRunService.UpdateMissionRunProperty( if ( !areaPolygonService.MissionTasksAreInsideAreaPolygon( - (List)missionRun.Tasks, + [.. missionRun.Tasks.Select((t) => t.ToMissionTaskDefinition())], currentInspectionArea.AreaPolygon ) ) diff --git a/backend/api/Services/Models/IsarMissionDefinition.cs b/backend/api/Services/Models/IsarMissionDefinition.cs index fe911d8be..c73cf409a 100644 --- a/backend/api/Services/Models/IsarMissionDefinition.cs +++ b/backend/api/Services/Models/IsarMissionDefinition.cs @@ -100,6 +100,9 @@ public struct IsarInspectionDefinition [JsonPropertyName("duration")] public float? Duration { get; set; } + [JsonPropertyName("analysis_types")] + public List? AnalysisTypes { get; set; } + public IsarInspectionDefinition(MissionTask missionTask) { var inspection = missionTask.Inspection!; @@ -115,6 +118,29 @@ public IsarInspectionDefinition(MissionTask missionTask) : null; InspectionDescription = missionTask.Description; Duration = inspection.VideoDuration; + AnalysisTypes = ToSaraAnalysisKeys(inspection.AnalysisTypes); + } + + private static List? ToSaraAnalysisKeys(IList? types) + { + if (types is null || types.Count == 0) + return null; + var mapped = types + .Select(t => + t switch + { + AnalysisType.Fencilla => "fencilla", + AnalysisType.CLOE => "cloe", + AnalysisType.ThermalReading => "thermal-reading", + AnalysisType.CO2 => "co2", + _ => null, + } + ) + .Where(s => s is not null) + .Cast() + .Distinct() + .ToList(); + return mapped.Count == 0 ? null : mapped; } } diff --git a/backend/api/Services/SourceService.cs b/backend/api/Services/SourceService.cs deleted file mode 100644 index 52b7264ca..000000000 --- a/backend/api/Services/SourceService.cs +++ /dev/null @@ -1,153 +0,0 @@ -using System.Text.Json; -using Api.Database.Context; -using Api.Database.Models; -using Api.Utilities; -using Microsoft.EntityFrameworkCore; - -namespace Api.Services -{ - public interface ISourceService - { - public Task Create(Source source); - - public Task> ReadAll(bool readOnly = true); - - public Task ReadById(string id, bool readOnly = true); - - public Task CheckForExistingSource(string sourceId); - - public Task CheckForExistingSourceFromTasks(IList tasks); - - public Task CreateSourceIfDoesNotExist( - List tasks, - bool readOnly = true - ); - - public Task?> GetMissionTasksFromSourceId(string id); - - public Task Delete(string id); - - public void DetachTracking(FlotillaDbContext context, Source source); - } - - [System.Diagnostics.CodeAnalysis.SuppressMessage( - "Globalization", - "CA1309:Use ordinal StringComparison", - Justification = "EF Core refrains from translating string comparison overloads to SQL" - )] - public class SourceService(FlotillaDbContext context, ILogger logger) - : ISourceService - { - public async Task Create(Source source) - { - context.Sources.Add(source); - await context.SaveChangesAsync(); - DetachTracking(context, source); - return source; - } - - public async Task> ReadAll(bool readOnly = true) - { - var query = GetSources(readOnly: readOnly); - - return await query.ToListAsync(); - } - - private IQueryable GetSources(bool readOnly = true) - { - return readOnly ? context.Sources.AsNoTracking() : context.Sources.AsTracking(); - } - - public async Task ReadById(string id, bool readOnly = true) - { - return await GetSources(readOnly: readOnly).FirstOrDefaultAsync(s => s.Id.Equals(id)); - } - - public async Task ReadBySourceId(string sourceId, bool readOnly = true) - { - return await GetSources(readOnly: readOnly) - .FirstOrDefaultAsync(s => s.SourceId.Equals(sourceId)); - } - - public async Task CheckForExistingSource(string sourceId) - { - return await ReadBySourceId(sourceId, readOnly: true); - } - - public async Task CheckForExistingSourceFromTasks(IList tasks) - { - string hash = MissionTask.CalculateHashFromTasks(tasks); - return await ReadBySourceId(hash, readOnly: true); - } - - public async Task?> GetMissionTasksFromSourceId(string id) - { - var existingSource = await ReadBySourceId(id, readOnly: true); - if (existingSource == null || existingSource.CustomMissionTasks == null) - return null; - - try - { - var content = JsonSerializer.Deserialize>( - existingSource.CustomMissionTasks - ); - - if (content == null) - return null; - - foreach (var task in content) - { - task.Id = Guid.NewGuid().ToString(); // This is needed as tasks are owned by mission runs and to update the tasks for the correct mission run - } - return content; - } - catch (Exception e) - { - logger.LogWarning( - "Unable to deserialize custom mission tasks with ID {Id}. {ErrorMessage}", - id, - e - ); - return null; - } - } - - public async Task CreateSourceIfDoesNotExist( - List tasks, - bool readOnly = true - ) - { - string json = JsonSerializer.Serialize(tasks); - string hash = MissionTask.CalculateHashFromTasks(tasks); - - var existingSource = await ReadById(hash, readOnly: readOnly); - - if (existingSource != null) - return existingSource; - - var newSource = await Create(new Source { SourceId = hash, CustomMissionTasks = json }); - - DetachTracking(context, newSource); - return newSource; - } - - public async Task Delete(string id) - { - var source = await GetSources().FirstOrDefaultAsync(ev => ev.Id.Equals(id)); - if (source is null) - { - return null; - } - - context.Sources.Remove(source); - await context.SaveChangesAsync(); - - return source; - } - - public void DetachTracking(FlotillaDbContext context, Source source) - { - context.Entry(source).State = EntityState.Detached; - } - } -} diff --git a/backend/api/Utilities/Exceptions.cs b/backend/api/Utilities/Exceptions.cs index 9d4d3ace5..241b853fb 100644 --- a/backend/api/Utilities/Exceptions.cs +++ b/backend/api/Utilities/Exceptions.cs @@ -60,10 +60,6 @@ public MissionResumeException(string message, int isarStatusCode) public int IsarStatusCode { get; set; } } - public class MissionLoaderUnavailableException(string message) : Exception(message) { } - - public class SourceException(string message) : Exception(message) { } - public class InstallationNotFoundException(string message) : Exception(message) { } public class PlantNotFoundException(string message) : Exception(message) { } diff --git a/backend/api/Utilities/SanitizeInput.cs b/backend/api/Utilities/SanitizeInput.cs index b371c56a1..4739a5ef9 100644 --- a/backend/api/Utilities/SanitizeInput.cs +++ b/backend/api/Utilities/SanitizeInput.cs @@ -24,9 +24,8 @@ public static ScheduledMissionQuery SanitizeUserInput(ScheduledMissionQuery inpu return inputQuery; } - public static CustomMissionQuery SanitizeUserInput(CustomMissionQuery inputQuery) + public static CreateMissionQuery SanitizeUserInput(CreateMissionQuery inputQuery) { - inputQuery.RobotId = SanitizeUserInput(inputQuery.RobotId); inputQuery.InstallationCode = SanitizeUserInput(inputQuery.InstallationCode); inputQuery.Name = SanitizeUserInput(inputQuery.Name); diff --git a/backend/api/appsettings.json b/backend/api/appsettings.json index 54ca54a7f..916a4db93 100644 --- a/backend/api/appsettings.json +++ b/backend/api/appsettings.json @@ -70,9 +70,6 @@ "InitializeInMemDb": false, "Timeout": 30 }, - "MissionLoader": { - "FileName": "Api.Services.MissionLoaders.EchoAndCustomMissionLoader" - }, "KeyVault": { "UseKeyVault": true }, diff --git a/frontend/src/api/BackendApi.tsx b/frontend/src/api/BackendApi.tsx index 5707fe522..60a2593f5 100644 --- a/frontend/src/api/BackendApi.tsx +++ b/frontend/src/api/BackendApi.tsx @@ -5,7 +5,6 @@ import { MediaStreamConfig } from 'models/VideoStream' import { MissionRunQueryParameters } from 'models/MissionRunQueryParameters' import { PaginatedResponse, PaginationHeader, PaginationHeaderName } from 'models/PaginatedResponse' import { Mission } from 'models/Mission' -import { CondensedMissionDefinition } from 'models/CondensedMissionDefinition' import { MissionDefinition } from 'models/MissionDefinition' import { MissionDefinitionQueryParameters } from 'models/MissionDefinitionQueryParameters' import { InspectionArea } from 'models/InspectionArea' @@ -80,12 +79,6 @@ export class BackendApi { return { pagination: pagination, content: result.content } } - async getAvailableMissions(installationCode: string): Promise { - const path: string = 'mission-loader/available-missions/' + installationCode - const result = await this.api.GET(path).catch(handleError('GET', path)) - return result.content - } - async getMissionDefinitions( parameters: MissionDefinitionQueryParameters ): Promise> { @@ -95,7 +88,6 @@ export class BackendApi { path = path + 'InstallationCode=' + parameters.installationCode + '&' if (parameters.inspectionArea) path = path + 'InspectionArea=' + parameters.inspectionArea + '&' - if (parameters.sourceId) path = path + 'SourceId=' + parameters.sourceId + '&' if (parameters.pageNumber) path = path + 'PageNumber=' + parameters.pageNumber + '&' if (parameters.pageSize) path = path + 'PageSize=' + parameters.pageSize + '&' if (parameters.orderBy) path = path + 'OrderBy=' + parameters.orderBy + '&' @@ -147,19 +139,6 @@ export class BackendApi { return result.content } - async postMission(missionSourceId: string, robotId: string, installationCode: string | null) { - const path: string = 'missions' - const robots: RobotWithoutTelemetry[] = await this.getEnabledRobots() - const desiredRobot = filterRobots(robots, robotId) - const body = { - robotId: desiredRobot[0].id, - missionSourceId: missionSourceId, - installationCode: installationCode, - } - const result = await this.api.POST(path, body).catch(handleError('POST', path)) - return result.content - } - async scheduleMissionDefinition(missionDefinitionId: string, robotId: string): Promise { const path: string = `missions/schedule/${missionDefinitionId}` const robots: RobotWithoutTelemetry[] = await this.getEnabledRobots() diff --git a/frontend/src/components/Contexts/MissionFilterContext.tsx b/frontend/src/components/Contexts/MissionFilterContext.tsx index ec28eb5a0..b7f31b497 100644 --- a/frontend/src/components/Contexts/MissionFilterContext.tsx +++ b/frontend/src/components/Contexts/MissionFilterContext.tsx @@ -1,6 +1,6 @@ import { createContext, FC, useContext, useEffect, useState, useMemo } from 'react' import { MissionStatusFilterOptions, missionStatusFilterOptionsIterable } from 'models/Mission' -import { InspectionType } from 'models/Inspection' +import { SensorType } from 'models/Inspection' import { useLanguageContext } from './LanguageContext' import { MissionRunQueryParameters } from 'models/MissionRunQueryParameters' import { useSearchParams } from 'react-router-dom' @@ -17,7 +17,7 @@ interface IMissionFilterContext { statuses: MissionStatusFilterOptions[] | undefined robotName: string | undefined tagId: string | undefined - inspectionTypes: InspectionType[] | undefined + inspectionTypes: SensorType[] | undefined minStartTime: number | undefined maxStartTime: number | undefined minEndTime: number | undefined @@ -28,7 +28,7 @@ interface IMissionFilterContext { switchStatuses: (newStatuses: MissionStatusFilterOptions[]) => void switchRobotName: (newRobotName: string | undefined) => void switchTagId: (newTagId: string | undefined) => void - switchInspectionTypes: (newInspectionTypes: InspectionType[]) => void + switchInspectionTypes: (newInspectionTypes: SensorType[]) => void switchMinStartTime: (newMinStartTime: number | undefined) => void switchMaxStartTime: (newMaxStartTime: number | undefined) => void switchMinEndTime: (newMinEndTime: number | undefined) => void @@ -174,7 +174,7 @@ export const MissionFilterProvider: FC = ({ children }) => { setFilterIsSet(true) setFilterState({ ...filterState, tagId: newTagId }) }, - switchInspectionTypes: (newInspectionTypes: InspectionType[]) => { + switchInspectionTypes: (newInspectionTypes: SensorType[]) => { setFilterIsSet(true) setFilterState({ ...filterState, inspectionTypes: newInspectionTypes }) }, diff --git a/frontend/src/components/Dialogs/MissionEditDialog.tsx b/frontend/src/components/Dialogs/MissionEditDialog.tsx index 360acafb7..9244ea83a 100644 --- a/frontend/src/components/Dialogs/MissionEditDialog.tsx +++ b/frontend/src/components/Dialogs/MissionEditDialog.tsx @@ -13,11 +13,9 @@ import { StyledDialog } from 'components/Styles/StyledComponents' import { allDays, DaysOfWeek, TimeAndDay } from 'models/AutoScheduleFrequency' import { MissionDefinition } from 'models/MissionDefinition' import { MissionDefinitionUpdateForm } from 'models/MissionDefinitionUpdateForm' -import { ChangeEvent, useContext, useState } from 'react' -import { useNavigate } from 'react-router-dom' +import { ChangeEvent, useState } from 'react' import styled from 'styled-components' import { useBackendApi } from 'api/UseBackendApi' -import { InstallationContext } from 'components/Contexts/InstallationContext' const StyledSummary = styled.div` padding: 16px 8px 0px 8px; @@ -57,8 +55,6 @@ const StyledTimeChips = styled.div` const useMissionUpdater = () => { const { TranslateText } = useLanguageContext() const { setAlert, setListAlert } = useAlertContext() - const { installation } = useContext(InstallationContext) - const navigate = useNavigate() const backendApi = useBackendApi() const updateMission = ( @@ -70,16 +66,12 @@ const useMissionUpdater = () => { comment: mission.comment, schedulingTimesCETperWeek: mission.autoScheduleFrequency?.schedulingTimesCETperWeek, name: mission.name, - isDeprecated: false, } const form: MissionDefinitionUpdateForm = { ...defaultForm, ...partialForm } backendApi .updateMissionDefinition(mission.id, form) - .then((missionDefinition) => { - onSuccess() - if (missionDefinition.isDeprecated) navigate(`/${installation.installationCode}`) - }) + .then(onSuccess) .catch(() => { setAlert( AlertType.RequestFail, diff --git a/frontend/src/components/Displays/TaskDisplay.tsx b/frontend/src/components/Displays/TaskDisplay.tsx index 5048ed624..24f7de703 100644 --- a/frontend/src/components/Displays/TaskDisplay.tsx +++ b/frontend/src/components/Displays/TaskDisplay.tsx @@ -3,13 +3,6 @@ import { Task } from 'models/Task' export const TagIdDisplay = ({ task }: { task: Task }) => { if (!task.tagId) return {'N/A'} - - if (task.tagLink) - return ( - - {task.tagId!} - - ) else return {task.tagId!} } diff --git a/frontend/src/models/CondensedMissionDefinition.ts b/frontend/src/models/CondensedMissionDefinition.ts deleted file mode 100644 index 008b8c981..000000000 --- a/frontend/src/models/CondensedMissionDefinition.ts +++ /dev/null @@ -1,6 +0,0 @@ -export interface CondensedMissionDefinition { - id: string - name: string - installationCode: string - sourceId: string -} diff --git a/frontend/src/models/Inspection.ts b/frontend/src/models/Inspection.ts index a29f726e0..a5c4bd5dd 100644 --- a/frontend/src/models/Inspection.ts +++ b/frontend/src/models/Inspection.ts @@ -1,14 +1,17 @@ +import { AnalysisType } from './MissionDefinition' import { Position } from './Position' export interface Inspection { id: string isarInspectionId: string isCompleted: boolean - inspectionType: InspectionType + inspectionType: SensorType analysisResult?: AnalysisResult inspectionTarget: Position videoDuration?: number inspectionUrl?: string + analysisTypes: AnalysisType[] + taskDescription?: string startTime?: Date endTime?: Date } @@ -25,7 +28,7 @@ interface AnalysisResult { blobName?: string } -export enum InspectionType { +export enum SensorType { Image = 'Image', ThermalImage = 'ThermalImage', Video = 'Video', @@ -40,18 +43,15 @@ export enum DisplayMethod { None = 'None', } -export const ValidInspectionReportInspectionTypes: InspectionType[] = [ - InspectionType.Image, - InspectionType.ThermalImage, -] +export const ValidInspectionReportInspectionTypes: SensorType[] = [SensorType.Image, SensorType.ThermalImage] -export const InspectionTypeToDisplayMethod: { [inspectionType in InspectionType]: DisplayMethod } = { - [InspectionType.Image]: DisplayMethod.Image, - [InspectionType.ThermalImage]: DisplayMethod.Image, - [InspectionType.CO2Measurement]: DisplayMethod.Number, - [InspectionType.Video]: DisplayMethod.None, - [InspectionType.ThermalVideo]: DisplayMethod.None, - [InspectionType.Audio]: DisplayMethod.None, +export const SensorTypeToDisplayMethod: { [sensorType in SensorType]: DisplayMethod } = { + [SensorType.Image]: DisplayMethod.Image, + [SensorType.ThermalImage]: DisplayMethod.Image, + [SensorType.CO2Measurement]: DisplayMethod.Number, + [SensorType.Video]: DisplayMethod.None, + [SensorType.ThermalVideo]: DisplayMethod.None, + [SensorType.Audio]: DisplayMethod.None, } export interface SaraInspectionVisualizationReady { diff --git a/frontend/src/models/MissionDefinition.ts b/frontend/src/models/MissionDefinition.ts index cd74d49d3..3ea5133d5 100644 --- a/frontend/src/models/MissionDefinition.ts +++ b/frontend/src/models/MissionDefinition.ts @@ -1,6 +1,33 @@ import { InspectionArea } from './InspectionArea' import { Mission } from './Mission' import { AutoScheduleFrequency } from './AutoScheduleFrequency' +import { Pose } from './Pose' +import { Position } from './Position' +import { SensorType } from './Inspection' + +export enum AnalysisType { + Fencilla = 'Fencilla', + CLOE = 'CLOE', + ThermalReading = 'ThermalReading', + CO2 = 'CO2', +} + +interface ZoomDescription { + objectWidth: number + objectHeight: number +} + +interface MissionTaskDefinition { + id: string + tagId: string + description?: string + robotPose: Pose + targetPosition: Position + zoomDescription?: ZoomDescription + analysisTypes: AnalysisType[] + sensorType: SensorType + videoDuration?: number +} export interface MissionDefinition { id: string @@ -11,6 +38,5 @@ export interface MissionDefinition { autoScheduleFrequency?: AutoScheduleFrequency lastSuccessfulRun?: Mission inspectionArea: InspectionArea - isDeprecated: boolean - sourceId: string + tasks: MissionTaskDefinition[] } diff --git a/frontend/src/models/MissionDefinitionQueryParameters.ts b/frontend/src/models/MissionDefinitionQueryParameters.ts index 62fd0aaae..3e154b7d9 100644 --- a/frontend/src/models/MissionDefinitionQueryParameters.ts +++ b/frontend/src/models/MissionDefinitionQueryParameters.ts @@ -3,7 +3,6 @@ export interface MissionDefinitionQueryParameters { nameSearch?: string robotNameSearch?: string inspectionArea?: string - sourceId?: string pageNumber?: number pageSize?: number orderBy?: string diff --git a/frontend/src/models/MissionDefinitionUpdateForm.ts b/frontend/src/models/MissionDefinitionUpdateForm.ts index 2c26b209e..241716e7d 100644 --- a/frontend/src/models/MissionDefinitionUpdateForm.ts +++ b/frontend/src/models/MissionDefinitionUpdateForm.ts @@ -4,5 +4,4 @@ export interface MissionDefinitionUpdateForm { comment?: string schedulingTimesCETperWeek?: TimeAndDay[] name?: string - isDeprecated?: boolean } diff --git a/frontend/src/models/MissionRunQueryParameters.ts b/frontend/src/models/MissionRunQueryParameters.ts index a98a2fc0a..ac8af70a6 100644 --- a/frontend/src/models/MissionRunQueryParameters.ts +++ b/frontend/src/models/MissionRunQueryParameters.ts @@ -1,4 +1,4 @@ -import { InspectionType } from './Inspection' +import { SensorType } from './Inspection' import { MissionStatus } from './Mission' export interface MissionRunQueryParameters { @@ -9,7 +9,7 @@ export interface MissionRunQueryParameters { nameSearch?: string robotNameSearch?: string tagSearch?: string - inspectionTypes?: InspectionType[] + inspectionTypes?: SensorType[] inspectionArea?: string minStartTime?: number maxStartTime?: number diff --git a/frontend/src/models/Task.ts b/frontend/src/models/Task.ts index f7057bcb2..a90617918 100644 --- a/frontend/src/models/Task.ts +++ b/frontend/src/models/Task.ts @@ -3,12 +3,9 @@ import { Pose } from './Pose' export interface Task { id: string - taskOrder: number tagId?: string description?: string - tagLink?: string robotPose: Pose - poseId?: number status: TaskStatus isCompleted: boolean startTime?: Date diff --git a/frontend/src/pages/FrontPage/MissionOverview/ScheduleMissionDialog/FetchingMissionsDialog.tsx b/frontend/src/pages/FrontPage/MissionOverview/ScheduleMissionDialog/FetchingMissionsDialog.tsx deleted file mode 100644 index 7e28179cf..000000000 --- a/frontend/src/pages/FrontPage/MissionOverview/ScheduleMissionDialog/FetchingMissionsDialog.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { Button, Card, Dialog, Typography, CircularProgress } from '@equinor/eds-core-react' -import styled from 'styled-components' -import { useLanguageContext } from 'components/Contexts/LanguageContext' - -const StyledMissionDialog = styled.div` - display: flex; - justify-content: space-between; -` -const StyledAutoComplete = styled(Card)` - display: flex; - justify-content: center; - padding: 8px; - gap: 25px; - box-shadow: none; -` -const StyledMissionSection = styled.div` - display: flex; - margin-left: auto; - margin-right: 0; - gap: 10px; -` -const StyledLoading = styled.div` - display: flex; - flex-direction: column; - align-items: center; - padding-top: 3rem; - gap: 1rem; -` -const StyledDialog = styled(Dialog)` - display: flex; - padding: 1rem; - width: 320px; -` - -export const FetchingMissionsDialog = ({ closeDialog }: { closeDialog: () => void }) => { - const { TranslateText } = useLanguageContext() - return ( - - - - - - {TranslateText('Fetching missions') + '...'} - - - - - - - - ) -} diff --git a/frontend/src/pages/FrontPage/MissionOverview/ScheduleMissionDialog/NoMissionsDialog.tsx b/frontend/src/pages/FrontPage/MissionOverview/ScheduleMissionDialog/NoMissionsDialog.tsx deleted file mode 100644 index fe49443d6..000000000 --- a/frontend/src/pages/FrontPage/MissionOverview/ScheduleMissionDialog/NoMissionsDialog.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { Button, Typography } from '@equinor/eds-core-react' -import { useLanguageContext } from 'components/Contexts/LanguageContext' -import { StyledDialog } from 'components/Styles/StyledComponents' - -export const NoMissionsDialog = ({ closeDialog }: { closeDialog: () => void }) => { - const { TranslateText } = useLanguageContext() - return ( - - - {TranslateText('No missions available')} - - - - {TranslateText('This installation does not have missions. Please create mission.')} - - - - - - - ) -} diff --git a/frontend/src/pages/FrontPage/MissionOverview/ScheduleMissionDialog/ScheduleMissionDialog.tsx b/frontend/src/pages/FrontPage/MissionOverview/ScheduleMissionDialog/ScheduleMissionDialog.tsx deleted file mode 100644 index bf9086c96..000000000 --- a/frontend/src/pages/FrontPage/MissionOverview/ScheduleMissionDialog/ScheduleMissionDialog.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { FetchingMissionsDialog } from './FetchingMissionsDialog' -import { NoMissionsDialog } from './NoMissionsDialog' -import { SelectMissionsToScheduleDialog } from './SelectMissionsToScheduleDialog' -import { CondensedMissionDefinition } from 'models/CondensedMissionDefinition' - -export const ScheduleMissionDialog = ({ - onClose, - missions, - isFetchingMissions, -}: { - onClose: () => void - missions: CondensedMissionDefinition[] - isFetchingMissions: boolean -}) => { - const isEmptyMissionsDialogOpen = !isFetchingMissions && missions.length === 0 - const isScheduleMissionDialogOpen = !isFetchingMissions && missions.length !== 0 - - return ( - <> - {isFetchingMissions && } - {isEmptyMissionsDialogOpen && } - {isScheduleMissionDialogOpen && ( - - )} - - ) -} diff --git a/frontend/src/pages/FrontPage/MissionOverview/ScheduleMissionDialog/SelectMissionsToScheduleDialog.tsx b/frontend/src/pages/FrontPage/MissionOverview/ScheduleMissionDialog/SelectMissionsToScheduleDialog.tsx deleted file mode 100644 index 4e52bd402..000000000 --- a/frontend/src/pages/FrontPage/MissionOverview/ScheduleMissionDialog/SelectMissionsToScheduleDialog.tsx +++ /dev/null @@ -1,189 +0,0 @@ -import { Autocomplete, Button, Card, Dialog, Typography } from '@equinor/eds-core-react' -import styled from 'styled-components' -import { useLanguageContext } from 'components/Contexts/LanguageContext' -import { memo, useContext, useState } from 'react' -import { RobotWithoutTelemetry, RobotStatus } from 'models/Robot' -import { CondensedMissionDefinition } from 'models/CondensedMissionDefinition' -import { useAssetContext } from 'components/Contexts/AssetContext' -import { useMissionsContext } from 'components/Contexts/MissionRunsContext' -import { AlertType, useAlertContext } from 'components/Contexts/AlertContext' -import { FailedRequestAlertContent, FailedRequestAlertListContent } from 'components/Alerts/FailedRequestAlert' -import { AlertCategory } from 'components/Alerts/AlertsBanner' -import { phone_width } from 'utils/constants' -import { useBackendApi } from 'api/UseBackendApi' -import { InstallationContext } from 'components/Contexts/InstallationContext' - -const StyledMissionDialog = styled.div` - display: flex; - justify-content: space-between; -` -const StyledAutoComplete = styled(Card)` - display: flex; - justify-content: center; - padding: 8px; - gap: 25px; - box-shadow: none; -` -const StyledMissionSection = styled.div` - display: flex; - margin-left: auto; - margin-right: 0; - gap: 10px; -` -const StyledDialog = styled(Dialog)` - display: flex; - padding: 1rem; - width: 580px; - - @media (max-width: ${phone_width}) { - width: 80vw; - } -` - -interface ScheduleDialogProps { - missionsList: CondensedMissionDefinition[] - closeDialog: () => void -} - -export const SelectMissionsToScheduleDialog = ({ missionsList, closeDialog }: ScheduleDialogProps) => { - const { TranslateText } = useLanguageContext() - const { installation } = useContext(InstallationContext) - const { setAlert, setListAlert } = useAlertContext() - const { setLoadingRobotMissionSet } = useMissionsContext() - const [selectedMissions, setSelectedMissions] = useState([]) - const [selectedRobot, setSelectedRobot] = useState(undefined) - const backendApi = useBackendApi() - - const onScheduleButtonPress = () => { - if (!selectedRobot) return - - selectedMissions.forEach((mission: CondensedMissionDefinition) => { - backendApi.postMission(mission.sourceId, selectedRobot.id, installation.installationCode).catch((e) => { - setAlert( - AlertType.RequestFail, - , - AlertCategory.ERROR - ) - setListAlert( - AlertType.RequestFail, - , - AlertCategory.ERROR - ) - setLoadingRobotMissionSet((currentSet: Set) => { - const updatedSet: Set = new Set(currentSet) - updatedSet.delete(String(mission.name + selectedRobot.id)) - return updatedSet - }) - }) - setLoadingRobotMissionSet((currentSet: Set) => { - const updatedSet: Set = new Set(currentSet) - updatedSet.add(String(mission.name + selectedRobot.id)) - return updatedSet - }) - }) - - setSelectedMissions([]) - setSelectedRobot(undefined) - closeDialog() - } - - return ( - - - - {TranslateText('Add mission to the queue')} - - - - - - - - - - ) -} - -const SelectMissionsComponent = memo( - ({ - missions, - selectedMissions, - setSelectedMissions, - multiple = true, - }: { - missions: CondensedMissionDefinition[] - selectedMissions: CondensedMissionDefinition[] - setSelectedMissions: (missions: CondensedMissionDefinition[]) => void - multiple?: boolean - }) => { - const { TranslateText } = useLanguageContext() - - return ( - m.name} - options={missions} - onOptionsChange={(changes) => setSelectedMissions(changes.selectedItems)} - label={TranslateText('Select missions')} - multiple={multiple} - selectedOptions={selectedMissions} - placeholder={`${selectedMissions.length}/${missions.length} ${TranslateText('selected')}`} - autoWidth - onFocus={(e) => e.preventDefault()} - /> - ) - } -) - -const SelectRobotComponent = memo( - ({ - selectedRobot, - setSelectedRobot, - }: { - selectedRobot: RobotWithoutTelemetry | undefined - setSelectedRobot: (r: RobotWithoutTelemetry | undefined) => void - }) => { - const { enabledRobots } = useAssetContext() - const { TranslateText } = useLanguageContext() - - return ( - (r ? r.name + ' (' + r.type + ')' : '')} - options={enabledRobots.filter( - (r) => - r.status === RobotStatus.Available || - r.status === RobotStatus.Home || - r.status === RobotStatus.ReturningHome || - r.status === RobotStatus.Busy || - r.status === RobotStatus.Recharging - )} - disabled={!enabledRobots} - selectedOptions={selectedRobot ? [selectedRobot] : []} - label={TranslateText('Select robot')} - onOptionsChange={(changes) => setSelectedRobot(changes.selectedItems[0])} - autoWidth - onFocus={(e) => e.preventDefault()} - /> - ) - } -) diff --git a/frontend/src/pages/InspectionReportPage/InspectionReportImage.tsx b/frontend/src/pages/InspectionReportPage/InspectionReportImage.tsx index d585a7dd7..e60747ca9 100644 --- a/frontend/src/pages/InspectionReportPage/InspectionReportImage.tsx +++ b/frontend/src/pages/InspectionReportPage/InspectionReportImage.tsx @@ -4,7 +4,7 @@ import { StyledInspection, StyledInspectionImage } from './InspectionStyles' import { tokens } from '@equinor/eds-tokens' import { CircularProgress, Typography } from '@equinor/eds-core-react' import styled from 'styled-components' -import { DisplayMethod, InspectionTypeToDisplayMethod } from 'models/Inspection' +import { DisplayMethod, SensorTypeToDisplayMethod } from 'models/Inspection' import { useLanguageContext } from 'components/Contexts/LanguageContext' const StyledSmallImagePlaceholder = styled.div` @@ -117,12 +117,12 @@ const InspectionValueWithPlaceholder = ({ task, isLargeImage }: { task: Task; is } const InspectionResultWithPlaceholder = ({ task, isLargeImage }: { task: Task; isLargeImage: boolean }) => { - if (InspectionTypeToDisplayMethod[task.inspection.inspectionType] === DisplayMethod.None) { + if (SensorTypeToDisplayMethod[task.inspection.inspectionType] === DisplayMethod.None) { const errorMsg = 'Viewing of the inspection type is not supported' return - } else if (InspectionTypeToDisplayMethod[task.inspection.inspectionType] === DisplayMethod.Number) { + } else if (SensorTypeToDisplayMethod[task.inspection.inspectionType] === DisplayMethod.Number) { return - } else if (InspectionTypeToDisplayMethod[task.inspection.inspectionType] === DisplayMethod.Image) { + } else if (SensorTypeToDisplayMethod[task.inspection.inspectionType] === DisplayMethod.Image) { return } } diff --git a/frontend/src/pages/InspectionReportPage/InspectionView.tsx b/frontend/src/pages/InspectionReportPage/InspectionView.tsx index 7a18e702c..7699ec7a6 100644 --- a/frontend/src/pages/InspectionReportPage/InspectionView.tsx +++ b/frontend/src/pages/InspectionReportPage/InspectionView.tsx @@ -30,7 +30,8 @@ export const InspectionDialogView = ({ selectedInspectionId, tasks }: Inspection const [switchImageDirection, setSwitchImageDirection] = useState(0) const { switchSelectedInspectionId } = useInspectionId() - const currentTask = tasks.find((t) => t.inspection.isarInspectionId == selectedInspectionId) + const taskIndex = tasks.findIndex((t) => t.inspection.isarInspectionId == selectedInspectionId) + const currentTask = tasks[taskIndex] const closeDialog = () => { switchSelectedInspectionId(undefined) @@ -76,7 +77,7 @@ export const InspectionDialogView = ({ selectedInspectionId, tasks }: Inspection - {TranslateText('Inspection report for task') + ' ' + (currentTask.taskOrder + 1)} + {TranslateText('Inspection report for task') + ' ' + (taskIndex + 1)} diff --git a/frontend/src/pages/MissionDefinitionPage/MissionDefinitionPage.tsx b/frontend/src/pages/MissionDefinitionPage/MissionDefinitionPage.tsx index f19ede9ac..a6efc0910 100644 --- a/frontend/src/pages/MissionDefinitionPage/MissionDefinitionPage.tsx +++ b/frontend/src/pages/MissionDefinitionPage/MissionDefinitionPage.tsx @@ -120,7 +120,6 @@ const MissionDefinitionPageBody = ({ missionDefinition }: { missionDefinition: M comment: missionDefinition.comment, schedulingTimesCETperWeek: [], name: missionDefinition.name, - isDeprecated: false, } backendApi.updateMissionDefinition(missionDefinition.id, defaultMissionDefinitionForm).catch(() => { setAlert( diff --git a/frontend/src/pages/MissionHistory/FilterSection.tsx b/frontend/src/pages/MissionHistory/FilterSection.tsx index 34706d8b9..9119a4025 100644 --- a/frontend/src/pages/MissionHistory/FilterSection.tsx +++ b/frontend/src/pages/MissionHistory/FilterSection.tsx @@ -13,7 +13,7 @@ import { useLanguageContext } from 'components/Contexts/LanguageContext' import { MissionStatusFilterOptions, missionStatusFilterOptionsIterable } from 'models/Mission' import { ChangeEvent, useState } from 'react' import { Icons } from 'utils/icons' -import { InspectionType } from 'models/Inspection' +import { SensorType } from 'models/Inspection' import { useMissionFilterContext } from 'components/Contexts/MissionFilterContext' import { tokens } from '@equinor/eds-tokens' import { phone_width } from 'utils/constants' @@ -62,8 +62,8 @@ export const FilterSection = () => { }) ) - const inspectionTypeTranslationMap: Map = new Map( - Object.values(InspectionType).map((inspectionType) => { + const sensorTypeTranslationMap: Map = new Map( + Object.values(SensorType).map((inspectionType) => { return [TranslateText(inspectionType), inspectionType] }) ) @@ -134,16 +134,16 @@ export const FilterSection = () => { onFocus={(e) => e.preventDefault()} /> ) => { filterFunctions.switchInspectionTypes( changes.selectedItems.map((selectedItem) => { - return inspectionTypeTranslationMap.get(selectedItem)! + return sensorTypeTranslationMap.get(selectedItem)! }) ) }} placeholder={`${filterState.inspectionTypes ? filterState.inspectionTypes.length : 0}/${ - Array.from(inspectionTypeTranslationMap.keys()).length + Array.from(sensorTypeTranslationMap.keys()).length } ${TranslateText('selected')}`} label={TranslateText('Inspection type')} initialSelectedOptions={ diff --git a/frontend/src/pages/MissionHistoryPage.tsx b/frontend/src/pages/MissionHistoryPage.tsx index fec08050d..e336c8024 100644 --- a/frontend/src/pages/MissionHistoryPage.tsx +++ b/frontend/src/pages/MissionHistoryPage.tsx @@ -9,7 +9,7 @@ import styled from 'styled-components' import { useLanguageContext } from 'components/Contexts/LanguageContext' import { PaginationHeader } from 'models/PaginatedResponse' import { useMissionFilterContext, IFilterState, MissionFilterProvider } from 'components/Contexts/MissionFilterContext' -import { InspectionType } from 'models/Inspection' +import { SensorType } from 'models/Inspection' import { tokens } from '@equinor/eds-tokens' import { SmallScreenInfoText } from 'utils/InfoText' import { AlertType, useAlertContext } from 'components/Contexts/AlertContext' @@ -155,7 +155,7 @@ const MissionHistoryViewComponent = () => { const toDisplayValue = ( filterName: string, - value: boolean | string | number | MissionStatusFilterOptions[] | InspectionType[] + value: boolean | string | number | MissionStatusFilterOptions[] | SensorType[] ) => { if (typeof value === 'boolean') { return '' diff --git a/frontend/src/pages/MissionPage/AnalysisResultView.tsx b/frontend/src/pages/MissionPage/AnalysisResultView.tsx index 7de89e3c8..c2f4ed7ee 100644 --- a/frontend/src/pages/MissionPage/AnalysisResultView.tsx +++ b/frontend/src/pages/MissionPage/AnalysisResultView.tsx @@ -42,7 +42,8 @@ export const AnalysisResultDialogView = ({ selectedAnalysisId, tasks }: Inspecti const onClose = () => switchSelectedAnalysisId(undefined) - const currentTask = tasks.find((t) => t.inspection.isarInspectionId == selectedAnalysisId) + const taskIndex = tasks.findIndex((t) => t.inspection.isarInspectionId == selectedAnalysisId) + const currentTask = tasks[taskIndex] if (!currentTask) { return ( @@ -61,7 +62,7 @@ export const AnalysisResultDialogView = ({ selectedAnalysisId, tasks }: Inspecti - {TranslateText('Analysis result for task') + ' ' + (currentTask.taskOrder + 1)} + {TranslateText('Analysis result for task') + ' ' + (taskIndex + 1)} diff --git a/frontend/src/pages/MissionPage/MapPosition/PointillaMapMarkers.tsx b/frontend/src/pages/MissionPage/MapPosition/PointillaMapMarkers.tsx index db2e436fe..8429d9a5a 100644 --- a/frontend/src/pages/MissionPage/MapPosition/PointillaMapMarkers.tsx +++ b/frontend/src/pages/MissionPage/MapPosition/PointillaMapMarkers.tsx @@ -19,12 +19,11 @@ const orderTasksByDrawOrder = (tasks: Task[]) => { const ra = rank(a), rb = rank(b) if (ra !== rb) return ra - rb - if (ra === 1) return b.taskOrder - a.taskOrder - return a.taskOrder - b.taskOrder + return 1 }) } -const getTaskMarker = (map: L.Map, task: Task) => { +const getTaskMarker = (map: L.Map, task: Task, index: number) => { const color = getColorsFromTaskStatus(task.status) const taskMarker = L.circleMarker([task.inspection.inspectionTarget.y, task.inspection.inspectionTarget.x], { @@ -34,7 +33,7 @@ const getTaskMarker = (map: L.Map, task: Task) => { weight: 1, fillOpacity: 0.8, }) - .bindTooltip((task.taskOrder + 1).toString(), { + .bindTooltip((index + 1).toString(), { permanent: true, direction: 'center', className: 'circleLabel', @@ -44,7 +43,7 @@ const getTaskMarker = (map: L.Map, task: Task) => { } export const getTaskMarkers = (map: L.Map, tasks: Task[]) => { - return orderTasksByDrawOrder(tasks).map((task) => getTaskMarker(map, task)) + return orderTasksByDrawOrder(tasks).map((task, index) => getTaskMarker(map, task, index)) } const getRobotAuraMarker = (map: L.Map, robotPose: Pose) => { diff --git a/frontend/src/pages/MissionPage/TaskOverview/TaskTable.tsx b/frontend/src/pages/MissionPage/TaskOverview/TaskTable.tsx index 4a0781cf8..2babf396c 100644 --- a/frontend/src/pages/MissionPage/TaskOverview/TaskTable.tsx +++ b/frontend/src/pages/MissionPage/TaskOverview/TaskTable.tsx @@ -44,9 +44,8 @@ export const TaskTable = ({ tasks, missionDefinitionPage }: TaskTableProps) => { } const TaskTableRows = ({ tasks, missionDefinitionPage }: TaskTableProps) => { - const rows = tasks.map((task) => { - // Workaround for current bug in echo - const order: number = task.taskOrder + 1 + const rows = tasks.map((task, index) => { + const order: number = index + 1 const rowStyle = task.status === TaskStatus.InProgress || task.status === TaskStatus.Paused ? { background: tokens.colors.infographic.primary__mist_blue.hex } diff --git a/frontend/src/pages/PredefinedMissionsPage.tsx b/frontend/src/pages/PredefinedMissionsPage.tsx index acdcb414f..50adc9438 100644 --- a/frontend/src/pages/PredefinedMissionsPage.tsx +++ b/frontend/src/pages/PredefinedMissionsPage.tsx @@ -2,25 +2,16 @@ import { InstallationContext } from 'components/Contexts/InstallationContext' import { Header } from 'components/Header/Header' import { NavBar } from 'components/Header/NavBar' import { useContext } from 'react' -import { Button, Icon, Typography } from '@equinor/eds-core-react' -import { tokens } from '@equinor/eds-tokens' +import { Typography } from '@equinor/eds-core-react' import { useLanguageContext } from 'components/Contexts/LanguageContext' -import { useRef, useState } from 'react' import { getInspectionDeadline } from 'utils/StringFormatting' import styled from 'styled-components' -import { useAssetContext } from 'components/Contexts/AssetContext' -import { AlertType, useAlertContext } from 'components/Contexts/AlertContext' -import { FailedRequestAlertContent, FailedRequestAlertListContent } from 'components/Alerts/FailedRequestAlert' -import { CondensedMissionDefinition } from 'models/CondensedMissionDefinition' -import { Icons } from 'utils/icons' +import { useAlertContext } from 'components/Contexts/AlertContext' import { StyledPage } from 'components/Styles/StyledComponents' import { useMissionDefinitionsContext } from 'components/Contexts/MissionDefinitionsContext' -import { AlertCategory } from 'components/Alerts/AlertsBanner' import { phone_width } from 'utils/constants' -import { useBackendApi } from 'api/UseBackendApi' import { AllInspectionsTable } from './InspectionPage/InspectionTable' import { Placeholder } from './InspectionPage/InspectionUtilities' -import { ScheduleMissionDialog } from './FrontPage/MissionOverview/ScheduleMissionDialog/ScheduleMissionDialog' const StyledContent = styled.div` display: flex; @@ -30,10 +21,6 @@ const StyledContent = styled.div` align-items: start; } ` -const StyledMissionButton = styled.div` - display: flex; - padding-bottom: 30px; -` const StyledPlaceholderContent = styled.div` width: 70vw; ` @@ -41,28 +28,13 @@ const StyledView = styled.div` display: flex; align-items: flex-start; ` -const AlignedTextButton = styled(Button)` - height: auto; - min-height: ${tokens.shape.button.minHeight}; - text-align: left; -` export const PredefinedMissionsPage = () => { - const { alerts, setAlert, setListAlert } = useAlertContext() + const { alerts } = useAlertContext() const { installation } = useContext(InstallationContext) const { TranslateText } = useLanguageContext() - const { enabledRobots } = useAssetContext() const { missionDefinitions } = useMissionDefinitionsContext() - const [isFetchingMissions, setIsFetchingMissions] = useState(false) - const [isScheduleMissionDialogOpen, setIsScheduleMissionDialogOpen] = useState(false) - const [missions, setMissions] = useState([]) - const backendApi = useBackendApi() - - const isScheduleButtonDisabled = enabledRobots.length === 0 || installation.installationCode === '' - - const anchorRef = useRef(null) - const allInspections = missionDefinitions.map((m) => { return { missionDefinition: m, @@ -72,41 +44,6 @@ export const PredefinedMissionsPage = () => { } }) - const fetchMissions = () => { - setIsFetchingMissions(true) - backendApi - .getAvailableMissions(installation.installationCode as string) - .then((missions) => { - setMissions(missions) - setIsFetchingMissions(false) - }) - .catch(() => { - setAlert( - AlertType.RequestFail, - , - AlertCategory.ERROR - ) - setListAlert( - AlertType.RequestFail, - , - AlertCategory.ERROR - ) - setIsFetchingMissions(false) - }) - } - - const onClickScheduleMission = () => { - setIsScheduleMissionDialogOpen(true) - fetchMissions() - } - - const AddPredefinedMissionsButton = () => ( - - - {TranslateText('Add predefined mission to queue')} - - ) - return ( <>
@@ -114,16 +51,6 @@ export const PredefinedMissionsPage = () => { - - {isScheduleMissionDialogOpen && ( - setIsScheduleMissionDialogOpen(false)} - /> - )} - - {allInspections.length > 0 ? ( ) : (