Skip to content

Commit 8e8000f

Browse files
committed
feat: Next.js (React) UI for iot-app examples.
- Replace http-proxy with Next.js' native Rewrite module that just works. - Add Giraffe plot for device measurements. - Add Device Card to view details, data, plot.
1 parent 696bc3b commit 8e8000f

12 files changed

+171
-88
lines changed

.env.development

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
# Development environment non-secret defaults
22

3-
API_URL=http://127.0.0.1:5200
3+
API_URL=http://127.0.0.1:5200/api

next.config.js

+12-1
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,15 @@ const nextConfig = {
33
reactStrictMode: true,
44
}
55

6-
module.exports = nextConfig
6+
module.exports = {
7+
async rewrites() {
8+
return {
9+
beforeFiles: [
10+
{
11+
source: '/api/:endpoint*',
12+
destination: `${process.env.API_URL}/:endpoint*`, // The :path parameter isn't used here so will be automatically passed in the query
13+
},
14+
]
15+
}
16+
},
17+
}

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"lint": "next lint"
1010
},
1111
"dependencies": {
12+
"@influxdata/giraffe": "^2.26.1",
1213
"next": "12.1.6",
1314
"react": "18.1.0",
1415
"react-dom": "18.1.0"

pages/api/[...path].ts

-9
This file was deleted.

pages/devices/_device.tsx

+82-17
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,93 @@
11
import React, { useState, useEffect } from 'react';
2+
import dynamic from 'next/dynamic'
23

3-
export default function Device({ device, deviceData, error, isLoading }:
4-
{device: any, deviceData: any, error: string | null, isLoading: boolean}
5-
) {
4+
/* Wrap DevicePlot with a dynamic import to disable server-side rendering.
5+
* Prevents `self is undefined` errors caused by Giraffe when rendered
6+
* with Node.
7+
*/
8+
const DynamicDevicePlotWithNoSSR = dynamic(
9+
() => import('./_devicePlot'),
10+
{ ssr: false }
11+
)
612

7-
function writeSimulatedData() {
13+
export default function Device({ deviceId }: {deviceId: string}) {
14+
const [error, setError] = useState<string | null>(null)
15+
const [isLoading, setIsLoading] = useState(false)
16+
const [device, setDevice] = useState<any[] | null>(null)
17+
const [data, setData] = useState<any | null>(null)
18+
19+
const getDeviceData = (deviceId: string) => {
20+
setIsLoading(true)
21+
setError(null)
22+
fetch(`/api/devices/${deviceId}/measurements`,
23+
{ headers: { "Accept": "application/csv" } }
24+
)
25+
.then((res) => {
26+
if(res.ok) {
27+
return res.text()
28+
} else {
29+
setError(`Device data ${res.statusText}`)
30+
setIsLoading(false)
31+
}
32+
})
33+
.then((data) =>{
34+
setData(data)
35+
setIsLoading(false)
36+
})
37+
}
38+
39+
const getDevice = (deviceId: string) => {
40+
setIsLoading(true)
41+
fetch(`/api/devices/${deviceId}`)
42+
.then((res) => res.json())
43+
.then((data) => {
44+
if(Array.isArray(data)) {
45+
setDevice(data)
46+
}
47+
if(data.error) {
48+
setError(data.error)
49+
}
50+
setIsLoading(false)
51+
})
52+
}
853

54+
useEffect(() => {
55+
deviceId &&
56+
getDevice(deviceId)
57+
getDeviceData(deviceId)
58+
}, [deviceId])
59+
60+
function writeSimulatedData() {
61+
fetch('/api/devices/generate', {
62+
method: 'POST',
63+
body: JSON.stringify({ deviceIds: [deviceId] }),
64+
headers: { "Content-Type": "application/json" }
65+
})
66+
.then((res) => {
67+
if(res.ok) {
68+
getDeviceData(deviceId)
69+
} else {
70+
setError(res.statusText)
71+
}
72+
})
973

1074
}
1175

1276
return (
13-
<>
14-
<div className='card'>
15-
<div className="alert">
16-
{ isLoading && <span>Loading...</span> }
17-
{ error &&
18-
<span className="alert-danger">{ error }</span>
19-
}
20-
</div>
21-
<h2>Device</h2>
22-
<p>{device.deviceId}</p>
23-
<p>{JSON.stringify(deviceData)}</p>
24-
77+
deviceId ?
78+
<div className='card'>
79+
<div className="alert">
80+
{ isLoading && <span>Loading...</span> }
81+
{ error &&
82+
<span className="alert-danger">{ error }</span>
83+
}
2584
</div>
26-
</>
85+
<h2>Device</h2>
86+
<p>{deviceId}</p>
87+
<p><button onClick={ writeSimulatedData }>Generate data for this device</button></p>
88+
{ data &&
89+
<div><DynamicDevicePlotWithNoSSR csv={data} title={'Measurements'} lastUpdated={''} /></div>
90+
}
91+
</div> : <></>
2792
)
2893
}

pages/devices/_deviceList.tsx

-37
This file was deleted.

pages/devices/_devicePlot.tsx

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { fromFlux, Plot, LayerTypes, Config, LayerConfig, HoverTimeProvider } from '@influxdata/giraffe'
2+
export default function DevicePlot({ csv, title, lastUpdated }) {
3+
4+
const style = {
5+
width: "calc(70vw - 20px)",
6+
height: "calc(70vh - 20px)",
7+
margin: "40px",
8+
}
9+
10+
const table = fromFlux(csv).table
11+
12+
const lineLayer: LayerConfig = {
13+
type: LayerTypes.Line,
14+
x: "_time",
15+
y: "_value",
16+
fill: []
17+
}
18+
19+
const tableLayer: LayerConfig = {
20+
type: LayerTypes.Table,
21+
timeZone: 'UTC',
22+
properties: {
23+
colors: [],
24+
tableOptions: {},
25+
fieldOptions: [],
26+
timeFormat: 'YYYY-MM-DD hh:mm:ss A',
27+
decimalPlaces: {
28+
isEnforced: false,
29+
digits: 5
30+
}
31+
}
32+
}
33+
34+
const simpleTableLayer: LayerConfig = {
35+
fluxResponse: csv,
36+
type: LayerTypes.SimpleTable,
37+
showAll: true
38+
}
39+
40+
const config: Config = {
41+
table,
42+
layers: [lineLayer],
43+
}
44+
45+
return(
46+
<div style={style}>
47+
<h3>{title} {lastUpdated}</h3>
48+
<Plot config={config} />
49+
</div>
50+
51+
)
52+
53+
}

pages/devices/_deviceRegistrationButton.tsx

+5-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@ export default function DeviceRegistrationButton({ deviceId, onError, isLoading}
99
isLoading(true)
1010
const body = JSON.stringify({ deviceId })
1111

12-
fetch('/api/devices/create', { method: 'POST', body })
12+
fetch('/api/devices/create', {
13+
method: 'POST',
14+
body,
15+
headers: { "Content-Type": "application/json" }
16+
})
1317
.then((res) => res.json())
1418
.then((data) => {
1519
if(data.error) {

pages/devices/_devicesCard.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import DeviceRegistrationButton from './_deviceRegistrationButton'
33
import DevicesList from './_devicesList'
44

55
export default function DevicesCard({ onSelectDevice }:
6-
{ onSelectDevice: (device: any) => void}
6+
{ onSelectDevice: (device: {deviceId: string}) => void }
77
) {
88
const [deviceId, setDeviceId] = useState('')
99
const [error, setError] = useState<string | null>(null)

pages/devices/_devicesList.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
import React, { useState, useEffect } from 'react';
22

33
export default function DevicesList({ deviceId, onSelectDevice, onError, isLoading}:
4-
{ deviceId: string, onSelectDevice: (device: any) => void,
4+
{ deviceId: string, onSelectDevice: (device: {deviceId: string}) => void,
55
onError: (error: string) => void, isLoading: (loading: boolean) => void }
66
) {
77
const [data, setData] = useState<any[] | null>(null)
88
const itemType = 'device'
99

1010
useEffect(() => {
1111
isLoading(true)
12-
fetch(`/api/devices/${deviceId || ''}`)
12+
fetch(`/api/devices/${deviceId || '' }`)
1313
.then((res) => res.json())
1414
.then((data) => {
1515
if(Array.isArray(data)) {

pages/devices/index.tsx

+2-19
Original file line numberDiff line numberDiff line change
@@ -8,33 +8,16 @@ export default function Devices() {
88
const [isLoading, setIsLoading] = useState(false)
99
const [deviceData, setDeviceData] = useState<any[] | null>(null)
1010

11-
function handleSelectDevice(selected: any) {
11+
function handleSelectDevice(selected: { deviceId: string }) {
1212
setDevice(selected)
13-
getDeviceData(selected.deviceId)
14-
}
15-
16-
function getDeviceData(deviceId: string) {
17-
setIsLoading(true)
18-
setError(null)
19-
fetch(`/api/data/${deviceId || ''}`)
20-
.then((res) => {
21-
if(res.ok) {
22-
const json = res.json()
23-
Array.isArray(json) && setDeviceData(json)
24-
setIsLoading(false)
25-
} else {
26-
setError(res.statusText)
27-
setIsLoading(false)
28-
}
29-
})
3013
}
3114

3215
return (
3316
<>
3417
{ device ?
3518
<div>
3619
<DevicesCard onSelectDevice={ handleSelectDevice } />
37-
<Device device={device} deviceData={deviceData} error={error} isLoading />
20+
<Device deviceId={device.deviceId} />
3821
</div>
3922
: <DevicesCard onSelectDevice={ handleSelectDevice } />
4023
}

yarn.lock

+12
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,13 @@
4646
resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45"
4747
integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==
4848

49+
"@influxdata/giraffe@^2.26.1":
50+
version "2.26.1"
51+
resolved "https://registry.yarnpkg.com/@influxdata/giraffe/-/giraffe-2.26.1.tgz#6af830da76c009373114b88b7e09a109313e3e40"
52+
integrity sha512-qi4SPFj8eFJZzK8+JMHLJ9AycQhA9CNOsZRhk+jiluZBNBVaIENF8fqQglCZWUEnMLsRf4fWo0sxlKycy2qOjQ==
53+
dependencies:
54+
merge-images "^2.0.0"
55+
4956
5057
version "12.1.6"
5158
resolved "https://registry.yarnpkg.com/@next/env/-/env-12.1.6.tgz#5f44823a78335355f00f1687cfc4f1dafa3eca08"
@@ -1155,6 +1162,11 @@ lru-cache@^6.0.0:
11551162
dependencies:
11561163
yallist "^4.0.0"
11571164

1165+
merge-images@^2.0.0:
1166+
version "2.0.0"
1167+
resolved "https://registry.yarnpkg.com/merge-images/-/merge-images-2.0.0.tgz#4d68d6d90ad8c33a91aced0b5d8660983bf4db3a"
1168+
integrity sha512-bpI4j54n/Zl6ZTgxaR3xWou/lqI53RAAt8peXijW37BKqoON83LQ7XCZqtFiwzBfEXIws1isYyR06584yffAyA==
1169+
11581170
merge2@^1.3.0, merge2@^1.4.1:
11591171
version "1.4.1"
11601172
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"

0 commit comments

Comments
 (0)