数据可视化是将大型数据集和指标转化为图表、图形和其他视觉效果的过程。由此产生的数据的可视化表示,使其更容易识别和分享实时趋势,异常值,以及对数据所代表的信息的新见解。使用CircleCIwebhooks,我们可以收集工作流和工作事件的数据。在本教程中,我将带领你完成创建一个基于React的仪表盘的步骤,以可视化这些数据。
前提条件
要跟上本教程,你需要对JavaScript ES6有良好的掌握。你还需要了解一些基本的React概念,如钩子和功能组件。
你将需要在你的工作站上安装这些东西。
- 最新的node.js安装,以及像npm或yarn这样的包管理器
- 你喜欢的代码编辑器
- 一个与您的CircleCI webhook进行通信的API。你可以在这里阅读关于设置一个。
- 确保本教程中涉及的Laravel API CircleCI webhook已经启动并运行。
我们的教程是不分平台的,但使用CircleCI作为例子。如果你没有CircleCI账户,请**在这里注册一个免费账户。**
开始使用
使用这个命令创建一个新的React应用程序。
yarn create react-app circleci_workflow_dashboard
cd circleci_workflow_dashboard
对于这个项目,我们将使用Ant Design来布置UI,使用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 。这个组件将把通知作为一个道具,并把它们呈现在一个表格中,可以根据类型和状态进行过滤。将这段代码添加到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;
该表有六列。主题、承诺人、发生时间、事件类型、事件状态和通知ID。
每一列都由columns 常量中的一个对象表示。在不需要特殊渲染的情况下,title 和dataIndex 足够用于列的声明。title 被用作该列的标题,而dataIndex 条目让antd知道该列被填充了哪个属性。
为了指定要在列中渲染的组件或JSX元素,我们在列的对象表示中添加一个render 键。我们用它来渲染我们先前创建的StatusTag 组件。我们还用它来渲染Happened At 列的一个格式良好的日期,以及作为工作流链接的通知ID。
构建图表组件
在本教程中,我们将在三个图表中渲染数据。
- 饼状图显示事件按状态的分布:成功、失败、错误、取消或未授权。
- 柱状图显示事件按类型的分布情况:工作流或工作
- 显示事件时间线的折线图
为了建立饼状图,在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的日期。
接下来,我们需要建立一个组件,将所有的图表显示在一个网格中。在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;
在这个组件中,我们将状态分布和类型分布与上面的时间线并排呈现。
像我们在这里做的那样传递通知,被称为道具钻取。虽然通常不鼓励这样做,但为了简化教程,我们在这里做了这个。在生产应用中,你应该考虑适当的状态管理实现。
把它放在一起
在所有的子组件就位后,更新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 函数将其保存为状态。然后我们声明一个函数来处理showTableView 状态变量的切换,它决定了数据是以表格还是图表的形式显示。
为了在不同的视图之间切换,我们渲染一个Switch ,并将showTableView 和handleSwitchValueChange 的值作为道具传递给它。
最后,根据showTableView 的值,TableView 或ChartView 组件被渲染。
表视图

图表视图

总结
在本教程中,我们研究了如何建立一个React仪表盘,使用API可视化管道事件。通过在图表中可视化数据,我们可以对我们的管道有一个高层次的了解,也可以对数据集进行理解,无论多大规模。虽然我们没有实现这个功能,但你也可以将数据导出到电子表格中,以便进一步分析。与你的团队分享这个样本项目,并扩大你的学习范围