From ce87279e2bceff53a2c7280b988d7c02bbef50d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eivind=20Sj=C3=B8vold?= Date: Mon, 5 Sep 2022 11:15:14 +0200 Subject: [PATCH] Add working schedule mission from GUI --- .../api/Controllers/EchoMissionController.cs | 71 +------------------ .../api/Controllers/EchoPlantController.cs | 54 ++++++++++++++ .../api/Controllers/Models/EchoPlantInfo.cs | 4 +- .../Models/EchoPlantInfoResponse.cs | 4 +- backend/api/Services/EchoService.cs | 35 ++------- backend/api/appsettings.json | 2 +- frontend/src/api/ApiCaller.tsx | 36 +++++----- .../src/components/Contexts/AssetContext.tsx | 2 +- frontend/src/components/Header/Header.tsx | 33 +++++---- .../MissionOverview/OngoingMissionView.tsx | 3 +- .../MissionOverview/ScheduleMissionDialog.tsx | 50 ++++++++++--- .../MissionOverview/UpcomingMissionView.tsx | 53 ++++++++++---- frontend/src/models/EchoMission.ts | 5 ++ frontend/src/models/EchoPlantInfo.ts | 4 -- frontend/src/utils/scheduleMission.tsx | 6 ++ 15 files changed, 196 insertions(+), 166 deletions(-) create mode 100644 backend/api/Controllers/EchoPlantController.cs delete mode 100644 frontend/src/models/EchoPlantInfo.ts create mode 100644 frontend/src/utils/scheduleMission.tsx diff --git a/backend/api/Controllers/EchoMissionController.cs b/backend/api/Controllers/EchoMissionController.cs index d3cb16480..42a71c263 100644 --- a/backend/api/Controllers/EchoMissionController.cs +++ b/backend/api/Controllers/EchoMissionController.cs @@ -34,11 +34,11 @@ public EchoMissionController(ILogger logger, IEchoService [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] [ProducesResponseType(StatusCodes.Status502BadGateway)] - public async Task>> GetEchoMissions() + public async Task>> GetEchoMissions(string? installationCode) { try { - var missions = await _echoService.GetMissions(); + var missions = await _echoService.GetMissions(installationCode); return Ok(missions); } catch (HttpRequestException e) @@ -95,71 +95,4 @@ public async Task> GetEchoMission([FromRoute] int miss return new StatusCodeResult(StatusCodes.Status500InternalServerError); } } - [HttpGet] - [Route("installation/{installationCode}")] - [ProducesResponseType(typeof(EchoMission), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - [ProducesResponseType(StatusCodes.Status502BadGateway)] - public async Task> GetEchoMissionsFromInstallation([FromRoute] string installationCode) - { - try - { - var mission = await _echoService.GetMissionsByInstallation(installationCode); - return Ok(mission); - } - catch (HttpRequestException e) - { - if (e.StatusCode.HasValue && (int)e.StatusCode.Value == 404) - { - _logger.LogWarning("Could not find echo mission from installation with installationCode={installationCode}", installationCode); - return NotFound("Echo mission not found"); - } - - _logger.LogError(e, "Error getting mission from Echo"); - return new StatusCodeResult(StatusCodes.Status502BadGateway); - } - catch (JsonException e) - { - _logger.LogError(e, "Error deserializing mission from Echo"); - return new StatusCodeResult(StatusCodes.Status500InternalServerError); - } - } - - [HttpGet] - [Route("echo-plant-info")] - [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - [ProducesResponseType(StatusCodes.Status502BadGateway)] - public async Task> GetEchoPlantInfos() - { - try - { - var echoPlantInfos = await _echoService.GetEchoPlantInfos(); - return Ok(echoPlantInfos); - } - catch (HttpRequestException e) - { - if (e.StatusCode.HasValue && (int)e.StatusCode.Value == 404) - { - _logger.LogWarning("Could not get plant info from Echo"); - return NotFound("Echo plant info not found"); - } - - _logger.LogError(e, "Error getting plant info from Echo"); - return new StatusCodeResult(StatusCodes.Status502BadGateway); - } - catch (JsonException e) - { - _logger.LogError(e, "Error deserializing plant info response from Echo"); - return new StatusCodeResult(StatusCodes.Status500InternalServerError); - } - } } diff --git a/backend/api/Controllers/EchoPlantController.cs b/backend/api/Controllers/EchoPlantController.cs new file mode 100644 index 000000000..794aaea28 --- /dev/null +++ b/backend/api/Controllers/EchoPlantController.cs @@ -0,0 +1,54 @@ +using System.Text.Json; +using Api.Controllers.Models; +using Api.Services; +using Microsoft.AspNetCore.Mvc; + +namespace Api.Controllers; +[ApiController] +[Route("echo-plant")] +public class EchoPlantController : ControllerBase +{ + private readonly ILogger _logger; + private readonly IEchoService _echoService; + public EchoPlantController(ILogger logger, IEchoService echoService) + { + _logger = logger; + _echoService = echoService; + } + + [HttpGet] + [Route("/all-plants-info")] + [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + [ProducesResponseType(StatusCodes.Status502BadGateway)] + public async Task> GetEchoPlantInfos() + { + try + { + var echoPlantInfos = await _echoService.GetEchoPlantInfos(); + return Ok(echoPlantInfos); + } + catch (HttpRequestException e) + { + if (e.StatusCode.HasValue && (int)e.StatusCode.Value == 404) + { + _logger.LogWarning("Could not get plant info from Echo"); + return NotFound("Echo plant info not found"); + } + + _logger.LogError(e, "Error getting plant info from Echo"); + return new StatusCodeResult(StatusCodes.Status502BadGateway); + } + catch (JsonException e) + { + _logger.LogError(e, "Error deserializing plant info response from Echo"); + return new StatusCodeResult(StatusCodes.Status500InternalServerError); + } + } +} + + diff --git a/backend/api/Controllers/Models/EchoPlantInfo.cs b/backend/api/Controllers/Models/EchoPlantInfo.cs index 6c4d3e45b..c03366abc 100644 --- a/backend/api/Controllers/Models/EchoPlantInfo.cs +++ b/backend/api/Controllers/Models/EchoPlantInfo.cs @@ -1,4 +1,4 @@ -#nullable disable +#nullable disable namespace Api.Controllers.Models { public class EchoPlantInfo @@ -6,4 +6,4 @@ public class EchoPlantInfo public string InstallationCode { get; set; } public string ProjectDescription { get; set; } } -} \ No newline at end of file +} diff --git a/backend/api/Controllers/Models/EchoPlantInfoResponse.cs b/backend/api/Controllers/Models/EchoPlantInfoResponse.cs index 3b867fe5f..4bb6b760f 100644 --- a/backend/api/Controllers/Models/EchoPlantInfoResponse.cs +++ b/backend/api/Controllers/Models/EchoPlantInfoResponse.cs @@ -1,4 +1,4 @@ -#nullable enable +#nullable enable using System.Text.Json.Serialization; namespace Api.Controllers.Models { @@ -14,7 +14,7 @@ public class EchoPlantInfoResponse public string? ProjectDescription { get; set; } [JsonPropertyName("plantDirectory")] - public string? plantDirectory { get; set; } + public string? PlantDirectory { get; set; } [JsonPropertyName("availableInEcho3D")] public bool AvailableInEcho3D { get; set; } diff --git a/backend/api/Services/EchoService.cs b/backend/api/Services/EchoService.cs index cbeceb154..d6fdc8422 100644 --- a/backend/api/Services/EchoService.cs +++ b/backend/api/Services/EchoService.cs @@ -8,11 +8,10 @@ namespace Api.Services { public interface IEchoService { - public abstract Task> GetMissions(); + public abstract Task> GetMissions(string? installationCode); public abstract Task GetMissionById(int missionId); - public abstract Task> GetMissionsByInstallation(string installationCode); public abstract Task> GetEchoPlantInfos(); } @@ -28,9 +27,11 @@ public EchoService(IConfiguration config, IDownstreamWebApi downstreamWebApi) _installationCode = config.GetValue("InstallationCode"); } - public async Task> GetMissions() + public async Task> GetMissions(string? installationCode) { - string relativePath = $"robots/robot-plan"; + string relativePath = string.IsNullOrEmpty(installationCode) ? + $"robots/robot-plan" : + $"robots/robot-plan?InstallationCode={installationCode}"; var response = await _echoApi.CallWebApiForAppAsync( ServiceName, @@ -51,31 +52,7 @@ public async Task> GetMissions() throw new JsonException("Failed to deserialize missions from Echo"); var missions = ProcessEchoMissions(echoMissions); - return missions; - } - public async Task> GetMissionsByInstallation(string installationCode) - { - string relativePath = $"robots/robot-plan?InstallationCode={installationCode}"; - - var response = await _echoApi.CallWebApiForAppAsync( - ServiceName, - options => - { - options.HttpMethod = HttpMethod.Get; - options.RelativePath = relativePath; - } - ); - - response.EnsureSuccessStatusCode(); - var echoMissions = await response.Content.ReadFromJsonAsync< - List - >(); - - if (echoMissions is null) - throw new JsonException("Failed to deserialize missions from Echo"); - - var missions = ProcessEchoMissions(echoMissions); return missions; } @@ -201,7 +178,7 @@ private EchoMission ProcessEchoMission(EchoMissionResponse echoMission) return mission; } - private List ProcessEchoPlantInfos(List echoPlantInfoResponse) + private static List ProcessEchoPlantInfos(List echoPlantInfoResponse) { var echoPlantInfos = new List(); foreach (var plant in echoPlantInfoResponse) diff --git a/backend/api/appsettings.json b/backend/api/appsettings.json index 0fa0b8f73..793ec060c 100644 --- a/backend/api/appsettings.json +++ b/backend/api/appsettings.json @@ -30,4 +30,4 @@ "Database": { "ConnectionString": "" } -} \ No newline at end of file +} diff --git a/frontend/src/api/ApiCaller.tsx b/frontend/src/api/ApiCaller.tsx index d43da5a28..bda475023 100644 --- a/frontend/src/api/ApiCaller.tsx +++ b/frontend/src/api/ApiCaller.tsx @@ -1,12 +1,11 @@ import { AccessTokenContext } from 'components/Pages/FlotillaSite' import { config } from 'config' -import { EchoMission } from 'models/EchoMission' +import { EchoMission, EchoPlantInfo } from 'models/EchoMission' import { Mission, MissionStatus } from 'models/Mission' -import { EchoPlantInfo } from 'models/EchoPlantInfo' -import { Report } from 'models/Report' import { Robot } from 'models/Robot' import { VideoStream } from 'models/VideoStream' import { useContext, useEffect, useRef } from 'react' +import { filterRobots } from 'utils/scheduleMission' export class BackendAPICaller { /* Implements the request sent to the backend api. @@ -86,25 +85,14 @@ export class BackendAPICaller { return result.body } - async getAllEchoMissions(): Promise { - const path: string = 'echo-missions' - const result = await this.GET(path).catch((e) => { + async getEchoMissions(installationCode: string = ''): Promise { + const path: string = 'echo-missions?installationCode=' + installationCode + const result = await this.GET(path).catch((e) => { throw new Error(`Failed to GET /${path}: ` + e) }) return result.body } - async getEchoMissionsForPlant(installationCode: string): Promise { - if (installationCode) { - const path: string = 'echo-missions/installation/' + installationCode; - const result = await this.GET(path).catch((e) => { - throw new Error(`Failed to GET /${path}: ` + e) - }) - return result.body - } - else - return this.getAllEchoMissions() - } async getMissionById(missionId: string): Promise { const path: string = 'missions/' + missionId const result = await this.GET(path).catch((e) => { @@ -121,12 +109,22 @@ export class BackendAPICaller { return result.body } async getEchoPlantInfo(): Promise { - const path: string = "echo-missions/echo-plant-info"; - const result = await this.GET(path).catch((e) => { + const path: string = 'all-plants-info' + const result = await this.GET(path).catch((e: Error) => { throw new Error(`Failed to GET /${path}: ` + e) }) return result.body } + async postMission(echoMissionId: number, startTime: Date) { + const path: string = 'missions' + const robots: Robot[] = await this.getRobots() + const desiredRobot = filterRobots(robots, 'R2-D2') + const body = { robotId: desiredRobot[0].id, echoMissionId: echoMissionId, startTime: startTime } + const result = await this.POST(path, body).catch((e) => { + throw new Error(`Failed to POST /${path}: ` + e) + }) + return result.body + } } export const useApi = () => { diff --git a/frontend/src/components/Contexts/AssetContext.tsx b/frontend/src/components/Contexts/AssetContext.tsx index 7a7ba45d7..59393569b 100644 --- a/frontend/src/components/Contexts/AssetContext.tsx +++ b/frontend/src/components/Contexts/AssetContext.tsx @@ -11,7 +11,7 @@ interface Props { const defaultAsset = { asset: 'test', - switchAsset: (newAsset: string) => { }, + switchAsset: (newAsset: string) => {}, } export const assetOptions = new Map([ diff --git a/frontend/src/components/Header/Header.tsx b/frontend/src/components/Header/Header.tsx index 01dc3030c..6b8ef061d 100644 --- a/frontend/src/components/Header/Header.tsx +++ b/frontend/src/components/Header/Header.tsx @@ -1,8 +1,8 @@ import { Button, Icon, Search, TopBar, Autocomplete } from '@equinor/eds-core-react' import { accessible, account_circle, notifications } from '@equinor/eds-icons' import { useApi } from 'api/ApiCaller' -import { assetOptions, useAssetContext } from 'components/Contexts/AssetContext' -import { EchoPlantInfo } from 'models/EchoPlantInfo' +import { useAssetContext } from 'components/Contexts/AssetContext' +import { EchoPlantInfo } from 'models/EchoMission' import { useEffect, useState } from 'react' import styled from 'styled-components' @@ -55,17 +55,14 @@ export function Header() { } function AssetPicker() { - const apiCaller = useApi(); - const [allPlantsMap, setAllPlantsMap] = useState>(); + const apiCaller = useApi() + const [allPlantsMap, setAllPlantsMap] = useState>() const { asset, switchAsset } = useAssetContext() useEffect(() => { apiCaller.getEchoPlantInfo().then((response: EchoPlantInfo[]) => { - var temporaryMap = new Map() - response.map((echoPlantInfo: EchoPlantInfo) => { - temporaryMap.set(echoPlantInfo.projectDescription, echoPlantInfo.installationCode) - }) - setAllPlantsMap(temporaryMap); - }); + const mapping = mapAssetCodeToName(response) + setAllPlantsMap(mapping) + }) }, []) let savedAsset = sessionStorage.getItem('assetString') let initialOption = '' @@ -82,11 +79,17 @@ function AssetPicker() { placeholder="Select asset" onOptionsChange={({ selectedItems }) => { const mapKey = mappedOptions.get(selectedItems[0]) - if (mapKey != undefined) - switchAsset(mapKey) - else - switchAsset("") + if (mapKey != undefined) switchAsset(mapKey) + else switchAsset('') }} /> ) -} \ No newline at end of file +} + +const mapAssetCodeToName = (echoPlantInfoArray: EchoPlantInfo[]): Map => { + var mapping = new Map() + echoPlantInfoArray.map((echoPlantInfo: EchoPlantInfo) => { + mapping.set(echoPlantInfo.projectDescription, echoPlantInfo.installationCode) + }) + return mapping +} diff --git a/frontend/src/components/MissionOverview/OngoingMissionView.tsx b/frontend/src/components/MissionOverview/OngoingMissionView.tsx index e893a4660..e71b81a00 100644 --- a/frontend/src/components/MissionOverview/OngoingMissionView.tsx +++ b/frontend/src/components/MissionOverview/OngoingMissionView.tsx @@ -21,8 +21,7 @@ export function OngoingMissionView() { const apiCaller = useApi() const [ongoingMissions, setOngoingMissions] = useState([]) useEffect(() => { - // Temporarily using the upcoming dummy missions for ongoing dummy missions - apiCaller.getMissionsByStatus(MissionStatus.Pending).then((missions) => { + apiCaller.getMissionsByStatus(MissionStatus.Ongoing).then((missions) => { setOngoingMissions(missions) }) }, []) diff --git a/frontend/src/components/MissionOverview/ScheduleMissionDialog.tsx b/frontend/src/components/MissionOverview/ScheduleMissionDialog.tsx index 92b618058..64dad0e33 100644 --- a/frontend/src/components/MissionOverview/ScheduleMissionDialog.tsx +++ b/frontend/src/components/MissionOverview/ScheduleMissionDialog.tsx @@ -1,9 +1,12 @@ -import { Autocomplete, Button, Card, Checkbox, Dialog, Typography } from "@equinor/eds-core-react"; -import { useState } from "react"; -import styled from "styled-components"; +import { Autocomplete, AutocompleteChanges, Button, Card, Checkbox, Dialog, Typography } from '@equinor/eds-core-react' +import { Mission } from 'models/Mission' +import { useState } from 'react' +import styled from 'styled-components' interface IProps { - options: Array; + options: Array + onSelectedMissions: (missions: string[]) => void + onScheduleButtonPress: () => void } const StyledMissionDialog = styled.div` @@ -22,22 +25,49 @@ const StyledMissionSection = styled.div` ` export const ScheduleMissionDialog = (props: IProps): JSX.Element => { - const [isOpen, setIsOpen] = useState(false); + const [isOpen, setIsOpen] = useState(false) + const onChange = (changes: AutocompleteChanges) => { + props.onSelectedMissions(changes.selectedItems) + } return ( <> - - + Schedule mission - - + + diff --git a/frontend/src/components/MissionOverview/UpcomingMissionView.tsx b/frontend/src/components/MissionOverview/UpcomingMissionView.tsx index d19f5007a..cc46d341f 100644 --- a/frontend/src/components/MissionOverview/UpcomingMissionView.tsx +++ b/frontend/src/components/MissionOverview/UpcomingMissionView.tsx @@ -6,7 +6,7 @@ import { useEffect, useState } from 'react' import { Mission, MissionStatus } from 'models/Mission' import { NoUpcomingMissionsPlaceholder } from './NoMissionPlaceholder' import { ScheduleMissionDialog } from './ScheduleMissionDialog' -import { Header } from 'components/Header/Header' +import { EchoMission } from 'models/EchoMission' const StyledMissionView = styled.div` display: grid; @@ -24,19 +24,33 @@ const MissionButtonView = styled.div` display: flex; gap: 2rem; ` -const processEchoMissions = (missions: Mission[]): string[] => { - const stringifiedArray: string[] = []; - missions.map((mission: Mission) => { - stringifiedArray.push(mission.id + ": " + mission.name); +const mapEchoMissionToString = (missions: EchoMission[]): Map => { + var missionMap = new Map() + missions.map((mission: EchoMission) => { + missionMap.set(mission.id + ': ' + mission.name, mission) }) - return stringifiedArray; + return missionMap } - export function UpcomingMissionView() { const apiCaller = useApi() const [upcomingMissions, setUpcomingMissions] = useState([]) - const [echoMissions, setEchoMissions] = useState([]); + const [selectedEchoMission, setSelectedEchoMissions] = useState([]) + const [echoMissions, setEchoMissions] = useState>() + const [assetString, setAssetString] = useState('') + + const onSelectedEchoMissions = (selectedEchoMissions: string[]) => { + var echoMissionsToSchedule: EchoMission[] = [] + selectedEchoMissions.map((selectedEchoMission: string) => { + if (echoMissions) echoMissionsToSchedule.push(echoMissions.get(selectedEchoMission) as EchoMission) + }) + setSelectedEchoMissions(echoMissionsToSchedule) + } + const onScheduleButtonPress = () => { + selectedEchoMission.map((mission: EchoMission) => { + apiCaller.postMission(mission.id, new Date()) + }) + } useEffect(() => { apiCaller.getMissionsByStatus(MissionStatus.Pending).then((missions) => { setUpcomingMissions(missions) @@ -45,8 +59,15 @@ export function UpcomingMissionView() { useEffect(() => { const installationCode = sessionStorage.getItem('assetString') - apiCaller.getEchoMissionsForPlant(installationCode as string).then((missions) => { - setEchoMissions(missions) + if (installationCode != assetString) { + setAssetString(installationCode as string) + } + }, [sessionStorage.getItem('assetString')]) + + useEffect(() => { + apiCaller.getEchoMissions(assetString).then((missions) => { + const mappedEchoMissions: Map = mapEchoMissionToString(missions) + setEchoMissions(mappedEchoMissions) }) }, [apiCaller]) @@ -69,8 +90,16 @@ export function UpcomingMissionView() { {upcomingMissions.length === 0 && } - - + {echoMissions && ( + <> + + + + )} ) diff --git a/frontend/src/models/EchoMission.ts b/frontend/src/models/EchoMission.ts index 8c4685b23..6556326af 100644 --- a/frontend/src/models/EchoMission.ts +++ b/frontend/src/models/EchoMission.ts @@ -16,3 +16,8 @@ export interface Inspection { inspectionType: string timeInSeconds: number } + +export interface EchoPlantInfo { + installationCode: string + projectDescription: string +} diff --git a/frontend/src/models/EchoPlantInfo.ts b/frontend/src/models/EchoPlantInfo.ts deleted file mode 100644 index f06da9d74..000000000 --- a/frontend/src/models/EchoPlantInfo.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface EchoPlantInfo { - installationCode: string - projectDescription: string -} \ No newline at end of file diff --git a/frontend/src/utils/scheduleMission.tsx b/frontend/src/utils/scheduleMission.tsx new file mode 100644 index 000000000..2a496e3be --- /dev/null +++ b/frontend/src/utils/scheduleMission.tsx @@ -0,0 +1,6 @@ +import { Robot } from 'models/Robot' + +export const filterRobots = (robots: Robot[], name: string): Robot[] => { + const desiredRobot = robots.filter((robot: Robot) => robot.name === name) + return desiredRobot +}