Skip to content

Commit 91d876a

Browse files
committed
Add cloe data view
1 parent 6efd6d9 commit 91d876a

8 files changed

Lines changed: 287 additions & 4 deletions

File tree

frontend/package-lock.json

Lines changed: 31 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"prettier": "^3.8.1",
2525
"react": "^19.2.4",
2626
"react-big-calendar": "^1.19.4",
27+
"react-chartjs-2": "^5.3.1",
2728
"react-dom": "^19.2.4",
2829
"react-leaflet": "^5.0.0",
2930
"react-modal": "^3.16.3",

frontend/src/language/en.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -376,5 +376,7 @@
376376
"Task Success Rate": "Task Success Rate",
377377
"Loading installation data": "Loading installation data",
378378
"RechargingWithMission": "Recharging",
379-
"GoingToRechargingWithMission": "Going home to recharge (mission paused)"
379+
"GoingToRechargingWithMission": "Going home to recharge (mission paused)",
380+
"Latest Value": "Latest Value",
381+
"Measured oil level throughout the last 7 days": "Measured oil level throughout the last 7 days"
380382
}

frontend/src/language/no.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -376,5 +376,7 @@
376376
"Task Success Rate": "Suksessrate for oppgaver",
377377
"Loading installation data": "Laster installasjonsdata",
378378
"RechargingWithMission": "Lader",
379-
"GoingToRechargingWithMission": "Returnerer for å lade (oppdrag pauset)"
379+
"GoingToRechargingWithMission": "Returnerer for å lade (oppdrag pauset)",
380+
"Latest Value": "Siste verdi",
381+
"Measured oil level throughout the last 7 days": "Målt oljenivå i løpet av de siste 7 dagene"
380382
}
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
import { Chip, Table, Typography } from '@equinor/eds-core-react'
2+
import { Mission, MissionStatus } from 'models/Mission'
3+
import { useCallback, useContext, useEffect, useState } from 'react'
4+
import styled from 'styled-components'
5+
import { useLanguageContext } from 'components/Contexts/LanguageContext'
6+
import { StyledPage } from 'components/Styles/StyledComponents'
7+
import { SignalREventLabels, useSignalRContext } from 'components/Contexts/SignalRContext'
8+
import { useBackendApi } from 'api/UseBackendApi'
9+
import { InstallationContext } from 'components/Contexts/InstallationContext'
10+
import { PlantMap } from './MissionPage/MapPosition/PointillaMapView'
11+
import { Task } from 'models/Task'
12+
import { DescriptionDisplay, TagIdDisplay } from './MissionPage/TaskOverview/TaskTable'
13+
import { formatDateTime } from 'utils/StringFormatting'
14+
import { LinePlot } from './CloePage/LinePlot'
15+
16+
const StyledTable = styled(Table)`
17+
max-width: 800px;
18+
`
19+
20+
const StyledPlot = styled.div`
21+
height: 650px;
22+
`
23+
24+
const StyledContainer = styled.div`
25+
display: flex;
26+
flex-direction: row;
27+
gap: 100px;
28+
align-items: start;
29+
`
30+
31+
const StyledContent = styled.div`
32+
display: flex;
33+
flex-direction: column;
34+
gap: 50px;
35+
padding-top: 50px;
36+
`
37+
38+
const DataTable = ({ tasks }: { tasks: Task[] }) => {
39+
const { TranslateText } = useLanguageContext()
40+
41+
return (
42+
<StyledTable>
43+
<Table.Head>
44+
<Table.Row>
45+
<Table.Cell>#</Table.Cell>
46+
<Table.Cell>{TranslateText('Tag-ID')}</Table.Cell>
47+
<Table.Cell>{TranslateText('Description')}</Table.Cell>
48+
<Table.Cell>{TranslateText('Latest Value')}</Table.Cell>
49+
<Table.Cell>{TranslateText('Timestamp')}</Table.Cell>
50+
</Table.Row>
51+
</Table.Head>
52+
<Table.Body>
53+
{tasks &&
54+
tasks
55+
.filter((task) => task.description === 'Spherical glass')
56+
.map((task, index) => (
57+
<Table.Row key={task.id}>
58+
<Table.Cell>
59+
<Chip>
60+
<Typography variant="body_short_bold">{index + 1}</Typography>
61+
</Chip>
62+
</Table.Cell>
63+
<Table.Cell>
64+
<TagIdDisplay task={task} />
65+
</Table.Cell>
66+
<Table.Cell>
67+
<DescriptionDisplay task={task} />
68+
</Table.Cell>
69+
{task.inspection.analysisResult?.value ? (
70+
<Table.Cell>
71+
<Typography>
72+
{Math.round(parseFloat(task.inspection.analysisResult?.value)) + '%'}
73+
</Typography>
74+
</Table.Cell>
75+
) : (
76+
<Table.Cell>
77+
<Typography>Analysis result not available</Typography>
78+
</Table.Cell>
79+
)}
80+
{task.endTime && (
81+
<Table.Cell>
82+
<Typography>{formatDateTime(task.endTime, 'dd.MM.yy - HH:mm')}</Typography>
83+
</Table.Cell>
84+
)}
85+
</Table.Row>
86+
))}
87+
</Table.Body>
88+
</StyledTable>
89+
)
90+
}
91+
92+
export const CloeDataViewPage = () => {
93+
const { TranslateText } = useLanguageContext()
94+
const { installation } = useContext(InstallationContext)
95+
const { registerEvent, connectionReady } = useSignalRContext()
96+
const [filteredMissions, setFilteredMissions] = useState<Mission[]>([])
97+
const [lastFilteredMission, setLastFilteredMission] = useState<Mission>()
98+
const backendApi = useBackendApi()
99+
100+
const updateFilteredMissions = useCallback(() => {
101+
backendApi
102+
.getMissionRuns({
103+
installationCode: installation.installationCode,
104+
orderBy: 'EndTime desc, Name',
105+
})
106+
.then((paginatedMissions) => {
107+
const missions = paginatedMissions.content
108+
.filter((mission) => mission.name?.toLowerCase().startsWith('avlesning'))
109+
.filter((mission) =>
110+
[MissionStatus.Successful, MissionStatus.PartiallySuccessful].includes(mission.status)
111+
)
112+
setFilteredMissions(missions)
113+
})
114+
.catch(() => {})
115+
}, [installation.installationCode])
116+
117+
useEffect(() => {
118+
updateFilteredMissions()
119+
}, [updateFilteredMissions])
120+
121+
useEffect(() => {
122+
if (filteredMissions.length > 0) {
123+
setLastFilteredMission(filteredMissions[0])
124+
}
125+
}, [filteredMissions])
126+
127+
useEffect(() => {
128+
if (connectionReady) {
129+
registerEvent(SignalREventLabels.analysisResultReady, () => {
130+
updateFilteredMissions()
131+
})
132+
}
133+
}, [registerEvent, connectionReady])
134+
135+
const plantCode = lastFilteredMission?.inspectionArea.plantCode
136+
? lastFilteredMission.inspectionArea.plantCode
137+
: undefined
138+
139+
const missionForMap = lastFilteredMission
140+
? {
141+
...lastFilteredMission,
142+
tasks: lastFilteredMission.tasks
143+
.filter((task) => task.description === 'Spherical glass')
144+
.map((task, index) => ({ ...task, taskOrder: index })),
145+
}
146+
: undefined
147+
148+
const missionsForPlot = filteredMissions
149+
.filter((mission) => {
150+
const date = mission.endTime ?? mission.startTime ?? mission.creationTime
151+
if (!date) return false
152+
const sevenDaysAgo = Date.now() - 7 * 24 * 60 * 60 * 1000
153+
return new Date(date).getTime() >= sevenDaysAgo
154+
})
155+
.map((mission) => ({
156+
...mission,
157+
tasks: mission.tasks
158+
.filter((task) => task.description === 'Spherical glass')
159+
.map((task, index) => ({ ...task, taskOrder: index })),
160+
}))
161+
162+
return (
163+
<StyledPage>
164+
<StyledContent>
165+
<Typography variant="h2">Data View for Constant Level Oilers</Typography>
166+
<StyledContainer>
167+
{lastFilteredMission && <DataTable tasks={lastFilteredMission.tasks} />}
168+
{plantCode && missionForMap && (
169+
<PlantMap plantCode={plantCode} floorId="0" mission={missionForMap} />
170+
)}
171+
</StyledContainer>
172+
<StyledPlot>
173+
<Typography variant="h4">
174+
{TranslateText('Measured oil level throughout the last 7 days')}
175+
</Typography>
176+
{missionsForPlot.length > 0 && <LinePlot missions={missionsForPlot} />}
177+
</StyledPlot>
178+
</StyledContent>
179+
</StyledPage>
180+
)
181+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import {
2+
Chart as ChartJS,
3+
CategoryScale,
4+
LinearScale,
5+
PointElement,
6+
LineElement,
7+
Title,
8+
Tooltip,
9+
Legend,
10+
ChartData,
11+
ChartOptions,
12+
} from 'chart.js'
13+
import { Mission } from 'models/Mission'
14+
import { Line } from 'react-chartjs-2'
15+
import { formatDateTime } from 'utils/StringFormatting'
16+
17+
ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend)
18+
19+
const options: ChartOptions<'line'> = {
20+
responsive: true,
21+
spanGaps: true,
22+
23+
plugins: {
24+
legend: {
25+
position: 'right' as const,
26+
},
27+
title: {
28+
display: false,
29+
},
30+
},
31+
}
32+
33+
const PALETTE: [string, string][] = [
34+
['rgb(255, 18, 67)', 'rgba(255, 18, 67, 0.5)'],
35+
['rgb(125, 0, 35)', 'rgba(125, 0, 35, 0.5)'],
36+
['rgb(36, 55, 70)', 'rgba(36, 55, 70, 0.5)'],
37+
['rgb(0, 112, 121)', 'rgba(0, 112, 121, 0.5)'],
38+
]
39+
40+
export const LinePlot = ({ missions }: { missions: Mission[] }) => {
41+
const chronological = [...missions].reverse()
42+
43+
const labels = chronological.map((m) => {
44+
const date = m.endTime ?? m.startTime ?? m.creationTime
45+
return formatDateTime(new Date(date), 'dd.MM.yy - HH:mm')
46+
})
47+
48+
const tags = missions[0].tasks.map((task, i) => task.tagId ?? `Task ${i + 1}`)
49+
50+
const datasets = tags.map((tagId, i) => {
51+
const [borderColor, backgroundColor] = PALETTE[i % PALETTE.length]
52+
return {
53+
label: tagId,
54+
data: chronological.map((mission) => {
55+
const raw = mission.tasks[i]?.inspection?.analysisResult?.value
56+
return raw !== undefined && raw !== null ? Number(raw) : null
57+
}),
58+
borderColor,
59+
backgroundColor,
60+
}
61+
})
62+
const chartData: ChartData<'line'> = { labels, datasets }
63+
return <Line options={options} data={chartData} />
64+
}

