diff --git a/backend/api/Controllers/InspectionController.cs b/backend/api/Controllers/InspectionController.cs index 39ffe10a4..9fafd4413 100644 --- a/backend/api/Controllers/InspectionController.cs +++ b/backend/api/Controllers/InspectionController.cs @@ -1,4 +1,7 @@ -using Api.Controllers.Models; +using System.Globalization; +using Api.Controllers.Models; +using Api.Database.Models; + using Api.Services; using Api.Services.MissionLoaders; using Api.Services.Models; @@ -11,7 +14,8 @@ namespace Api.Controllers [Route("inspection")] public class InspectionController( ILogger logger, - IEchoService echoService + IEchoService echoService, + IInspectionService inspectionService ) : ControllerBase { /// @@ -49,5 +53,56 @@ public async Task> Create([FromRoute] string throw; } } + + /// + /// Lookup the inspection image for task with specified isarTaskId + /// + /// + /// + [HttpGet] + [Authorize(Roles = Role.User)] + [Route("{installationCode}/{taskId}/taskId")] + [ProducesResponseType(typeof(Inspection), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task> GetInspectionImageById([FromRoute] string installationCode, string taskId) + { + Inspection? inspection; + try + { + inspection = await inspectionService.ReadByIsarTaskId(taskId, readOnly: true); + if (inspection == null) return NotFound($"Could not find inspection for task with Id {taskId}."); + + } + catch (Exception e) + { + logger.LogError(e, $"Error while finding an inspection with task Id {taskId}"); + return StatusCode(StatusCodes.Status500InternalServerError); + } + + if (inspection.Id == null) return NotFound($"Could not find Id for Inspection with task ID {taskId}."); + + var inspectionData = await inspectionService.GetInspectionStorageInfo(inspection.Id); + + if (inspectionData == null) return NotFound($"Could not find inspection data for inspection with Id {inspection.Id}."); + + if (!inspectionData.BlobContainer.ToLower(CultureInfo.CurrentCulture).Equals(installationCode.ToLower(CultureInfo.CurrentCulture), StringComparison.Ordinal)) + { + return NotFound($"Could not find inspection data for inspection with Id {inspection.Id} because blob name {inspectionData.BlobName} does not match installation {installationCode}."); + } + + try + { + byte[] inspectionStream = await inspectionService.FetchInpectionImage(inspectionData.BlobName, inspectionData.BlobContainer, inspectionData.StorageAccount); + return File(inspectionStream, "image/png"); + } + catch (Azure.RequestFailedException) + { + return NotFound($"Could not find inspection blob {inspectionData.BlobName} in container {inspectionData.BlobContainer} and storage account {inspectionData.StorageAccount}."); + } + } } } diff --git a/backend/api/Program.cs b/backend/api/Program.cs index c33e87916..c150ca116 100644 --- a/backend/api/Program.cs +++ b/backend/api/Program.cs @@ -121,6 +121,7 @@ builder.Services.Configure(builder.Configuration.GetSection("AzureAd")); builder.Services.Configure(builder.Configuration.GetSection("Maps")); + builder.Services .AddControllers() .AddJsonOptions( @@ -142,6 +143,7 @@ .AddInMemoryTokenCaches() .AddDownstreamApi(EchoService.ServiceName, builder.Configuration.GetSection("Echo")) .AddDownstreamApi(StidService.ServiceName, builder.Configuration.GetSection("Stid")) + .AddDownstreamApi(InspectionService.ServiceName, builder.Configuration.GetSection("IDA")) .AddDownstreamApi(IsarService.ServiceName, builder.Configuration.GetSection("Isar")); builder.Services.AddAuthorizationBuilder().AddFallbackPolicy( diff --git a/backend/api/Services/InspectionService.cs b/backend/api/Services/InspectionService.cs index 377f13982..aff7a0e51 100644 --- a/backend/api/Services/InspectionService.cs +++ b/backend/api/Services/InspectionService.cs @@ -1,18 +1,23 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; +using System.Net; +using System.Text.Json; using Api.Controllers.Models; using Api.Database.Context; using Api.Database.Models; using Api.Services.Models; using Api.Utilities; using Microsoft.EntityFrameworkCore; +using Microsoft.Identity.Abstractions; namespace Api.Services { public interface IInspectionService { + public Task FetchInpectionImage(string inpectionName, string installationCode, string storageAccount); public Task UpdateInspectionStatus(string isarTaskId, IsarTaskStatus isarTaskStatus); public Task ReadByIsarTaskId(string id, bool readOnly = true); public Task AddFinding(InspectionFindingQuery inspectionFindingsQuery, string isarTaskId); + public Task GetInspectionStorageInfo(string inspectionId); } @@ -21,8 +26,16 @@ public interface IInspectionService "CA1309:Use ordinal StringComparison", Justification = "EF Core refrains from translating string comparison overloads to SQL" )] - public class InspectionService(FlotillaDbContext context, ILogger logger, IAccessRoleService accessRoleService) : IInspectionService + public class InspectionService(FlotillaDbContext context, ILogger logger, IDownstreamApi idaApi, IAccessRoleService accessRoleService, + IBlobService blobService) : IInspectionService { + public const string ServiceName = "IDA"; + + public async Task FetchInpectionImage(string inpectionName, string installationCode, string storageAccount) + { + return await blobService.DownloadBlob(inpectionName, installationCode, storageAccount); + } + public async Task UpdateInspectionStatus(string isarTaskId, IsarTaskStatus isarTaskStatus) { var inspection = await ReadByIsarTaskId(isarTaskId, readOnly: false); @@ -96,5 +109,44 @@ private IQueryable GetInspections(bool readOnly = true) inspection = await Update(inspection); return inspection; } + + public async Task GetInspectionStorageInfo(string inspectionId) + { + string relativePath = $"InspectionData/{inspectionId}/inspection-data-storage-location"; + + var response = await idaApi.CallApiForUserAsync( + ServiceName, + options => + { + options.HttpMethod = HttpMethod.Get.Method; + options.RelativePath = relativePath; + } + ); + + + if (response.StatusCode == HttpStatusCode.Accepted) + { + logger.LogInformation("Inspection data storage location for inspection with Id {inspectionId} is not yet available", inspectionId); + return null; + } + + if (response.StatusCode == HttpStatusCode.InternalServerError) + { + logger.LogError("Inetrnal server error when trying to get inspection data for inspection with Id {inspectionId}", inspectionId); + return null; + } + + if (response.StatusCode == HttpStatusCode.NotFound) + { + logger.LogError("Could not find inspection data for inspection with Id {inspectionId}", inspectionId); + return null; + } + + var inspectionData = await response.Content.ReadFromJsonAsync< + IDAInspectionDataResponse + >() ?? throw new JsonException("Failed to deserialize inspection data from IDA."); + + return inspectionData; + } } } diff --git a/backend/api/Services/Models/IDAInspectionDataResponse.cs b/backend/api/Services/Models/IDAInspectionDataResponse.cs new file mode 100644 index 000000000..e33c21cb5 --- /dev/null +++ b/backend/api/Services/Models/IDAInspectionDataResponse.cs @@ -0,0 +1,17 @@ +using System.Text.Json.Serialization; + +namespace Api.Services.Models +{ + public class IDAInspectionDataResponse + { + [JsonPropertyName("storageAccount")] + public required string StorageAccount { get; set; } + + [JsonPropertyName("blobContainer")] + public required string BlobContainer { get; set; } + + [JsonPropertyName("blobName")] + public required string BlobName { get; set; } + + } +} diff --git a/backend/api/appsettings.Development.json b/backend/api/appsettings.Development.json index 09d690d06..5088861f1 100644 --- a/backend/api/appsettings.Development.json +++ b/backend/api/appsettings.Development.json @@ -9,6 +9,10 @@ "Isar": { "Scopes": ["fd384acd-5c1b-4c44-a1ac-d41d720ed0fe/.default"] }, + "IDA": { + "BaseUrl": "http://ida.robotics-analytics-dev.svc.cluster.local:8100/", + "Scopes": ["bd4b0a3e-af88-4b7c-aab2-ad4956f2f789/.default"] + }, "Maps": { "StorageAccount": "flotillamaps" }, diff --git a/backend/api/appsettings.Local.json b/backend/api/appsettings.Local.json index 5aea17f4d..7e6c43aa5 100644 --- a/backend/api/appsettings.Local.json +++ b/backend/api/appsettings.Local.json @@ -7,13 +7,15 @@ "VaultUri": "https://flotilladevkv.vault.azure.net/" }, "Isar": { - "Scopes": [ - "fd384acd-5c1b-4c44-a1ac-d41d720ed0fe/.default" - ] + "Scopes": ["fd384acd-5c1b-4c44-a1ac-d41d720ed0fe/.default"] }, "Maps": { "StorageAccount": "flotillamaps" }, + "IDA": { + "BaseUrl": "https://localhost:8100/", + "Scopes": ["bd4b0a3e-af88-4b7c-aab2-ad4956f2f789/.default"] + }, "AllowedHosts": "*", "AllowedOrigins": [ "https://*.equinor.com/", diff --git a/backend/api/appsettings.Production.json b/backend/api/appsettings.Production.json index 10cf8db36..8513f6808 100644 --- a/backend/api/appsettings.Production.json +++ b/backend/api/appsettings.Production.json @@ -9,6 +9,10 @@ "Isar": { "Scopes": ["e08edece-ba2d-4fe1-8cd1-ee7b05ba7155/.default"] }, + "IDA": { + "BaseUrl": "http://ida.robotics-analytics-prod.svc.cluster.local:8100/", + "Scopes": ["16df0336-e42b-45c6-a380-8f6fe66e1fa3/.default"] + }, "Maps": { "StorageAccount": "flotillamaps" }, diff --git a/backend/api/appsettings.Staging.json b/backend/api/appsettings.Staging.json index 32543700f..cb10ac712 100644 --- a/backend/api/appsettings.Staging.json +++ b/backend/api/appsettings.Staging.json @@ -9,6 +9,10 @@ "Isar": { "Scopes": ["9cd787ea-8ce2-4d18-8bc8-279e7a8e6289/.default"] }, + "IDA": { + "BaseUrl": "http://ida.robotics-analytics-staging.svc.cluster.local:8100/", + "Scopes": ["6f40ba9b-2029-400e-85e9-f1922cbf12c1/.default"] + }, "Maps": { "StorageAccount": "flotillamaps" }, diff --git a/backend/api/appsettings.json b/backend/api/appsettings.json index e26994f26..9750a8bb6 100644 --- a/backend/api/appsettings.json +++ b/backend/api/appsettings.json @@ -5,15 +5,11 @@ }, "Echo": { "BaseUrl": "https://echohubapi.equinor.com/api", - "Scopes": [ - "bf0b2569-e09c-42f0-8095-5a52a873eb7b/.default" - ] + "Scopes": ["bf0b2569-e09c-42f0-8095-5a52a873eb7b/.default"] }, "Stid": { "BaseUrl": "https://stidapi.equinor.com/", - "Scopes": [ - "1734406c-3449-4192-a50d-7c3a63d3f57d/.default" - ] + "Scopes": ["1734406c-3449-4192-a50d-7c3a63d3f57d/.default"] }, "IsarConnectionTimeout": 10, "Logging": { @@ -29,9 +25,7 @@ } }, "AllowedHosts": "*", - "AllowedOrigins": [ - "https://*.equinor.com/" - ], + "AllowedOrigins": ["https://*.equinor.com/"], "Database": { "ConnectionString": "", "InitializeInMemDb": false diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 170779258..32baea97f 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -15,6 +15,7 @@ import { config } from 'config' import { MissionDefinitionsProvider } from 'components/Contexts/MissionDefinitionsContext' import { MediaStreamProvider } from 'components/Contexts/MediaStreamContext' import { DockProvider } from 'components/Contexts/DockContext' +import { InspectionsProvider } from 'components/Contexts/InpectionsContext' const appInsights = new ApplicationInsights({ config: { @@ -32,32 +33,34 @@ const App = () => ( - - - - - - - - -
- -
-
- - - - - - - -
-
-
-
-
-
-
+ + + + + + + + + +
+ +
+
+ + + + + + + +
+
+
+
+
+
+
+
diff --git a/frontend/src/api/ApiCaller.tsx b/frontend/src/api/ApiCaller.tsx index cac0e2be9..57565d545 100644 --- a/frontend/src/api/ApiCaller.tsx +++ b/frontend/src/api/ApiCaller.tsx @@ -430,4 +430,12 @@ export class BackendAPICaller { }) return result.content } + + static async getInspection(installationCode: string, taskId: string): Promise { + const path: string = 'inspection/' + installationCode + '/' + taskId + '/taskId' + + return BackendAPICaller.GET(path, 'image/png') + .then((response) => response.content) + .catch(BackendAPICaller.handleError('GET', path)) + } } diff --git a/frontend/src/components/Contexts/InpectionsContext.tsx b/frontend/src/components/Contexts/InpectionsContext.tsx new file mode 100644 index 000000000..e8019fb5c --- /dev/null +++ b/frontend/src/components/Contexts/InpectionsContext.tsx @@ -0,0 +1,102 @@ +import { createContext, FC, useContext, useState, useEffect, useRef } from 'react' +import { BackendAPICaller } from 'api/ApiCaller' +import { Task } from 'models/Task' +import { useInstallationContext } from './InstallationContext' + +interface IInspectionsContext { + selectedInspectionTask: Task | undefined + selectedInspectionTasks: Task[] + switchSelectedInspectionTask: (selectedInspectionTask: Task | undefined) => void + switchSelectedInspectionTasks: (selectedInspectionTask: Task[]) => void + mappingInspectionTasksObjectURL: { [taskIsarId: string]: string } +} + +interface Props { + children: React.ReactNode +} + +const defaultInspectionsContext = { + selectedInspectionTask: undefined, + selectedInspectionTasks: [], + switchSelectedInspectionTask: (selectedInspectionTask: Task | undefined) => undefined, + switchSelectedInspectionTasks: (selectedInspectionTasks: Task[]) => [], + mappingInspectionTasksObjectURL: {}, +} + +const InspectionsContext = createContext(defaultInspectionsContext) + +export const InspectionsProvider: FC = ({ children }) => { + const { installationCode } = useInstallationContext() + + const [selectedInspectionTask, setSelectedInspectionTask] = useState() + const [selectedInspectionTasks, setSelectedInspectionTasks] = useState([]) + const [selectedInspectionTasksToFetch, setSelectedInspectionTasksToFetch] = useState([]) + + const [mappingInspectionTasksObjectURL, setMappingInspectionTasksObjectURL] = useState<{ + [taskId: string]: string + }>({}) + + const [triggerFetch, setTriggerFetch] = useState(false) + const [startTimer, setStartTimer] = useState(false) + const imageObjectURL = useRef('') + + useEffect(() => { + const timeoutId = setTimeout(() => { + if (selectedInspectionTasksToFetch.length > 0) + setTriggerFetch((oldSetTriggerToFetch) => !oldSetTriggerToFetch) + }, 10000) + return () => clearTimeout(timeoutId) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [startTimer]) + + useEffect(() => { + Object.values(selectedInspectionTasksToFetch).forEach((task, index) => { + if (task.isarTaskId) { + BackendAPICaller.getInspection(installationCode, task.isarTaskId!) + .then((imageBlob) => { + imageObjectURL.current = URL.createObjectURL(imageBlob) + }) + .then(() => { + setMappingInspectionTasksObjectURL((oldMappingInspectionTasksObjectURL) => { + return { ...oldMappingInspectionTasksObjectURL, [task.isarTaskId!]: imageObjectURL.current } + }) + setSelectedInspectionTasksToFetch((oldSelectedInspectionTasksToFetch) => { + let newInspectionTaksToFetch = { ...oldSelectedInspectionTasksToFetch } + delete newInspectionTaksToFetch[index] + return newInspectionTaksToFetch + }) + }) + .catch(() => { + setStartTimer((oldValue) => !oldValue) + }) + } + }) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [installationCode, selectedInspectionTasksToFetch, triggerFetch]) + + const switchSelectedInspectionTask = (selectedName: Task | undefined) => { + setSelectedInspectionTask(selectedName) + } + + const switchSelectedInspectionTasks = (selectedName: Task[]) => { + setMappingInspectionTasksObjectURL({}) + setSelectedInspectionTasks(selectedName) + setSelectedInspectionTasksToFetch(selectedName) + } + + return ( + + {children} + + ) +} + +export const useInspectionsContext = () => useContext(InspectionsContext) diff --git a/frontend/src/components/Pages/InspectionReportPage.tsx/InspectionStyles.tsx b/frontend/src/components/Pages/InspectionReportPage.tsx/InspectionStyles.tsx new file mode 100644 index 000000000..8423bd358 --- /dev/null +++ b/frontend/src/components/Pages/InspectionReportPage.tsx/InspectionStyles.tsx @@ -0,0 +1,125 @@ +import { Button, Card, Dialog } from '@equinor/eds-core-react' +import { tokens } from '@equinor/eds-tokens' +import { styled } from 'styled-components' + +export const StyledInspection = styled.canvas` + flex: 1 0 0; + align-self: stretch; + width: 80vh; + + @media (max-width: 600px) { + width: 95vw; + } +` + +export const StyledInspectionImage = styled.canvas` + flex: 1 0 0; + align-self: center; + max-width: 100%; +` + +export const StyledDialog = styled(Dialog)` + display: flex; + width: 100%; + max-height: 80vh; + + @media (max-width: 600px) { + display: none; + } +` +export const StyledCloseButton = styled(Button)` + width: 24px; + height: 24px; +` +export const StyledDialogContent = styled(Dialog.Content)` + display: flex; + flex-direction: column; + gap: 10px; +` +export const StyledDialogHeader = styled.div` + display: flex; + padding: 16px; + justify-content: space-between; + align-items: center; + align-self: stretch; + border-bottom: 1px solid ${tokens.colors.ui.background__medium.hex}; + height: 24px; +` + +export const StyledBottomContent = styled.div` + display: flex; + padding: 16px; + justify-content: space-between; + align-items: center; + align-self: stretch; +` + +export const StyledInfoContent = styled.div` + display: flex; + flex-direction: column; + align-items: flex-start; +` + +export const StyledSection = styled.div` + display: flex; + padding: 24px; + min-width: 240px; + flex-direction: column; + align-items: flex-start; + gap: 8px; + border-radius: 6px; + border: 1.194px solid ${tokens.colors.ui.background__medium.hex}; + background: ${tokens.colors.ui.background__default.hex}; + overflow-y: scroll; +` + +export const StyledImagesSection = styled.div` + display: flex; + align-items: center; + gap: 16px; +` + +export const StyledImageCard = styled(Card)` + display: flex; + width: 150px; + align-self: stretch; + padding: 4px; + flex-direction: column; + align-items: flex-start; + gap: 2px; + border-radius: 2px; + border: 1px solid ${tokens.colors.ui.background__medium.hex}; + background: ${tokens.colors.ui.background__default.hex}; + box-shadow: + 0px 2.389px 4.778px 0px ${tokens.colors.ui.background__light.hex}, + 0px 3.583px 4.778px 0px ${tokens.colors.ui.background__light.hex}; + cursor: pointer; +` + +export const StyledInspectionData = styled.div` + display: flex; + flex-direction: column; + justify-content: center; + align-items: flex-start; + gap: 8px; +` +export const StyledInspectionContent = styled.div` + display: flex; + flex-direction: column; + align-items: flex-start; +` +export const StyledDialogInspectionView = styled.div` + display: flex; + flex-direction: row; + gap: 16px; +` + +export const StyledInspectionCards = styled.div` + display: flex; + justify-content: start; + align-items: flex-start; + align-content: flex-start; + gap: 8px; + align-self: stretch; + flex-wrap: wrap; +` diff --git a/frontend/src/components/Pages/InspectionReportPage.tsx/InspectionView.tsx b/frontend/src/components/Pages/InspectionReportPage.tsx/InspectionView.tsx new file mode 100644 index 000000000..2c5ed477d --- /dev/null +++ b/frontend/src/components/Pages/InspectionReportPage.tsx/InspectionView.tsx @@ -0,0 +1,229 @@ +import { Icon, Typography } from '@equinor/eds-core-react' +import { useCallback, useEffect, useRef, useState } from 'react' +import { Task, TaskStatus } from 'models/Task' +import { useInstallationContext } from 'components/Contexts/InstallationContext' +import { Icons } from 'utils/icons' +import { useLanguageContext } from 'components/Contexts/LanguageContext' +import { formatDateTime } from 'utils/StringFormatting' +import { useInspectionsContext } from 'components/Contexts/InpectionsContext' +import { + StyledBottomContent, + StyledCloseButton, + StyledDialog, + StyledDialogContent, + StyledDialogHeader, + StyledDialogInspectionView, + StyledImageCard, + StyledImagesSection, + StyledInfoContent, + StyledInspection, + StyledInspectionCards, + StyledInspectionContent, + StyledInspectionData, + StyledInspectionImage, + StyledSection, +} from './InspectionStyles' + +interface InspectionDialogViewProps { + task: Task + tasks: Task[] +} + +const getMeta = async (url: string) => { + const image = new Image() + image.src = url + await image.decode() + return image +} + +export const InspectionDialogView = ({ task, tasks }: InspectionDialogViewProps) => { + const { TranslateText } = useLanguageContext() + const { installationName } = useInstallationContext() + const [inspectionImage, setInspectionImage] = useState(document.createElement('img')) + const imageObjectURL = useRef('') + + const { switchSelectedInspectionTask, mappingInspectionTasksObjectURL } = useInspectionsContext() + + const updateImage = useCallback(() => { + if (task.isarTaskId && mappingInspectionTasksObjectURL[task.isarTaskId]) { + imageObjectURL.current = mappingInspectionTasksObjectURL[task.isarTaskId] + + getMeta(imageObjectURL.current).then((img) => { + const inspectionCanvas = document.getElementById('inspectionCanvas') as HTMLCanvasElement + if (inspectionCanvas) { + inspectionCanvas.width = img.width + inspectionCanvas.height = img.height + let context = inspectionCanvas.getContext('2d') + if (context) { + context.drawImage(img, 0, 0) + } + } + setInspectionImage(img) + }) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [mappingInspectionTasksObjectURL]) + + useEffect(() => { + updateImage() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [mappingInspectionTasksObjectURL, inspectionImage]) + + return ( + <> + {imageObjectURL.current !== '' && ( + + + + + {TranslateText('Inspection report')} + + switchSelectedInspectionTask(undefined)}> + + + + +
+ + + + {TranslateText('Installation') + ':'} + {installationName} + + + {TranslateText('Tag') + ':'} + {task.tagId} + + {task.description && ( + + + {TranslateText('Description') + ':'} + + {task.description} + + )} + {task.endTime && ( + + + {TranslateText('Timestamp') + ':'} + + + {formatDateTime(task.endTime, 'dd.MM.yy - HH:mm')} + + + )} + +
+ +
+
+
+ )} + + ) +} + +interface InspectionsViewSectionProps { + tasks: Task[] + dialogView?: boolean | undefined +} + +export const InspectionsViewSection = ({ tasks, dialogView }: InspectionsViewSectionProps) => { + const { TranslateText } = useLanguageContext() + const { switchSelectedInspectionTask } = useInspectionsContext() + + return ( + <> + + {!dialogView && {TranslateText('Last completed inspection')}} + + + {Object.keys(tasks).length > 0 && + tasks.map( + (task) => + task.status === TaskStatus.Successful && ( + switchSelectedInspectionTask(task)} + > + + + {task.tagId && ( + + + {TranslateText('Tag') + ':'} + + {task.tagId} + + )} + {task.endTime && ( + + + {TranslateText('Timestamp') + ':'} + + + {formatDateTime(task.endTime!, 'dd.MM.yy - HH:mm')} + + + )} + + + ) + )} + + + + + ) +} + +interface GetInspectionImageProps { + task: Task + tasks: Task[] +} + +const GetInspectionImage = ({ task, tasks }: GetInspectionImageProps) => { + const imageObjectURL = useRef('') + const [inspectionImage, setInspectionImage] = useState(document.createElement('img')) + + const { switchSelectedInspectionTasks, mappingInspectionTasksObjectURL } = useInspectionsContext() + + useEffect(() => { + switchSelectedInspectionTasks(tasks) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [tasks]) + + const updateImage = useCallback(() => { + if (task.isarTaskId && mappingInspectionTasksObjectURL[task.isarTaskId]) { + imageObjectURL.current = mappingInspectionTasksObjectURL[task.isarTaskId] + + getMeta(imageObjectURL.current).then((img) => { + const inspectionCanvas = document.getElementById(task.isarTaskId!) as HTMLCanvasElement + if (inspectionCanvas) { + inspectionCanvas.width = img.width + inspectionCanvas.height = img.height + let context = inspectionCanvas.getContext('2d') + if (context) { + context.drawImage(img, 0, 0) + } + } + setInspectionImage(img) + }) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [mappingInspectionTasksObjectURL]) + + useEffect(() => { + updateImage() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [mappingInspectionTasksObjectURL, inspectionImage]) + + return +} diff --git a/frontend/src/components/Pages/MissionPage/MissionPage.tsx b/frontend/src/components/Pages/MissionPage/MissionPage.tsx index 660ac1866..89cfb67bb 100644 --- a/frontend/src/components/Pages/MissionPage/MissionPage.tsx +++ b/frontend/src/components/Pages/MissionPage/MissionPage.tsx @@ -17,6 +17,8 @@ import { AlertCategory } from 'components/Alerts/AlertsBanner' import { useMediaStreamContext } from 'components/Contexts/MediaStreamContext' import { tokens } from '@equinor/eds-tokens' import { StyledPage } from 'components/Styles/StyledComponents' +import { InspectionDialogView, InspectionsViewSection } from '../InspectionReportPage.tsx/InspectionView' +import { useInspectionsContext } from 'components/Contexts/InpectionsContext' const StyledMissionPage = styled(StyledPage)` background-color: ${tokens.colors.ui.background__light.hex}; @@ -42,6 +44,7 @@ export const MissionPage = () => { const [selectedMission, setSelectedMission] = useState() const { registerEvent, connectionReady } = useSignalRContext() const { mediaStreams, addMediaStreamConfigIfItDoesNotExist } = useMediaStreamContext() + const { selectedInspectionTask } = useInspectionsContext() useEffect(() => { if (selectedMission && !Object.keys(mediaStreams).includes(selectedMission?.robot.id)) @@ -109,6 +112,10 @@ export const MissionPage = () => { )} + {selectedInspectionTask && selectedInspectionTask.isarTaskId && ( + + )} + )} diff --git a/frontend/src/components/Pages/MissionPage/TaskOverview/TaskTable.tsx b/frontend/src/components/Pages/MissionPage/TaskOverview/TaskTable.tsx index 50f223b41..f2106a3c4 100644 --- a/frontend/src/components/Pages/MissionPage/TaskOverview/TaskTable.tsx +++ b/frontend/src/components/Pages/MissionPage/TaskOverview/TaskTable.tsx @@ -1,4 +1,4 @@ -import { Chip, Table, Typography } from '@equinor/eds-core-react' +import { Button, Chip, Table, Typography } from '@equinor/eds-core-react' import styled from 'styled-components' import { TaskStatusDisplay } from './TaskStatusDisplay' import { useLanguageContext } from 'components/Contexts/LanguageContext' @@ -6,6 +6,8 @@ import { Task, TaskStatus } from 'models/Task' import { tokens } from '@equinor/eds-tokens' import { getColorsFromTaskStatus } from 'utils/MarkerStyles' import { StyledTableBody, StyledTableCaptionGray, StyledTableCell } from 'components/Styles/StyledComponents' +import { InspectionType } from 'models/Inspection' +import { useInspectionsContext } from 'components/Contexts/InpectionsContext' const StyledTable = styled(Table)` display: block; @@ -25,29 +27,39 @@ const StyledTypography = styled(Typography)` padding-bottom: 10px; ` +interface TaskTableProps { + tasks: Task[] | undefined +} -export const TaskTable = ({ tasks }: { tasks: Task[] | undefined }) => { +export const TaskTable = ({ tasks }: TaskTableProps) => { const { TranslateText } = useLanguageContext() + return ( - - - {TranslateText('Tasks')} - - - - # - {TranslateText('Tag-ID')} - {TranslateText('Description')} - {TranslateText('Inspection Types')} - {TranslateText('Status')} - - - {tasks && } - + <> + + + {TranslateText('Tasks')} + + + + # + {TranslateText('Tag-ID')} + {TranslateText('Description')} + {TranslateText('Inspection Types')} + {TranslateText('Status')} + + + {tasks && } + + ) } -const TaskTableRows = ({ tasks }: { tasks: Task[] }) => { +interface TaskTableRowsProps { + tasks: Task[] +} + +const TaskTableRows = ({ tasks }: TaskTableRowsProps) => { const rows = tasks.map((task) => { // Workaround for current bug in echo const order: number = task.taskOrder + 1 @@ -104,15 +116,35 @@ const DescriptionDisplay = ({ task }: { task: Task }) => { return {capitalizeFirstLetter(task.description)} } -const InspectionTypesDisplay = ({ task }: { task: Task }) => { +interface InspectionTypesDisplayProps { + task: Task +} + +const InspectionTypesDisplay = ({ task }: InspectionTypesDisplayProps) => { const { TranslateText } = useLanguageContext() + const { switchSelectedInspectionTask } = useInspectionsContext() + return ( <> {task.inspection && - (task.inspection.inspectionUrl ? ( - - {TranslateText(task.inspection.inspectionType as string)} - + (task.inspection.inspectionType === InspectionType.Image ? ( + task.status === TaskStatus.Successful ? ( + + ) : ( + + ) ) : ( {TranslateText(task.inspection.inspectionType as string)} diff --git a/frontend/src/language/en.json b/frontend/src/language/en.json index a193ec619..3c0db4385 100644 --- a/frontend/src/language/en.json +++ b/frontend/src/language/en.json @@ -275,5 +275,9 @@ "Not available": "Not available", "Stop button pressed with no tasktype warning text": "A stop command was recieved with no ongoing tasks.", "No mission running": "No Flotilla mission is running", - "Stop button pressed with no mission confirmation text": "Do you want to send the stop command to the robot regardless?" + "Stop button pressed with no mission confirmation text": "Do you want to send the stop command to the robot regardless?", + "Tag": "Tag", + "Timestamp": "Timestamp", + "Last completed inspection": "Last completed inspection", + "Inspection report": "Inspection report" } diff --git a/frontend/src/language/no.json b/frontend/src/language/no.json index 6f93a155e..6acf46fba 100644 --- a/frontend/src/language/no.json +++ b/frontend/src/language/no.json @@ -275,5 +275,9 @@ "Not available": "Ikke tilgjengelig", "No mission running": "Ingen pågående Flotilla oppdrag", "Stop button pressed with no tasktype warning text": "Et stopp signal ble mottat uten noen pågående oppdrag.", - "Stop button pressed with no mission confirmation text": "Ønsker du å sende stopp signalet til roboten likevel?" + "Stop button pressed with no mission confirmation text": "Ønsker du å sende stopp signalet til roboten likevel?", + "Tag": "Tag", + "Timestamp": "Tidspunkt", + "Last completed inspection": "Siste gjennomførte inspeksjon", + "Inspection report": "Inspeksjonsrapport" }