チュートリアルJan 6, 20228 分 READ

ワークフローやジョブのイベントを可視化する React ダッシュボードの構築方法

Olususi Oluyemi

フルスタック デベロッパー兼テクニカル ライター

データの可視化とは、大量のデータ セットやメトリクスを、表やグラフなどの目でとらえやすい形に変換するプロセスです。 データを目に見える形で表示することで、データに眠る情報を浮き彫りにして、トレンドや外れ値、新しいインサイトをリアルタイムで共有しやすくなります。 CircleCI には、ワークフローやジョブのイベントに関するデータを収集する Webhook 機能が用意されています。 このチュートリアルでは、これらのデータを可視化する React ベースのダッシュボードを構築する方法について、順を追って詳しくご紹介します。

前提条件

このチュートリアルは、JavaScript ES6 についての知識がある方を対象としています。 また、React の基本的なコンセプト (フックや関数コンポーネント) について知っていることも前提となります。

チュートリアルを始める前に、以下のものをインストールしてください。

  1. 最新バージョンの Node.js とパッケージ マネージャー (npm や yarn)
  2. お好きなコード エディター
  3. CircleCI Webhook と通信するための API。 API のセットアップ方法についてはこちら (英語) を参照してください。
  4. CircleCI Webhook 用の Laravel API。こちらのチュートリアル (英語) に従って、インストールし起動してください。

本チュートリアルはすべての CI/CD プラットフォームに対応していますが、サンプルとして CircleCI を使用します。 CircleCI アカウントをお持ちでない場合は、こちらから無料アカウントを作成してください。

作業を開始する

次のコマンドを実行して、新しい React アプリケーションを作成します。

yarn create react-app circleci_workflow_dashboard

cd circleci_workflow_dashboard

このプロジェクトでは、UI のレイアウト構築に Ant Design を、グラフの表示に react-chartjs-2 を使用します。

これらのツールも yarn でインストールします。

yarn add antd react-chartjs-2@3.3.0 chart.js

最後に、Ant Design のスタイルをインポートします。 src/App.css に次の文を追加してください。

@import '~antd/dist/antd.css';

ユーティリティ機能を追加する

次に、API との通信手段を追加しましょう。 src フォルダー内に、新しく utility という名前のフォルダーを作成します。 utility フォルダー内に、API.js という名前のファイルを作成します。 src/utility/API.js ファイルに、次のように入力します。

export const makeGETRequest = (url) => {
  return fetch(url, {
    method: "GET",
    headers: {
      "Content-Type": "application/json",
    },
  }).then((response) => response.json());
};

この関数は、API に GET リクエストを送信して、 API からの JSON 応答を返します。

簡略化のため、エラー処理は含めていません。

さらに、日付を読みやすい形式に変換するユーティリティ関数も作成しましょう。 utility フォルダー内に、Date.js という名前のファイルを作成して、次のように入力します。

export const formatDate = (date) =>
  new Date(date).toLocaleString("en-GB", {
    month: "long",
    weekday: "long",
    day: "numeric",
    year: "numeric",
  });

ダッシュボード コンポーネントを構築する

今回は、ステータスとイベント タイプによるフィルター機能を備えた、イベントの表を表示するダッシュボードを構築します。 さらに、表とグラフを切り替えて表示する機能も実装します。

src フォルダー内に、新しく components という名前のフォルダーを作成します。 このフォルダーに、これから作成するカスタム コンポーネントをすべて格納します。

components フォルダー内に、StatusTag.jsx という名前のファイルを作成します。 このコンポーネントは、イベントのステータスに応じた色でタグを表示するためのものです。 StatusTag.jsx に次のように入力します。

import React from "react";
import { Tag } from "antd";

const StatusTag = ({ status }) => {
  const statusColours = {
    success: "green",
    failed: "volcano",
    error: "red",
    canceled: "orange",
    unauthorized: "magenta",
  };
  return <Tag color={statusColours[status]}>{status}</Tag>;
};