frontend/src/pages/FlotillaSite.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { InfoPage } from './InfoPage'
55
import { MissionDefinitionPageRouter, MissionPageRouter, RobotPageRouter, SimpleMissionPageRouter } from './PageRouter'
66
import { PageNotFound } from './NotFoundPage'
77
import { DataViewPage } from './MissionHistory/DataViewPage'
8+
import { CloeDataViewPage } from './CloeDataViewPage'
89
import { MissionControlPage } from './MissionControlPage'
910
import { AreaOverviewPage } from './AreaOverviewPage'
1011
import { PredefinedMissionsPage } from './PredefinedMissionsPage'
@@ -50,6 +51,7 @@ export const FlotillaSite = () => {
5051
<Route path="robots" element={<RobotStatusPage />} />
5152
<Route path="statistics" element={<StatisticsPage />} />
5253
<Route path="data-view" element={<DataViewPage />} />
54+
<Route path="cloe-view" element={<CloeDataViewPage />} />
5355
<Route path="mission/:missionId" element={<MissionPageRouter />} />
5456
<Route path="mission-simple" element={<SimpleMissionPageRouter />} />
5557
<Route path="missiondefinition/:missionId" element={<MissionDefinitionPageRouter />} />

frontend/src/pages/MissionPage/TaskOverview/TaskTable.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ const TaskTableRows = ({ tasks, missionDefinitionPage }: TaskTableProps) => {
9494
return <>{rows}</>
9595
}
9696

97-
const TagIdDisplay = ({ task }: { task: Task }) => {
97+
export const TagIdDisplay = ({ task }: { task: Task }) => {
9898
if (!task.tagId) return <Typography key={task.id + 'tagId'}>{'N/A'}</Typography>
9999

100100
if (task.tagLink)
@@ -106,7 +106,7 @@ const TagIdDisplay = ({ task }: { task: Task }) => {
106106
else return <Typography key={task.id + 'tagId'}>{task.tagId!}</Typography>
107107
}
108108

109-
const DescriptionDisplay = ({ task }: { task: Task }) => {
109+
export const DescriptionDisplay = ({ task }: { task: Task }) => {
110110
if (!task.description) return <Typography key={task.id + 'descr'}>{'N/A'}</Typography>
111111
return <Typography key={task.id + 'descr'}>{task.description}</Typography>
112112
}

0 commit comments

Comments
 (0)