|
1
|
|
|
import React, { Component } from 'react'; |
|
2
|
|
|
import { Card, CardHeader, CardBody, ListGroup, ListGroupItem } from 'reactstrap'; |
|
3
|
|
|
import { withTranslation } from 'react-i18next'; |
|
4
|
|
|
import { v4 as uuid } from 'uuid'; |
|
5
|
|
|
import { Line } from 'react-chartjs-2'; |
|
6
|
|
|
import { Chart } from 'chart.js'; |
|
7
|
|
|
import { |
|
8
|
|
|
LineController, LineElement, PointElement, |
|
9
|
|
|
LinearScale, Title, Tooltip, Legend, CategoryScale |
|
10
|
|
|
} from 'chart.js'; |
|
11
|
|
|
import { APIBaseURL } from '../../../config'; |
|
12
|
|
|
import { getCookieValue } from '../../../helpers/utils'; |
|
13
|
|
|
import { toast } from 'react-toastify'; |
|
14
|
|
|
|
|
15
|
|
|
Chart.register( |
|
16
|
|
|
LineController, LineElement, PointElement, |
|
17
|
|
|
LinearScale, CategoryScale, Title, Tooltip, Legend |
|
18
|
|
|
); |
|
19
|
|
|
|
|
20
|
|
|
const dividerBorder = '1px solid rgba(0, 0, 0, 0.05)'; |
|
21
|
|
|
const listItemBorderColor = 'rgba(0, 0, 0, 0.05)'; |
|
22
|
|
|
|
|
23
|
|
|
class RealtimeData extends Component { |
|
24
|
|
|
_isMounted = false; |
|
25
|
|
|
refreshInterval; |
|
26
|
|
|
state = { |
|
27
|
|
|
pointList: [], |
|
28
|
|
|
trendData: {} |
|
29
|
|
|
}; |
|
30
|
|
|
|
|
31
|
|
|
componentWillUnmount() { |
|
32
|
|
|
this._isMounted = false; |
|
33
|
|
|
clearInterval(this.refreshInterval); |
|
34
|
|
|
} |
|
35
|
|
|
|
|
36
|
|
|
componentDidMount() { |
|
37
|
|
|
this._isMounted = true; |
|
38
|
|
|
this.fetchData(); |
|
39
|
|
|
|
|
40
|
|
|
this.refreshInterval = setInterval(() => { |
|
41
|
|
|
this.fetchData(); |
|
42
|
|
|
}, (60 + Math.floor(Math.random() * Math.floor(10))) * 1000); |
|
43
|
|
|
} |
|
44
|
|
|
|
|
45
|
|
|
fetchData = async () => { |
|
46
|
|
|
try { |
|
47
|
|
|
const response = await fetch( |
|
48
|
|
|
`${APIBaseURL}/reports/spaceenvironmentmonitor?sensorid=${this.props.sensorId}&timerange=24h`, |
|
49
|
|
|
{ |
|
50
|
|
|
method: 'GET', |
|
51
|
|
|
headers: { |
|
52
|
|
|
'Content-type': 'application/json', |
|
53
|
|
|
'User-UUID': getCookieValue('user_uuid'), |
|
54
|
|
|
'Token': getCookieValue('token') |
|
55
|
|
|
} |
|
56
|
|
|
} |
|
57
|
|
|
); |
|
58
|
|
|
|
|
59
|
|
|
if (!response.ok) throw new Error('Data fetch failed'); |
|
60
|
|
|
const json = await response.json(); |
|
61
|
|
|
|
|
62
|
|
|
const pointList = []; |
|
63
|
|
|
const trendData = {}; |
|
64
|
|
|
|
|
65
|
|
|
if (json['energy_value']) { |
|
66
|
|
|
pointList.push({ |
|
67
|
|
|
name: json['energy_value'].name, |
|
68
|
|
|
value: json['energy_value'].values.length > 0 |
|
69
|
|
|
? json['energy_value'].values[json['energy_value'].values.length - 1] |
|
70
|
|
|
: undefined |
|
71
|
|
|
}); |
|
72
|
|
|
trendData[json['energy_value'].name] = { |
|
73
|
|
|
values: json['energy_value'].values, |
|
74
|
|
|
timestamps: json['energy_value'].timestamps.map(ts => ts.substring(11, 16)) |
|
75
|
|
|
}; |
|
76
|
|
|
} |
|
77
|
|
|
|
|
78
|
|
|
json['parameters']['names'].forEach((name, index) => { |
|
79
|
|
|
const values = json['parameters']['values'][index]; |
|
80
|
|
|
const timestamps = json['parameters']['timestamps'][index].map(ts => ts.substring(11, 16)); |
|
81
|
|
|
|
|
82
|
|
|
pointList.push({ |
|
83
|
|
|
name, |
|
84
|
|
|
value: values.length > 0 ? values[values.length - 1] : undefined |
|
85
|
|
|
}); |
|
86
|
|
|
|
|
87
|
|
|
trendData[name] = { values, timestamps }; |
|
88
|
|
|
}); |
|
89
|
|
|
|
|
90
|
|
|
if (this._isMounted) { |
|
91
|
|
|
this.setState({ pointList, trendData }); |
|
92
|
|
|
} |
|
93
|
|
|
} catch (err) { |
|
94
|
|
|
console.error('Realtime data fetch error:', err); |
|
95
|
|
|
toast.error(this.props.t(err.message || 'Data fetch failed')); |
|
96
|
|
|
} |
|
97
|
|
|
}; |
|
98
|
|
|
|
|
99
|
|
|
sampleData = (data, maxPoints = 24) => { |
|
100
|
|
|
if (data.length <= maxPoints) return data; |
|
101
|
|
|
|
|
102
|
|
|
const step = Math.ceil(data.length / maxPoints); |
|
103
|
|
|
return data.filter((_, index) => index % step === 0); |
|
104
|
|
|
}; |
|
105
|
|
|
|
|
106
|
|
|
renderTrendChart = (name, data) => { |
|
107
|
|
|
const colors = ['#36A2EB', '#4BC0C0', '#FF9F40', '#9966FF', '#FF6384', '#00CC99']; |
|
108
|
|
|
const colorIndex = Object.keys(this.state.trendData).indexOf(name) % colors.length; |
|
109
|
|
|
|
|
110
|
|
|
const sampledValues = this.sampleData(data.values, 24); |
|
111
|
|
|
const sampledTimestamps = this.sampleData(data.timestamps, 24); |
|
112
|
|
|
|
|
113
|
|
|
return ( |
|
114
|
|
|
<Line |
|
115
|
|
|
data={{ |
|
116
|
|
|
labels: sampledTimestamps, |
|
117
|
|
|
datasets: [{ |
|
118
|
|
|
label: name, |
|
119
|
|
|
data: sampledValues, |
|
120
|
|
|
borderColor: colors[colorIndex], |
|
121
|
|
|
backgroundColor: `${colors[colorIndex]}20`, |
|
122
|
|
|
borderWidth: 2, |
|
123
|
|
|
pointRadius: 1.5, |
|
124
|
|
|
pointHoverRadius: 3, |
|
125
|
|
|
tension: 0.4, |
|
126
|
|
|
fill: true |
|
127
|
|
|
}] |
|
128
|
|
|
}} |
|
129
|
|
|
options={{ |
|
130
|
|
|
responsive: true, |
|
131
|
|
|
maintainAspectRatio: false, |
|
132
|
|
|
plugins: { |
|
133
|
|
|
legend: { display: false }, |
|
134
|
|
|
tooltip: { |
|
135
|
|
|
mode: 'index', |
|
136
|
|
|
intersect: false, |
|
137
|
|
|
backgroundColor: 'rgba(0, 0, 0, 0.7)', |
|
138
|
|
|
titleFont: { size: 10 }, |
|
139
|
|
|
bodyFont: { size: 10 } |
|
140
|
|
|
} |
|
141
|
|
|
}, |
|
142
|
|
|
scales: { |
|
143
|
|
|
x: { |
|
144
|
|
|
display: false |
|
145
|
|
|
}, |
|
146
|
|
|
y: { |
|
147
|
|
|
display: false, |
|
148
|
|
|
beginAtZero: false |
|
149
|
|
|
} |
|
150
|
|
|
}, |
|
151
|
|
|
interaction: { |
|
152
|
|
|
mode: 'nearest', |
|
153
|
|
|
axis: 'x', |
|
154
|
|
|
intersect: false |
|
155
|
|
|
} |
|
156
|
|
|
}} |
|
157
|
|
|
height={50} |
|
158
|
|
|
/> |
|
159
|
|
|
); |
|
160
|
|
|
}; |
|
161
|
|
|
|
|
162
|
|
|
render() { |
|
163
|
|
|
const { t, isActive, onClick } = this.props; |
|
164
|
|
|
|
|
165
|
|
|
return ( |
|
166
|
|
|
<Card |
|
167
|
|
|
className={`h-100 shadow-sm cursor-pointer ${isActive ? 'border-primary' : 'border-light'}`} |
|
168
|
|
|
onClick={onClick} |
|
169
|
|
|
> |
|
170
|
|
|
<CardHeader className="bg-white border-bottom py-3"> |
|
171
|
|
|
<h6 className="mb-0">{this.props.sensorName}</h6> |
|
172
|
|
|
</CardHeader> |
|
173
|
|
|
<CardBody className="p-2"> |
|
174
|
|
|
<ListGroup flush className="mt-1"> |
|
175
|
|
|
<ListGroupItem |
|
176
|
|
|
className="bg-transparent font-weight-bold border-top-0 py-1" |
|
177
|
|
|
style={{ borderColor: listItemBorderColor }} |
|
178
|
|
|
> |
|
179
|
|
|
<div className="d-flex justify-content-between"> |
|
180
|
|
|
<span className="text-muted">{t('Point')}</span> |
|
181
|
|
|
<span className="text-muted">{t('Value')}</span> |
|
182
|
|
|
</div> |
|
183
|
|
|
</ListGroupItem> |
|
184
|
|
|
{this.state.pointList.map((item, index) => ( |
|
185
|
|
|
<ListGroupItem |
|
186
|
|
|
key={uuid()} |
|
187
|
|
|
className="bg-transparent d-flex flex-column justify-content-between py-1" |
|
188
|
|
|
style={{ borderColor: listItemBorderColor }} |
|
189
|
|
|
> |
|
190
|
|
|
<div className="d-flex justify-content-between align-items-center mb-0"> |
|
191
|
|
|
<span>{item.name}</span> |
|
192
|
|
|
<span className="font-weight-bold text-primary">{item.value}</span> |
|
193
|
|
|
</div> |
|
194
|
|
|
{this.state.trendData[item.name] && ( |
|
195
|
|
|
<div className="mt-0"> |
|
196
|
|
|
{this.renderTrendChart(item.name, this.state.trendData[item.name])} |
|
197
|
|
|
</div> |
|
198
|
|
|
)} |
|
199
|
|
|
</ListGroupItem> |
|
200
|
|
|
))} |
|
201
|
|
|
</ListGroup> |
|
202
|
|
|
</CardBody> |
|
203
|
|
|
</Card> |
|
204
|
|
|
); |
|
205
|
|
|
} |
|
206
|
|
|
} |
|
207
|
|
|
|
|
208
|
|
|
export default withTranslation()(RealtimeData); |