export default StatusTag;

次に、components フォルダー内に、TableView.jsx という名前のファイルを作成します。 このコンポーネントは、通知を props (プロパティ) として受け取り、タイプとステータスによるフィルター機能を備えた表内にレンダリングします。 TableView.jsx に次のコードを入力します。

import React from "react";
import { Table } from "antd";
import { formatDate } from "../utility/Date";
import StatusTag from "./StatusTag";

const TableView = ({ notifications }) => {
  const filterHandler = (value, record, key) => record[key] === value;

  const columns = [
    {
      title: "Subject",
      dataIndex: "commit_subject",
    },
    {
      title: "Commit Author",
      dataIndex: "commit_author",
    },
    {
      title: "Happened At",
      dataIndex: "happened_at",
      render: (text) => formatDate(text),
    },
    {
      title: "Event Type",
      dataIndex: "type",
      filters: [
        { text: "Job", value: "job-completed" },
        { text: "Workflow", value: "workflow-completed" },
      ],
      onFilter: (value, record) => filterHandler(value, record, "type"),
    },
    {
      title: "Event Status",
      dataIndex: "event_status",
      filters: [
        { text: "Success", value: "success" },
        { text: "Failed", value: "failed" },
        { text: "Error", value: "error" },
        { text: "Canceled", value: "canceled" },
        { text: "Unauthorized", value: "unauthorized" },
      ],
      render: (text) => <StatusTag status={text} />,
      onFilter: (value, record) => filterHandler(value, record, "event_status"),
    },
    {
      title: "Notification ID",
      dataIndex: "notification_id",
      render: (text, record) => (
        <a href={record["workflow_url"]} target="_blank" rel="noreferrer">
          {text}
        </a>
      ),
    },
  ];

  return (
    <Table dataSource={notifications} columns={columns} rowKey="id" bordered />
  );
};

export default TableView;

この表では、Subject (件名)、Commit Author (コミット実行者)、Happened At (発生日)、Event Type (イベント タイプ)、Event Status (イベント ステータス)、Notification ID (通知 ID) という 6 つの列を作成しています。

各列は、columns 定数のオブジェクトとして表現します。 特殊なレンダリングが必要ない場合、列の宣言では title と dataIndex を指定するだけで十分です。 title は列のタイトルを指定し、dataIndex はこの列に入力するプロパティを Ant Design に指示します。

列内に表示するコンポーネントまたは JSX 要素を指定する場合は、render キーで列のオブジェクト表現を追加します。 このキーは、先ほど作成した StatusTag コンポーネントをレンダリングするために使用します。 また、Happened At (発生日) 列に日付を整形して表示するのと、Notification ID (通知 ID) 列にワークフローへのリンクを表示するためにも使用しています。

グラフ コンポーネントを構築する

このチュートリアルでは、データを次の 3 つのグラフで表示します。

  1. イベントのステータス (Success、Failed、Error、Canceled、Unauthorized) 別の分布を示す円グラフ
  2. イベントのタイプ (ワークフロー、ジョブ) 別の分布を示す棒グラフ
  3. イベントの時系列を示す折れ線グラフ

円グラフを構築するために、components フォルダー内に StatusDistribution.jsx という名前のファイルを作成して、次のコードを入力します。

import React from "react";
import { Pie } from "react-chartjs-2";

