Skip to content

Commit 8c9f8c0

Browse files
committed
Add cloe data view
1 parent 984840d commit 8c9f8c0

8 files changed

Lines changed: 315 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
@@ -374,5 +374,7 @@
374374
"Successful Tasks": "Successful Tasks",
375375
"Failed Tasks": "Failed Tasks",
376376
"Task Success Rate": "Task Success Rate",
377-
"Loading installation data": "Loading installation data"
377+
"Loading installation data": "Loading installation data",
378+
"Latest Value": "Latest Value",
379+
"Measured oil level throughout the last 7 days": "Measured oil level throughout the last 7 days"
378380
}

frontend/src/language/no.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -374,5 +374,7 @@
374374
"Successful Tasks": "Vellykkede oppgaver",
375375
"Failed Tasks": "Mislykkede oppgaver",
376376
"Task Success Rate": "Suksessrate for oppgaver",
377-
"Loading installation data": "Laster installasjonsdata"
377+
"Loading installation data": "Laster installasjonsdata",
378+
"Latest Value": "Siste verdi",
379+
"Measured oil level throughout the last 7 days": "Målt oljenivå i løpet av de siste 7 dagene"
378380
}
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
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 = () => (
39+
<CloeDataViewComponent />
40+
)
41+
42+
export const DataTable = ({ tasks }: { tasks: Task[] }) => {
43+
const { TranslateText } = useLanguageContext()
44+
45+
return (
46+
<StyledTable>
47+
<Table.Head>
48+
<Table.Row>
49+
<Table.Cell>#</Table.Cell>
50+
<Table.Cell>{TranslateText('Tag-ID')}</Table.Cell>
51+
<Table.Cell>{TranslateText('Description')}</Table.Cell>
52+
<Table.Cell>{TranslateText('Latest Value')}</Table.Cell>
53+
<Table.Cell>{TranslateText('Timestamp')}</Table.Cell>
54+
</Table.Row>
55+
</Table.Head>
56+
<Table.Body>
57+
{tasks &&
58+
tasks.filter((task) => task.description === 'Spherical glass').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+
125+
useEffect(() => {
126+
if (
127+
lastChangedMission &&
128+
lastChangedMission.installationCode === installation.installationCode &&
129+
![MissionStatus.Pending, MissionStatus.Queued, MissionStatus.Ongoing].includes(lastChangedMission.status)
130+
) {
131+
updateFilteredMissions()
132+
}
133+
}, [lastChangedMission])
134+
135+
useEffect(() => {
136+
if (connectionReady) {
137+
registerEvent(SignalREventLabels.missionRunCreated, (username: string, message: string) => {
138+
setLastChangedMission(JSON.parse(message))
139+
})
140+
registerEvent(SignalREventLabels.missionRunUpdated, (username: string, message: string) => {
141+
setLastChangedMission(JSON.parse(message))
142+
})
143+
registerEvent(SignalREventLabels.missionRunDeleted, (username: string, message: string) => {
144+
setLastChangedMission(JSON.parse(message))
145+
})
146+
}
147+
}, [registerEvent, connectionReady])
148+
149+
150+
const plantCode = lastFilteredMission?.inspectionArea.plantCode
151+
? lastFilteredMission.inspectionArea.plantCode
152+
: undefined
153+
154+
const missionForMap = lastFilteredMission
155+
? {
156+
...lastFilteredMission,
157+
tasks: lastFilteredMission.tasks
158+
.filter((task) => task.description === 'Spherical glass')
159+
.map((task, index) => ({ ...task, taskOrder: index })),
160+
}
161+
: undefined
162+
163+
const missionsForPlot = filteredMissions
164+
.filter((mission) => {
165+
const date = mission.endTime ?? mission.startTime ?? mission.creationTime
166+
if (!date) return false
167+
const sevenDaysAgo = Date.now() - 7 * 24 * 60 * 60 * 1000
168+
return new Date(date).getTime() >= sevenDaysAgo
169+
})
170+
.map((mission) => ({
171+
...mission,
172+
tasks: mission.tasks
173+
.filter((task) => task.description === 'Spherical glass')
174+
.map((task, index) => ({ ...task, taskOrder: index })),
175+
}))
176+
177+
return (
178+
<StyledPage>
179+
<StyledContent>
180+
<Typography variant="h2">Data View for Constant Level Oilers</Typography>
181+
<StyledContainer>
182+
{lastFilteredMission && <DataTable tasks={lastFilteredMission.tasks} />}
183+
{plantCode && missionForMap && (
184+
<PlantMap plantCode={plantCode} floorId="0" mission={missionForMap} />
185+
)}
186+
</StyledContainer>
187+
<StyledPlot>
188+
<Typography variant="h4">
189+
{TranslateText("Measured oil level throughout the last 7 days")}
190+
</Typography>
191+
{missionsForPlot.length > 0 && <LinePlot missions={missionsForPlot} />}
192+
</StyledPlot>
193+
</StyledContent>
194+
</StyledPage>
195+
)
196+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
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(
18+
CategoryScale,
19+
LinearScale,
20+
PointElement,
21+
LineElement,
22+
Title,
23+
Tooltip,
24+
Legend
25+
)
26+
27+
28+
export const options: ChartOptions<'line'> = {
29+
responsive: true,
30+
spanGaps: true,
31+
32+
plugins: {
33+
legend: {
34+
position: 'right' as const,
35+
},
36+
title: {
37+
display: false,
38+
},
39+
},
40+
}
41+
42+
43+
const PALETTE: [string, string][] = [
44+
['rgb(255, 18, 67)', 'rgba(255, 18, 67, 0.5)'],
45+
['rgb(125, 0, 35)', 'rgba(125, 0, 35, 0.5)'],
46+
['rgb(36, 55, 70)', 'rgba(36, 55, 70, 0.5)'],
47+
['rgb(0, 112, 121)', 'rgba(0, 112, 121, 0.5)'],
48+
]
49+
50+
export const LinePlot = ({ missions }: { missions: Mission[] }) => {
51+
const chronological = [...missions].reverse()
52+
53+
const labels = chronological.map((m) => {
54+
const date = m.endTime ?? m.startTime ?? m.creationTime
55+
return formatDateTime(new Date(date), 'dd.MM.yy - HH:mm')
56+
})
57+
58+
const tags = missions[0].tasks.map((task, i) => task.tagId ?? `Task ${i + 1}`)
59+
60+
const datasets = tags.map((tagId, i) => {
61+
const [borderColor, backgroundColor] = PALETTE[i % PALETTE.length]
62+
return {
63+
label: tagId,
64+
data: chronological.map((mission) => {
65+
const raw = mission.tasks[i]?.inspection?.analysisResult?.value
66+
return raw !== undefined && raw !== null ? Number(raw) : null
67+
}),
68+
borderColor,
69+
backgroundColor,
70+
}
71+
})
72+
const chartData: ChartData<'line'> = { labels, datasets }
73+
console.log(chartData)
74+
return (
75+
<Line options={options} data={chartData} />
76+
)
77+
}

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)