Skip to content

Commit 2ed0811

Browse files
committed
Add cloe data view
1 parent 6efd6d9 commit 2ed0811

8 files changed

Lines changed: 301 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: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
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+
export const CloeDataViewPage = () => <CloeDataViewComponent />
39+
40+
const DataTable = ({ tasks }: { tasks: Task[] }) => {
41+
const { TranslateText } = useLanguageContext()
42+
43+
return (
44+
<StyledTable>
45+
<Table.Head>
46+
<Table.Row>
47+
<Table.Cell>#</Table.Cell>
48+
<Table.Cell>{TranslateText('Tag-ID')}</Table.Cell>
49+
<Table.Cell>{TranslateText('Description')}</Table.Cell>
50+
<Table.Cell>{TranslateText('Latest Value')}</Table.Cell>
51+
<Table.Cell>{TranslateText('Timestamp')}</Table.Cell>
52+
</Table.Row>
53+
</Table.Head>
54+
<Table.Body>
55+
{tasks &&
56+
tasks
57+
.filter((task) => task.description === 'Spherical glass')
58+
.map((task, index) => (
59+
<Table.Row key={task.id}>
60+
<Table.Cell>
61+
<Chip>
62+
<Typography variant="body_short_bold">{index + 1}</Typography>
63+
</Chip>
64+
</Table.Cell>
65+
<Table.Cell>
66+
<TagIdDisplay task={task} />
67+
</Table.Cell>
68+
<Table.Cell>
69+
<DescriptionDisplay task={task} />
70+
</Table.Cell>
71+
{task.inspection.analysisResult?.value ? (
72+
<Table.Cell>
73+
<Typography>{task.inspection.analysisResult?.value}</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+
const CloeDataViewComponent = () => {
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 [lastChangedMission, setLastChangedMission] = useState<Mission | undefined>(undefined)
99+
const backendApi = useBackendApi()
100+
101+
const updateFilteredMissions = useCallback(() => {
102+
backendApi
103+
.getMissionRuns({
104+
installationCode: installation.installationCode,
105+
orderBy: 'EndTime desc, Name',
106+
})
107+
.then((paginatedMissions) => {
108+
const missions = paginatedMissions.content.filter((mission) =>
109+
mission.name?.toLowerCase().startsWith('avlesning')
110+
)
111+
setFilteredMissions(missions)
112+
})
113+
.catch(() => {})
114+
}, [installation.installationCode])
115+
116+
useEffect(() => {
117+
updateFilteredMissions()
118+
}, [updateFilteredMissions])
119+
120+
useEffect(() => {
121+
if (filteredMissions) setLastFilteredMission(filteredMissions[0])
122+
}, [filteredMissions])
123+
124+
useEffect(() => {
125+
if (
126+
lastChangedMission &&
127+
lastChangedMission.installationCode === installation.installationCode &&
128+
![MissionStatus.Pending, MissionStatus.Queued, MissionStatus.Ongoing].includes(lastChangedMission.status)
129+
) {
130+
updateFilteredMissions()
131+
}
132+
}, [lastChangedMission])
133+
134+
useEffect(() => {
135+
if (connectionReady) {
136+
registerEvent(SignalREventLabels.missionRunCreated, (username: string, message: string) => {
137+
setLastChangedMission(JSON.parse(message))
138+
})
139+
registerEvent(SignalREventLabels.missionRunUpdated, (username: string, message: string) => {
140+
setLastChangedMission(JSON.parse(message))
141+
})
142+
registerEvent(SignalREventLabels.missionRunDeleted, (username: string, message: string) => {
143+
setLastChangedMission(JSON.parse(message))
144+
})
145+
}
146+
}, [registerEvent, connectionReady])
147+
148+
const plantCode = lastFilteredMission?.inspectionArea.plantCode
149+
? lastFilteredMission.inspectionArea.plantCode
150+
: undefined
151+
152+
const missionForMap = lastFilteredMission
153+
? {
154+
...lastFilteredMission,
155+
tasks: lastFilteredMission.tasks
156+
.filter((task) => task.description === 'Spherical glass')
157+
.map((task, index) => ({ ...task, taskOrder: index })),
158+
}
159+
: undefined
160+
161+
const missionsForPlot = filteredMissions
162+
.filter((mission) => {
163+
const date = mission.endTime ?? mission.startTime ?? mission.creationTime
164+
if (!date) return false
165+
const sevenDaysAgo = Date.now() - 7 * 24 * 60 * 60 * 1000
166+
return new Date(date).getTime() >= sevenDaysAgo
167+
})
168+
.map((mission) => ({
169+
...mission,
170+
tasks: mission.tasks
171+
.filter((task) => task.description === 'Spherical glass')
172+
.map((task, index) => ({ ...task, taskOrder: index })),
173+
}))
174+
175+
return (
176+
<StyledPage>
177+
<StyledContent>
178+
<Typography variant="h2">Data View for Constant Level Oilers</Typography>
179+
<StyledContainer>
180+
{lastFilteredMission && <DataTable tasks={lastFilteredMission.tasks} />}
181+
{plantCode && missionForMap && (
182+
<PlantMap plantCode={plantCode} floorId="0" mission={missionForMap} />
183+
)}
184+
</StyledContainer>
185+
<StyledPlot>
186+
<Typography variant="h4">
187+
{TranslateText('Measured oil level throughout the last 7 days')}
188+
</Typography>
189+
{missionsForPlot.length > 0 && <LinePlot missions={missionsForPlot} />}
190+
</StyledPlot>
191+
</StyledContent>
192+
</StyledPage>
193+
)
194+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
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+
console.log(chartData)
64+
return <Line options={options} data={chartData} />
65+
}

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)