const StatusDistribution = ({ notifications }) => {
  const sortedNotifications = notifications.reduce(
    (sortedNotifications, notification) => {
      sortedNotifications[notification["event_status"]]++;
      return sortedNotifications;
    },
    { error: 0, failed: 0, success: 0, unauthorized: 0, canceled: 0 }
  );

  const data = {
    labels: ["Error", "Failed", "Success", "Unauthorized", "Canceled"],
    datasets: [
      {
        data: Object.values(sortedNotifications),
        backgroundColor: [
          "rgba(255, 99, 132, 0.2)",
          "rgba(255, 206, 86, 0.2)",
          "rgba(75, 192, 192, 0.2)",
          "rgba(153, 102, 255, 0.2)",
          "rgba(255, 159, 64, 0.2)",
        ],
        borderColor: [
          "rgba(255, 99, 132, 1)",
          "rgba(255, 206, 86, 1)",
          "rgba(75, 192, 192, 1)",
          "rgba(153, 102, 255, 1)",
          "rgba(255, 159, 64, 1)",
        ],
        borderWidth: 1,
      },
    ],
  };

  return (
    <>
      <div className="header">
        <h1 className="title">Status Distribution</h1>
      </div>
      <Pie data={data} height={50} />
    </>
  );
};

export default StatusDistribution;

ここでは、通知の配列に reduce 関数を使用することで、通知をステータス別に並べ替えて集計しています。 返された値は datasets 構成の data キーに渡して、さらにこのキーを Pie コンポーネントに渡しています。

次は棒グラフを構築するために、components フォルダー内に TypeDistribution.jsx という名前のファイルを作成して、次のコードを入力します。

import React from "react";
import { Bar } from "react-chartjs-2";

const TypeDistribution = ({ notifications }) => {
  const sortedNotifications = notifications.reduce(
    (sortedNotifications, notification) => {
      sortedNotifications[notification["type"]]++;
      return sortedNotifications;
    },
    { "job-completed": 0, "workflow-completed": 0 }
  );

  const data = {
    labels: ["Job", "Workflow"],
    datasets: [
      {
        label: "Event Type",
        data: Object.values(sortedNotifications),
        backgroundColor: ["rgba(54, 162, 235, 0.2)", "rgba(75, 192, 192, 0.2)"],
        borderColor: ["rgba(54, 162, 235, 1)", "rgba(75, 192, 192, 1)"],
        borderWidth: 1,
      },
    ],
  };

  const options = {
    scales: {
      y: {
        beginAtZero: true,
      },
    },
  };

  return (
    <>
      <div className="header">
        <h1 className="title">Type Distribution</h1>
      </div>
      <Bar data={data} options={options} height={500} />
    </>
  );
};

export default TypeDistribution;

ステータス分布の円グラフと同様に、タイプに応じて通知を並べ替えて集計しています。 集計後の値は datasets 構成の data キーに渡して、さらにこのキーを Bar コンポーネントに渡しています。

通知のタイムラインを構築するために、components フォルダー内に Timeline.jsx という名前のファイルを作成して、次のコードを入力します。

import React from "react";
import { Line } from "react-chartjs-2";
import { formatDate } from "../utility/Date";

const Timeline = ({ notifications }) => {
  const sortedNotifications = notifications.reduce(
    (sortedNotifications, notification) => {
      const notificationDate = formatDate(notification["happened_at"]);
      if (notificationDate in sortedNotifications) {
        sortedNotifications[notificationDate]++;
      } else {
        sortedNotifications[notificationDate] = 1;
      }
      return sortedNotifications;
    },
    {}
  );

  const data = {
    labels: Object.keys(sortedNotifications),
    datasets: [
      {
        label: "Number of events",
        data: Object.values(sortedNotifications),
        fill: false,
        backgroundColor: "rgb(255, 99, 132)",
        borderColor: "rgba(255, 99, 132, 0.2)",
      },
    ],
  };

  const options = {
    scales: {
      y: {
        beginAtZero: true,
      },
    },
  };
  return (
    <>
      <div className="header">
        <h1 className="title">Event Timeline</h1>
      </div>
      <Line data={data} options={options} height={500} width={1500} />
    </>
  );
};

export default Timeline;

このコンポーネントは、並べ替え機能がこれまでとは異なります。 初期オブジェクトであらゆる日付を指定することは不可能なので、まず空のオブジェクトを作成します。 次に、通知ごとに、ある日付のキーがすでに存在するかチェックします。 キーが存在する場合は日付の値を 1 増やし、存在しない場合は日付の値を 1 に設定しています。

