Введение

В этом блоге мы хотели продемонстрировать использование GridDB как лучшего друга веб-приложения. Мы решили сохранить всю функциональность этого приложения в самом Интернете — это означает, что мы будем использовать GridDB WebAPI на всех этапах процесса: создание схемы, прием и запросы.

Весь процесс будет обрабатываться нашим интерфейсом React.js. Это означает, что пользователь введет свои учетные данные GridDB WebAPI, загрузит CSV-файл по своему выбору, выберет, будет ли этот контейнер COLLECTION или TIMESERIES, а затем создаст правильную схему в GridDB. После создания контейнера весь файл .csv будет загружен в базу данных.

Как только данные будут на месте, вы можете запросить получение данных с вашего сервера, а затем, наконец, визуализировать их с помощью recharts библиотеки.

Предпосылки

Чтобы продолжить, вам понадобится запущенный и работающий сервер GridDB. Вам также потребуется установить GridDB WebAPI и запустить его на своем сервере.

Для внешнего интерфейса вам нужно будет установить react.js вместе с библиотекой диаграмм. Вы можете увидеть, как выглядит файл package.json здесь:

{
  "name": "griddb-charts",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@emotion/react": "^11.8.2",
    "@emotion/styled": "^11.8.1",
    "@mui/material": "^5.5.0",
    "@testing-library/jest-dom": "^5.16.2",
    "@testing-library/react": "^12.1.4",
    "@testing-library/user-event": "^13.5.0",
    "axios": "^0.26.1",
    "react": "^17.0.2",
    "react-dom": "^17.0.2",
    "react-scripts": "5.0.0",
    "recharts": "^2.1.9",
    "web-vitals": "^2.1.4"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": [
      "react-app",
      "react-app/jest"
    ]
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  }
}

Верхняя часть вашего файла также займет весь ваш импорт:

import React, { useState, useEffect } from 'react';
import './App.css';
import { usePapaParse } from 'react-papaparse';
import {
  BarChart,
  Bar,
  XAxis,
  YAxis,
  Tooltip,
  Legend
} from "recharts";
import Box from '@mui/material/Box';
import InputLabel from '@mui/material/InputLabel';
import MenuItem from '@mui/material/MenuItem';
import FormControl from '@mui/material/FormControl';
import Select from '@mui/material/Select';
import Container from '@mui/material/Container';
import TextField from '@mui/material/TextField';
import Button from '@mui/material/Button';
import Dialog from '@mui/material/Dialog';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import FormHelperText from "@mui/material/FormHelperText";
import Grid from '@mui/material/Grid'

ETL

Для реализации мы пройдемся в следующем порядке: учетные данные пользователя, создание и отправка схемы/контейнера, передача данных в контейнер, запрос данных, а затем мы закончим отображением данных на диаграмме.

Реквизиты для входа

Для начала мы создадим несколько простых текстовых полей, чтобы пользователь мог вводить свои учетные данные пользователя и администратора. Для нашего блога мы просто будем использовать значения по умолчанию GridDB admin/admin в качестве доказательства концепции.

Кроме того, для простоты мы будем использовать фреймворк material ui React.js.

<TextField
            id="user"
            label="Username"
            variant="standard"
            onChange={handleUser}
            required={true}
          />
          <TextField
            id="standard-basic"
            label="Password"
            variant="standard"
            onChange={handlePass}
            required={true}
          />

Здесь вы можете видеть, что мы обрабатываем наши текстовые поля с помощью отдельных функций. Мы будем использовать useState React для обработки изменений и сохранения ввода пользователя. Для простоты у нас не будет никакой проверки — мы просто сохраним каждый ввод в состояние

const [user, setUser] = useState('')
  const [pass, setPass] = useState('')
const handleUser = (event) => {
    let val = event.target.value
    setUser(val)
  }
const handlePass = (event) => {
    let val = event.target.value
    setPass(val)
  }

Чтобы сделать правильные запросы fetch javascript, нам также потребуется преобразовать учетные данные пользователя в base64. Мы можем сделать это так:

const [encodedUserPass, setEncodedUserPass] = useState('')
useEffect(() => {
    let encodedUserPass = btoa(user + ":" + pass)
    setEncodedUserPass(encodedUserPass)
  }, [pass])

