From d646443b44889b10e9ac93c75495b61c43024ce8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20Chirico=20Indreb=C3=B8?= Date: Mon, 6 Jan 2025 14:47:15 +0100 Subject: [PATCH] Do partial robot updates with SignalR --- .../Models/RobotAttributeResponse.cs | 37 ++++++ backend/api/Services/RobotService.cs | 106 +++++++++++++----- .../src/components/Contexts/RobotContext.tsx | 37 +++++- .../components/Contexts/SignalRContext.tsx | 1 + frontend/src/models/Robot.ts | 6 + 5 files changed, 155 insertions(+), 32 deletions(-) create mode 100644 backend/api/Controllers/Models/RobotAttributeResponse.cs diff --git a/backend/api/Controllers/Models/RobotAttributeResponse.cs b/backend/api/Controllers/Models/RobotAttributeResponse.cs new file mode 100644 index 00000000..ab5f4337 --- /dev/null +++ b/backend/api/Controllers/Models/RobotAttributeResponse.cs @@ -0,0 +1,37 @@ +using System.Text.Json.Serialization; +using Api.Database.Models; + +namespace Api.Controllers.Models +{ + public class RobotAttributeResponse + { + public string Id { get; set; } + + public string PropertyName { get; set; } + + public object? Value { get; set; } + + [JsonConstructor] +#nullable disable + public RobotAttributeResponse() { } + +#nullable enable + + public RobotAttributeResponse(string robotId, string propertyName, object? robotProperty) + { + Id = robotId; + PropertyName = propertyName; + Value = robotProperty; + if ( + !typeof(RobotResponse) + .GetProperties() + .Any(property => property.Name == propertyName) + ) + { + throw new ArgumentException( + $"Property {robotProperty} does not match any attributes in the RobotAttributeResponse class" + ); + } + } + } +} diff --git a/backend/api/Services/RobotService.cs b/backend/api/Services/RobotService.cs index 50dab6c0..ec647bc1 100644 --- a/backend/api/Services/RobotService.cs +++ b/backend/api/Services/RobotService.cs @@ -334,11 +334,8 @@ public async Task Update(Robot robot) context.Update(robot); await ApplyDatabaseUpdate(robot.CurrentInstallation); - _ = signalRService.SendMessageAsync( - "Robot updated", - robot?.CurrentInstallation, - robot != null ? new RobotResponse(robot) : null - ); + if (robot.CurrentInstallation != null) + NotifySignalROfUpdatedRobot(robot!, robot!.CurrentInstallation!); DetachTracking(robot!); } @@ -421,33 +418,57 @@ private async Task UpdateRobotProperty( throw new RobotNotFoundException(errorMessage); } - foreach (var property in typeof(Robot).GetProperties()) + var updatedProperty = typeof(Robot) + .GetProperties() + .FirstOrDefault((property) => property.Name == propertyName); + + if (updatedProperty == null) { - if (property.Name == propertyName) - { - if (isLogLevelDebug) - logger.LogDebug( - "Setting {robotName} field {propertyName} from {oldValue} to {NewValue}", - robot.Name, - propertyName, - property.GetValue(robot), - value - ); - else - logger.LogInformation( - "Setting {robotName} field {propertyName} from {oldValue} to {NewValue}", - robot.Name, - propertyName, - property.GetValue(robot), - value - ); - property.SetValue(robot, value); - } + logger.LogError( + "Failed to update {robotName} as it did not have the property {property}", + robot.Name, + propertyName + ); + DetachTracking(robot); + return; } + if (isLogLevelDebug) + logger.LogDebug( + "Setting {robotName} field {propertyName} from {oldValue} to {NewValue}", + robot.Name, + propertyName, + updatedProperty.GetValue(robot), + value + ); + else + logger.LogInformation( + "Setting {robotName} field {propertyName} from {oldValue} to {NewValue}", + robot.Name, + propertyName, + updatedProperty.GetValue(robot), + value + ); + + updatedProperty.SetValue(robot, value); + try { - await Update(robot); + if (robot.CurrentInspectionArea is not null) + context.Entry(robot.CurrentInspectionArea).State = EntityState.Unchanged; + context.Entry(robot.Model).State = EntityState.Unchanged; + + context.Update(robot); + await ApplyDatabaseUpdate(robot.CurrentInstallation); + if (robot.CurrentInstallation != null) + NotifySignalROfUpdatedRobot( + robot!, + robot!.CurrentInstallation!, + propertyName, + value + ); + + DetachTracking(robot!); } catch (InvalidOperationException e) { @@ -491,6 +512,37 @@ private async Task VerifyThatUserIsAuthorizedToUpdateDataForInstallation( ); } + private void NotifySignalROfUpdatedRobot( + Robot robot, + Installation installation, + string propertyName, + object? propertyValue + ) + { + try + { + if (propertyName == typeof(InspectionArea).Name) + { + if (propertyValue != null) + propertyValue = new InspectionAreaResponse((InspectionArea)propertyValue); + propertyValue = (InspectionAreaResponse?)propertyValue; + } + + var responseObject = new RobotAttributeResponse( + robot.Id, + propertyName, + propertyName + ); + + _ = signalRService.SendMessageAsync( + "Robot attribute updated", + installation, + robot != null ? responseObject : null + ); + } + catch (ArgumentException) { } + } + private void NotifySignalROfUpdatedRobot(Robot robot, Installation installation) { _ = signalRService.SendMessageAsync( diff --git a/frontend/src/components/Contexts/RobotContext.tsx b/frontend/src/components/Contexts/RobotContext.tsx index 1153632e..b2335d51 100644 --- a/frontend/src/components/Contexts/RobotContext.tsx +++ b/frontend/src/components/Contexts/RobotContext.tsx @@ -1,6 +1,6 @@ import { createContext, useContext, useState, FC, useEffect } from 'react' import { BackendAPICaller } from 'api/ApiCaller' -import { Robot } from 'models/Robot' +import { Robot, RobotAttributeUpdate } from 'models/Robot' import { SignalREventLabels, useSignalRContext } from './SignalRContext' import { useLanguageContext } from './LanguageContext' import { AlertType, useAlertContext } from './AlertContext' @@ -8,11 +8,18 @@ import { FailedRequestAlertContent, FailedRequestAlertListContent } from 'compon import { AlertCategory } from 'components/Alerts/AlertsBanner' import { useInstallationContext } from './InstallationContext' -const upsertRobotList = (list: Robot[], mission: Robot) => { +const upsertRobotList = (list: Robot[], robot: Robot) => { const newList = [...list] - const i = newList.findIndex((e) => e.id === mission.id) - if (i > -1) newList[i] = mission - else newList.push(mission) + const i = newList.findIndex((e) => e.id === robot.id) + if (i > -1) newList[i] = robot + else newList.push(robot) + return newList +} + +const updateRobotInList = (list: Robot[], robotId: string, mapping: (old: Robot) => Robot) => { + const newList = [...list] + const i = newList.findIndex((e) => e.id === robotId) + if (i > -1) newList[i] = mapping(newList[i]) return newList } @@ -60,6 +67,26 @@ export const RobotProvider: FC = ({ children }) => { return [...oldRobotListCopy] }) }) + registerEvent(SignalREventLabels.robotAttributeUpdated, (username: string, message: string) => { + const updatedRobot: RobotAttributeUpdate = JSON.parse(message) + // The check below makes it so that it is not treated as null in the code. + + const updatedProperty = updatedRobot.propertyName + if (!updatedProperty) return + + const updatedValue = updatedRobot.value + + const updateFunction = (oldRobot: Robot): Robot => { + if (Object.keys(oldRobot).includes(updatedProperty)) + oldRobot = { ...oldRobot, [updatedProperty]: updatedValue } + return oldRobot + } + setEnabledRobots((oldRobotList) => { + let oldRobotListCopy = [...oldRobotList] + oldRobotListCopy = updateRobotInList(oldRobotListCopy, updatedRobot.id, updateFunction) + return [...oldRobotListCopy] + }) + }) registerEvent(SignalREventLabels.robotDeleted, (username: string, message: string) => { const updatedRobot: Robot = JSON.parse(message) setEnabledRobots((oldRobotList) => { diff --git a/frontend/src/components/Contexts/SignalRContext.tsx b/frontend/src/components/Contexts/SignalRContext.tsx index 0a63c0ba..2d691ed8 100644 --- a/frontend/src/components/Contexts/SignalRContext.tsx +++ b/frontend/src/components/Contexts/SignalRContext.tsx @@ -119,6 +119,7 @@ export enum SignalREventLabels { inspectionAreaDeleted = 'InspectionArea deleted', robotAdded = 'Robot added', robotUpdated = 'Robot updated', + robotAttributeUpdated = 'Robot attribute updated', robotDeleted = 'Robot deleted', inspectionUpdated = 'Inspection updated', alert = 'Alert', diff --git a/frontend/src/models/Robot.ts b/frontend/src/models/Robot.ts index 037a8621..8cc4b9dd 100644 --- a/frontend/src/models/Robot.ts +++ b/frontend/src/models/Robot.ts @@ -21,6 +21,12 @@ export enum RobotFlotillaStatus { Recharging = 'Recharging', } +export interface RobotAttributeUpdate { + id: string + propertyName: string + value: any +} + export interface Robot { id: string name?: string