Device Registrations: managing devices

(The Device Registrations page, showing the Register and Remove Device buttons.)

 

View the source code for this file.

The code for the Device Registration page contains some similar code to the Dashboard. We do some imports, define a device configuration, and fetch that configuration.

On this page, we start exposing functionality that performs calls to the IoT Center server itself.

DevicesPage

A lot happens in this block, which makes up most of the file. The block includes definitions of functions to add and remove authorizations, and code that creates and populates elements of the UI.

const DevicesPage: FunctionComponent<Props> = ({helpCollapsed}) => {
    // Some setup happens here.
    // useEffect(() => {...}
    const removeAuthorization {...}
    const addAuthorization  {...}
    // Define table columns.
    // Return page content.
 }

(Source) This code has been condensed.

One piece of data needed is the registration time of each device. This uses a Flux query.

fetchLastEntryTime

const fetchLastEntryTime = async (deviceId: string): Promise<LastEntryTime> => {
  const config = await fetchDeviceConfig(deviceId)
  const {
    // influx_url: url, // use '/influx' proxy to avoid problem with InfluxDB v2 Beta (Docker)
    influx_token: token,
    influx_org: org,
    influx_bucket: bucket,
    id,
  } = config
  const queryApi = new InfluxDB({url: '/influx', token}).getQueryApi(org)
  const result = await queryTable(
    queryApi,
    flux`
  import "influxdata/influxdb/v1"
  from(bucket: ${bucket})
    |> range(start: -1y)
    |> filter(fn: (r) => r.clientId == ${id})
    |> filter(fn: (r) => r._measurement == "environment")
    |> keep(columns: ["_time"])
    |> max(column: "_time")
    `
  )
  const lastColumn = result.getColumn('_time')
  if (lastColumn !== null) {
    const [lastEntry] = lastColumn as number[]
    return {lastEntry, deviceId}
  }
  return {deviceId}
}

(Source)

Let’s examine the Flux query called into the code above. It uses the range(), filter(), keep(), and max() functions.

import "influxdata/influxdb/v1"
from(bucket: ${bucket})
  |> range(start: -1y)
  |> filter(fn: (r) => r.clientId == ${id})
  |> filter(fn: (r) => r._measurement == "environment")
  |> keep(columns: ["_time"])
  |> max(column: "_time")

Buttons: register and remove devices

Here we focus on the functionality provided in the browser UI for the IoT Center. The following code registers a new device from the UI button and form.

Register a device

  const addAuthorization = async (deviceId: string) => {
    try {
      setLoading(true)
      const response = await fetch(`/api/env/${deviceId}`)
      if (response.status >= 300) {
        const text = await response.text()
        throw new Error(`${response.status} ${text}`)
      }
      const {newlyRegistered} = await response.json()

      setLoading(false)
      if (newlyRegistered) {
        antdMessage.success(`Device '${deviceId}' was registered`, 2)
      } else {
        antdMessage.success(`Device '${deviceId}' is already registered`, 2)
      }
    } catch (e) {
      setLoading(false)
      setMessage({
        title: 'Cannot register device',
        description: String(e),
        type: 'error',
      })
    } finally {
      setDataStamp(dataStamp + 1)
    }
  }

(Source)

The fourth line here /api/env/${deviceId} calls to an API endpoint we’ll look at in Chapter 6.

Remove Device

Once a device is registered, it appears on the page. Clicking on the Remove Device button will send an API call via an HTTP DELETE request to /api/devices/<deviceID>.

The function responsible for this is removeAuthorization:

  const removeAuthorization = async (device: DeviceInfo) => {
    try {
      setLoading(true)
      const response = await fetch(`/api/devices/${device.deviceId}`, {
        method: 'DELETE',
      })
      if (response.status >= 300) {
        const text = await response.text()
        throw new Error(`${response.status} ${text}`)
      }
      setLoading(false)
      antdMessage.success(`Device ${device.deviceId} was unregistered`, 2)
    } catch (e) {
      setLoading(false)
      setMessage({
        title: 'Cannot remove device',
        description: String(e),
        type: 'error',
      })
    } finally {
      setDataStamp(dataStamp + 1)
    }
  }

(Source)

Page code: building the page

Defining the devices table

The table sorts devices by DeviceID and Registration Time.

  // define table columns
  const columnDefinitions: ColumnsType<DeviceInfo> = [
    {
      title: 'Device ID',
      dataIndex: 'deviceId',
      defaultSortOrder: 'ascend',
      render: (deviceId: string) => (
        <Link to={`/devices/${deviceId}`}>{deviceId}</Link>
      ),
      sorter: (a: DeviceInfo, b: DeviceInfo) =>
        a.deviceId > b.deviceId ? 1 : -1,
    },
    {
      title: 'Registration Time',
      dataIndex: 'createdAt',
      responsive: helpLayout(['lg'], ['xxl']),
    },
    {
      title: 'Last Entry',
      dataIndex: 'deviceId',
      render: (id: string) => {
        const lastEntry = lastEntries.find(({deviceId}) => deviceId === id)
          ?.lastEntry
        if (lastEntry != null && lastEntry !== 0)
          return timeFormatter({
            timeZone: 'UTC',
            format: 'YYYY-MM-DD HH:mm:ss ZZ',
          })(lastEntry)
      },
      responsive: helpLayout(['xl'], []),
    },
    {
      title: '',
      key: 'action',
      align: 'right',
      render: (_: string, device: DeviceInfo) => (
        <>
          <Tooltip title="Go to device settings" placement="topRight">
            <Button
              icon={<SettingFilled />}
              href={`/devices/${device.deviceId}`}
            />
          </Tooltip>
          <Tooltip title="Go to device dashboard" placement="topRight">
            <Button
              icon={<AreaChartOutlined />}
              href={`/dashboard/${device.deviceId}`}
            />
          </Tooltip>
          <Popconfirm
            icon={<ExclamationCircleFilled style={{color: 'red'}} />}
            title={`Are you sure to remove '${device.deviceId}' ?`}
            onConfirm={() => removeAuthorization(device)}
            okText="Yes"
            okType="danger"
            cancelText="No"
          >
            <Tooltip title="Remove device" placement="topRight" color="red">
              <Button type="default" icon={<DeleteFilled />} danger />
            </Tooltip>
          </Popconfirm>
        </>
      ),
    },
  ]

(Source)

Returning page content

  return (
    <PageContent
      title="Device Registrations"
      spin={loading}
      message={message}
      titleExtra={
        <>
          <Tooltip title="Register a new Device">
            <Button
              type="dashed"
              onClick={() => {
                let deviceId = ''
                Modal.confirm({
                  title: 'Register Device',
                  icon: '',
                  content: (
                    <Form name="registerDevice" initialValues={{deviceId}}>
                      <Form.Item
                        name="deviceId"
                        rules={[
                          {required: true, message: 'Please input device ID !'},
                        ]}
                      >
                        <Input
                          placeholder="Device ID"
                          onChange={(e) => (deviceId = e.target.value)}
                        />
                      </Form.Item>
                    </Form>
                  ),
                  onOk: () => {
                    addAuthorization(deviceId)
                  },
                  okText: 'Register',
                })
              }}
            >
              Register
            </Button>
          </Tooltip>
          <Tooltip title="Reload Table">
            <Button
              type="primary"
              onClick={() => setDataStamp(dataStamp + 1)}
              style={{marginRight: '8px'}}
            >
              Reload
            </Button>
          </Tooltip>
        </>
      }
    >
      <Table dataSource={data} columns={columnDefinitions}></Table>
    </PageContent>
  )
}

(Source)