[pass] после обратного вызова функции для useEffect просто означает, что мы будем обновлять эту конкретную функцию при обновлении переменной состояния pass. Не самое элегантное решение, но для наших целей сгодится.

Загрузка CSV-файла

Во-первых, давайте разрешим нашему пользователю загрузить файл .csv. Это очень легко сделать с помощью HTML.

<label >Choose a CSV File to Upload:</label>
<input type="file"
          id="csvFile" name="file" onChange={fileHandler}
          accept=".csv"></input>

Наша функция fileHandler будет использовать библиотеку papa-parse для чтения файла, анализа содержимого и установки некоторых состояний React для последующего использования.

import { usePapaParse } from 'react-papaparse';
  const { readString } = usePapaParse();
const [fullCsvData, setFullCsvData] = useState(null);
  const [fileName, setFileName] = useState('');
  const [selectedFile, setSelectedFile] = useState(null);
const fileHandler = (event) => {
    const file = event.target.files[0]
    let name = file.name
    let x = name.substring(0, name.indexOf('.')); //remove the .csv file extension
    const reader = new FileReader();
    reader.addEventListener('load', (event) => {
      var data = event.target.result
readString(data, {
        worker: true,
        complete: (results) => {
          setFileName(x)
          console.log("selected file: ", results.data[0])
          setFullCsvData(results.data)
          setSelectedFile(results.data[0]);
          setOpen(true)
        },
      });
    });
reader.readAsText(file);
};

Таким образом, как только пользователь загрузит файл .csv, функция fileHandler сработает, приняв файл как event. Затем он прочитает содержимое файла, а затем проанализирует все. После анализа мы установим различные состояния React: имя файла, полное содержимое csv и первый массив данных, соответствующий именам столбцов.

Установка типов столбцов

При синтаксическом анализе пользовательского файла может быть сложно надежно проанализировать тип данных строки. Чтобы обойти эту проблему, мы просто откроем модальное окно после загрузки файла и позволим пользователю установить тип данных для каждого столбца. Мы будем использовать эти данные для формирования нашего объекта, который используется для создания схемы с веб-API.

Это структура, которую ожидает WebAPI:

'{
    "columns": [
        {
            "name": "timestamp",
            "type": "TIMESTAMP"
        },
        {
            "name": "name",
            "type": "STRING"
        },
        {
            "name": "value",
            "type": "FLOAT"
        }
    ],
    "container_name": "test",
    "container_type": "TIME_SERIES",
    "rowkey": true
}'

Как только пользователь введет информацию о столбце и нажмет кнопку «Создать схему», мы подготовим необходимые данные для отправки через HTTP-запрос.

‹Button onClick={handleSchema} variant="contained"›Создать схему‹/Button›

Функция handleSchema просто вызывает нашу функцию putSchema

HTTP-запрос GridDB WebAPI (поместить схему/контейнер)

const handleSchema = () => {
    if (Object.keys(chartColumns).length !== 0) { //chartColumns is an array of the column names from the csv
      putSchema(chartColumns)
    }
  }
const putSchema = (obj) => {
let data = new Object();
    data.columns = [];
    let n = Object.keys(obj).length
    let i = 0
    for (const property in obj) {
      if (i < n) {
        data.columns[i] = { "name": property, "type": (obj[property]).toUpperCase() }
        i++
      }
    }
    data["container_name"] = fileName // grabbed from the react State 
    data["container_type"] = "COLLECTION" // hardcoded for now
    data["rowkey"] = "true"
let raw = JSON.stringify(data);
let myHeaders = new Headers();
    myHeaders.append("Content-type", "application/json")
    myHeaders.append("Authorization", "Basic " + encodedUserPass);
let requestOptions = {
      method: 'POST',
      headers: myHeaders,
      body: raw,
      redirect: 'follow'
    };
fetch(`http://${ADDRESS}/griddb/v2/defaultCluster/dbs/public/containers`, requestOptions)
      .then(response => response.text())
      .then(result => {
        setChartColumns("Successful. Now Push Data") // displays where the object was being formed
        console.log(result)
      })
      .catch(error => {
        setChartColumns(error)
        console.log("Error: ", error)
      });
  }

Здесь мы делаем наш первый HTTP-запрос веб-API. Прежде чем мы это сделаем, мы берем все данные, которые мы собрали, и формируем структуру данных, которую ожидает API. На данный момент мы жестко закодировали тип контейнера COLLECTION, но это можно легко исправить, добавив переключатель в модальное окно.