最後に、グリッド内にすべてのグラフをレンダリングするコンポーネントを構築しましょう。 components フォルダー内に ChartView.jsx という名前のファイルを作成して、次のコードを入力します。

import React from "react";
import StatusDistribution from "./StatusDistribution";
import { Col, Row } from "antd";
import TypeDistribution from "./TypeDistribution";
import Timeline from "./Timeline";

const ChartView = ({ notifications }) => {
  return (
    <>
      <Timeline notifications={notifications} />
      <Row style={{ marginTop: "30px" }} gutter={96}>
        <Col>
          <StatusDistribution notifications={notifications} />
        </Col>
        <Col offset={6}>
          <TypeDistribution notifications={notifications} />
        </Col>
      </Row>
    </>
  );
};

export default ChartView;

このコンポーネントは、タイムライン折れ線グラフの下に、ステータス分布の円グラフとタイプ分布の棒グラフを横に並べて表示します。

ここでの通知の渡し方は、prop drilling (プロパティのバケツリレー) と呼ばれるものです。 一般には避けるべき手法ですが、 チュートリアルを簡略化するためのものだとご了承ください。 本番のアプリを実装する場合には、適切な state (ステート) 管理を実装することをおすすめします。

ダッシュボードを構築する

子コンポーネントがすべて揃ったところで、src/App.js を次のように編集しましょう。

import "./App.css";
import { makeGETRequest } from "./utility/Api";
import { useEffect, useState } from "react";
import { Card, Col, Row, Switch } from "antd";
import TableView from "./components/TableView";
import ChartView from "./components/ChartView";

const App = () => {
  const [notifications, setNotifications] = useState([]);
  const [showTableView, setShowTableView] = useState(false);

  useEffect(() => {
    makeGETRequest("http://127.0.0.1:8000/api/circleci").then((response) => {
      setNotifications(response);
      console.log(response);
    });
  }, []);

  const handleSwitchValueChange = () => {
    setShowTableView((showTableView) => !showTableView);
  };

  return (
    <Card style={{ margin: "2%" }}>
      <Row style={{ marginBottom: "10px" }}>
        <Col span={6} offset={18}>
          Show Data as Table
          <Switch checked={showTableView} onChange={handleSwitchValueChange} />
        </Col>
      </Row>
      {showTableView ? (
        <TableView notifications={notifications} />
      ) : (
        <ChartView notifications={notifications} />
      )}
    </Card>
  );
};

export default App;

このコンポーネントでは、まず useEffect フックで、API から通知を受け取り、setNotifications 関数により state として保存します。 次に、showTableView state 変数の切り替えを処理する関数を宣言しています。この関数により、データの表示先を表とグラフのどちらにするかを決定します。

ビューを切り替えるために、Switch をレンダリングして、showTableView と handleSwitchValueChange の値にプロパティとして渡します。

最後に、showTableView の値に応じて、TableView と ChartView のいずれかのコンポーネントをレンダリングします。

テーブル ビュー

ダッシュボードのテーブル ビュー

グラフ ビュー

ダッシュボードのグラフ ビュー

おわりに

このチュートリアルでは、API からパイプライン イベントを取得して可視化する React ダッシュボードの構築方法を学びました。 データをグラフとして可視化する手法には、CI/CD パイプラインの概要を把握しやすくなるだけでなく、どのようなサイズのデータセットも簡単に理解できるというメリットもあります。 今回は実装しませんでしたが、さらなる分析のために、スプレッドシートへのデータ エクスポート機能を組み込むこともおすすめします。 ぜひ、サンプル プロジェクトをチームで共有し、知見を広げてみてください!

本記事で使用したコードは、GitHub から取得できます。また、CircleCI Webhook についてのドキュメントは、こちらでご覧いただけます。