Главное, что нам нужно сделать, это создать объект, который имеет массив столбцов внутри с каждым именем столбца и типом внутри. Как только структура данных установлена, вы просто JSON.stringify отправляете ее в теле запроса вместе с файлом requestOptions. Последнее, что следует отметить, это то, что после выполнения запроса, если есть ошибка, она будет показана в модальном окне. В случае успеха появится простое сообщение, предлагающее пользователю отправить данные.

GridDB WebAPI передает данные CSV в контейнер

Затем, как только сервер GridDB принял наш HTTP-запрос на создание контейнера и его схемы, теперь мы можем отправить полные .csv данные на наш сервер с помощью HTTP-запроса.

Пользователю потребуется нажать кнопку PUSH DATA, которая активирует функцию putData.

const handlePushingData = () => {
    if (fullCsvData !== null) {
      putData(fullCsvData)
    }
  }
const putData = (data) => {
data.shift();
    let removeEmpties = data.filter(ele => ele.length > 1)
    console.log("data: ", removeEmpties)
let raw = JSON.stringify(removeEmpties)
    console.log(raw)
let myHeaders = new Headers();
    myHeaders.append("Content-type", "application/json")
    myHeaders.append("Authorization", "Basic " + encodedUserPass);
let requestOptions = {
      method: 'PUT',
      headers: myHeaders,
      body: raw,
      redirect: 'follow'
    };
fetch(`http://${ADDRESS}/griddb/v2/defaultCluster/dbs/public/containers/${fileName}/rows`, requestOptions)
      .then(response => response.text())
      .then(result => setChartColumns("Successful: ", result))
      .catch(error => {
        setChartColumns(error)
        console.log("Error: ", error)
      });
  }

Как и в случае с функцией putSchema, мы получаем все содержимое наших данных из состояния React, а затем выполняем некоторые базовые действия, чтобы получить нужные данные для отправки. Во-первых, мы избавляемся от первого элемента массива, так как это просто имена столбцов. Затем мы избавляемся от любых пустых элементов (если они есть). Все остальное должно быть самоочевидным — единственное предостережение в том, что метод HTTP-запроса для этого — PUT (а не POST).

В случае успеха количество столбцов будет отображаться в модальном окне. Если ошибка, она также будет отображаться в модальном окне. На данный момент мы загрузили наши .csv данные на наш сервер. Далее идет запрос и отображение.

Запрос

В демонстрационных целях мы также запросим сервер GridDB, чтобы повторно получить наши данные для их визуализации. Делать запросы с помощью WebAPI очень просто:

curl -X POST --basic -u admin:admin -H "Content-type:application/json" http://127.0.0.1:8080/griddb/v2/defaultCluster/dbs/public/containers/test/rows -d  '{"limit":1000}'

Для этого приложения мы загрузим его после того, как пользователь введет свои учетные данные и нажмет кнопку QUERY, которая запускает функцию handleSubmitCreds. Сервер ответит полными данными вашего запроса; затем мы возьмем эти данные и преобразуем их во что-то, что recharts сможет отобразить.

const handleSubmitCreds = () => {
    let raw = JSON.stringify({
      "limit": 100
    });
let myHeaders = new Headers();
    myHeaders.append("Content-type", "application/json")
    myHeaders.append("Authorization", "Basic " + encodedUserPass);
let requestOptions = {
      method: 'POST',
      headers: myHeaders,
      body: raw,
      redirect: 'follow'
    };
fetch(`http://${ADDRESS}/griddb/v2/defaultCluster/dbs/public/containers/CEREAL/rows`, requestOptions)
      .then(response => response.text())
      .then(result => {
        let resp = JSON.parse(result)
        let rows = resp.rows
let c = resp.columns
        let columns = [];
        c.forEach(val => columns.push(val.name))
let map = new Map();
        let fullChartData = [];
        // transform data into more usable obj
        for (let i = 0; i < 72; i++) { //hard coding the length of rows (72)
          for (let j = 0; j < 16; j++) { // hard coding length of columns (16)
            map.set(columns[j], rows[i][j])
          }
          const obj = Object.fromEntries(map);
          fullChartData.push(obj)
        }
        setData(fullChartData)
})
      .catch(error => console.log('error', error));
  }

Пустой массив в useEffect просто говорит нашему React запускать эту функцию только при загрузке страницы, а не при каждом повторном рендеринге.

Визуализация данных

Вышеупомянутая функция не только запросила данные, но и сумела преобразовать данные в более удобную форму.

Чтобы получить данные, которые ожидает recharts, мы будем использовать объект карты Javascript для установки пар данных ключ: значение, а затем создадим объект из этой карты, используя метод Object.fromEntries.

Для этой демонстрации мы будем использовать набор данных каггловских хлопьев. С данными, загруженными из приведенной выше функции, мы можем разместить все данные на линейной диаграмме, но я думаю, что более интересным вариантом использования было бы отображение макро- и микроэлементов одной отдельной каши на гистограмме. Для этого мы создадим раскрывающийся список со всеми перечисленными хлопьями. Когда пользователь выбирает хлопья, приложение находит соответствующие данные и отображает их на диаграмме.

<FormControl fullWidth>
      <InputLabel id="demo-simple-select-label">Cereal</InputLabel>
      <Select labelId="demo-simple-select-label" id="demo-simple-select" value={choice} label="Cereal" onChange={handleChange}>
        <MenuItem value={"100% Bran"}>100% Bran</MenuItem>
        <MenuItem value={"100% Natural Bran"}>100% Natural Bran</MenuItem>
        <MenuItem value={"All-Bran"}>All Bran</MenuItem>
        <MenuItem value={"All-Bran with Extra Fiber"}> All-Bran with Extra Fiber</MenuItem>
        <MenuItem value={"Almond Delight"}>Almond Delight</MenuItem>

Когда пользователь выбирает хлопья, наша функция handleChange срабатывает:

const handleChange = (event) => {
    let val = event.target.value
    console.log("val: ", val)
    setChoice(val);
  };

Что установит состояние React для выбора с названием хлопьев. Когда приложение обнаружит это изменение, оно активирует следующую функцию:

useEffect(() => {
    if (data !== null) {
      let userChoice = data.find(val => val.NAME == choice)
      setDisplayedData(userChoice);
    } else console.log("data still null")
  }, [choice])

Эта функция будет использовать метод поиска массива javascript, который находит правильные данные из полного набора данных, который сохраняется в нашем состоянии React как data. Как только он найдет эти данные, гистограмма отобразит правильные данные ( displayedData):

<BarChart
          width={1500}
          height={500}
          data={[displayedData]}
          margin={{
            top: 5,
            right: 30,
            left: 20,
            bottom: 5,
          }}
        >
          <XAxis dataKey="NAME" />
          <YAxis />
          <Tooltip />
          <Legend />
          <Bar type="monotone" stackId="a" dataKey="MANUFACTURER" fill="#FFF" />
          <Bar type="monotone" stackId="a" dataKey="TYPE" fill="#FFF" />
          <Bar type="monotone" stackId="b" dataKey="CALORIES" fill="red" />
          <Bar type="monotone" stackId="c" dataKey="PROTEIN" fill="pink" />
          <Bar type="monotone" stackId="c" dataKey="FAT" fill="orange" />
          <Bar type="monotone" stackId="d" dataKey="SODIUM" fill="#82ca9d" />
          <Bar type="monotone" stackId="d" dataKey="FIBER" fill="#82ca9d" />
          <Bar type="monotone" stackId="c" dataKey="CARBO" fill="purple" />
          <Bar type="monotone" stackId="e" dataKey="SUGARS" fill="#82ca9d" />
          <Bar type="monotone" stackId="e" dataKey="POTASS" fill="#82ca9d" />
          <Bar type="monotone" stackId="f" dataKey="VITAMINS" fill="#82ca9d" />
          <Bar type="monotone" stackId="f" dataKey="SHELF" fill="#82ca9d" />
          <Bar type="monotone" stackId="f" dataKey="WEIGHT" fill="#82ca9d" />
        </BarChart>

Для этой таблицы вы можете сложить различные микро- и макроэлементы, как вам нравится, с разными цветами. Для этой демонстрации я не устанавливал ничего, кроме установки калорий на отдельной панели, чтобы мы могли их видеть. Вот диаграмма для хрустящих тостов с корицей

Заключение

Теперь мы можем легко получить и загрузить наши данные csv в красивый файл react.js recharts.

Полный исходный код можно найти на нашей странице Github.

Первоначально опубликовано на https://griddb.net 25 марта 2022 г.