Following the previous post on the Jenkins Wallboard, this one finally describes how to create our React client.
Source code
The whole source code of this article is in my GitLab repository.
As a reminder
Our architecture
Our project structure at this point
└── node-react-jenkins-wallboard
├── client
├── server
| ├── node_modules
| ├── src
| | ├── config.js
| | ├── Kpi.js
| | ├── Toolbox.js
| | └── Usage.js
| ├── package.json
| └── server.js
└── jenkins
├── node_modules
├── package.json
└── jenkins.js
Install REACT and linked tools
In client
folder, do the following
NPM init
C:\node-react-jenkins-wallboard\client> npm init -y
REACT modules
We install React
C:\node-react-jenkins-wallboard\client> npm install react --save
...
C:\node-react-jenkins-wallboard\client> npm install react-dom --save
...
WEBPACK and BABEL
We use Webpack associated to Babel to generate our webapp and to transpile JSX and ES6 file to javascript interpreted by browsers.
To avoid problems of path we will install webpack
and webpack-cli
(command prompt, mandatory because we use a webpack version >= 4) in global way.
C:\node-react-jenkins-wallboard\client> npm install webpack -g
...
C:\node-react-jenkins-wallboard\client> npm install webpack-cli -g
...
C:\node-react-jenkins-wallboard\client> npm install babel-core --save-dev
...
C:\node-react-jenkins-wallboard\client> npm install babel-loader --save-dev
...
C:\node-react-jenkins-wallboard\client> npm install babel-preset-es2015 --save-dev
...
C:\node-react-jenkins-wallboard\client> npm install babel-preset-react --save-dev
...
Package.json at this point
It’s interesting to have a look at it because we can see the modules versions we use for this project
{
"name": "client",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"react": "^16.3.2",
"react-dom": "^16.3.2"
},
"devDependencies": {
"babel-core": "^6.26.0",
"babel-loader": "^7.1.4",
"babel-preset-es2015": "^6.24.1",
"babel-preset-react": "^6.24.1"
}
}
Last node proxy update
As seen in part one of this post we wish our node proxy serves the React app.
We will see below that all our react files will be put in the webapp
folder in our server
directory.
For this we have to
Create REACT output directory
We create the webapp
folder in the server
directory. As seen below, this new directory is where webpack will generate the app bundle.
So our directory structure is now like this
└── node-react-jenkins-wallboard
├── client
| ├── node_modules
| └── package.json
├── server
| ├── webapp
| ├── node_modules
| ├── src
| | ├── config.js
| | ├── Kpi.js
| | ├── Toolbox.js
| | └── Usage.js
| ├── package.json
| └── server.js
└── jenkins
├── node_modules
├── package.json
└── jenkins.js
Update server.js file
Now, we have to configure express for rendering static files (index.html and other js files).
Let’s have a look at the new server.js
file content
'use strict';
/* globals */
global.__BASE = __dirname;
/* we will use path.join for our requires to be posix and windows compatible */
var path = require('path');
/* usage verification */
var { Usage } = require(path.join((__BASE + '/src/Usage')));
var myUsage = new Usage();
myUsage.check();
/* object which retrieves jenkins kpis */
var { Kpi } = require(path.join((__BASE + '/src/Kpi')));
var myKpi = new Kpi();
/* run the cache update every 15 seconds */
function refreshCache() {
myKpi.refresh();
}
setInterval(refreshCache, 15000);
/* */
var express = require('express');
var app = express();
/* entry point for static files of the REACT app */
/* so a request on http://host:port will return the REACT app */
app.use(express.static(path.join(__BASE, 'webapp')));
/* get jenkins kpis */
app.get('/kpis', function (req, res) {
res.status(200).json(myKpi);
return;
});
/* */
app.listen(myUsage.proxyPort, function () {
console.log('Proxy server started on port [%d]', myUsage.proxyPort);
});
First version of our REACT App
In the client
directory
Create index.hmtl
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Jenkins Wallboard</title>
</head>
<body>
<div id="app"></div>
<script type="text/javascript" src="bundle.js"></script>
</body>
</html>
Create App.js
Here is our first REACT component, a very simple one, it’s our main single page.
It’s a JSX class even if the file suffix is .js
(see also the webpack configuration file below).
import React from "react";
class App extends React.Component {
render() {
return (
<div>
<center>
<font size="6">Jenkins Wallboard</font>
</center>
</div>
);
}
}
export default App;
Create main.js
import React from "react";
import ReactDom from "react-dom";
import App from "./App";
ReactDom.render(
<App />,
document.getElementById('app')
);
Create webpack.config.js
We use Webpack to transpile JSX and ES6 classes in ES5 javascript and to put the created bundle.js
in our output directory server/webapp
we have created above.
var path = require('path');
module.exports = {
mode: 'none',
entry: [
path.resolve(__dirname, "./main.js")
],
output: {
path: path.resolve(__dirname, "../server/webapp"),
filename: "bundle.js"
},
module: {
rules: [
/* javascript */
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['es2015', 'react']
}
}
},
/* jsx */
{
test: /\.jsx$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['es2015', 'react']
}
}
}
]
},
/* our './myComponent' requires or imports are equivalent to 'myComponent.jsx' */
resolve: {
extensions: ['*', '.js', '.jsx']
}
}
Build
Last setup
Copy the above index.html
in the server/webapp
directory.
Build
c:\node-react-jenkins-wallboard\client>webpack
Hash: 1a324766ac97da21554f
Version: webpack 4.6.0
Time: 9537ms
Built at: 2018-04-27 16:11:49
Asset Size Chunks Chunk Names
bundle.js 666 KiB 0 [emitted] main
Entrypoint main = bundle.js
[0] multi ./main.js 28 bytes {0} [built]
[1] ./main.js 466 bytes {0} [built]
[23] ./App.js 2.36 KiB {0} [built]
+ 21 hidden modules
Our structure directory at this point
└── node-react-jenkins-wallboard
├── client
| ├── node_modules
| ├── App.js
| ├── index.html
| ├── main.js
| ├── package.json
| └── webpack.config.js
├── server
| ├── webapp
| | ├── bundle.js
| | └── index.html
| ├── node_modules
| ├── src
| | ├── config.js
| | ├── Kpi.js
| | ├── Toolbox.js
| | └── Usage.js
| ├── package.json
| └── server.js
└── jenkins
├── node_modules
├── package.json
└── jenkins.js
Test
- run the fake jenkins server
node jenkins
(port 3999) - run the node proxy server
node server
(default port 3001) - in the browser, enter
http://localhost:3001/
in the address bar
You should have the following result
Second version of our REACT App
We add now to our App
class the way to request the kpis we want to display. Let’s begin by the tests results named metrics
.
Modify App.js
import React from "react";
class App extends React.Component {
constructor() {
super();
this.timer;
this.state = { 'data': {} };
}
getData() {
let data = {};
let xhr = new XMLHttpRequest();
xhr.open('GET', encodeURI('/kpis'));
xhr.responseType = 'json';
xhr.onload = () => {
if (xhr.status === 200) { data = xhr.response; }
this.setState({ 'data': data });
};
xhr.send();
}
componentDidMount() {
this.timer = setInterval(() => this.getData(), 5000);
}
componentWillUnmount() {
clearInterval(this.timer);
}
render() {
let tests = [];
if (this.state.data.metrics) {
for (let i = 0; i < this.state.data.metrics.length; i++) {
let style = { 'color': 'green' };
let count = this.state.data.metrics[i].metrics.totalCount;
if (this.state.data.metrics[i].metrics.failCount > 0) {
style = { 'color': 'red' };
count = this.state.data.metrics[i].metrics.failCount;
}
tests.push(<div key={i}>
<font size="6">{this.state.data.metrics[i].project} / </font>
<font size="3">{this.state.data.metrics[i].job} / </font>
<font size="9" style={style}>{count}</font>
</div>);
}
}
return (
<div>
<div>
<center>
<font size="9">Jenkins Wallboard</font>
</center>
</div>
<div>
{tests}
</div>
</div>
);
}
}
export default App;
Note that we used arrow functions to automatically bind this
.
As shown below and just for information we could have used classical functions with bind
.
onload
xhr.onload = function () {
if (xhr.status === 200) { data = xhr.response; }
this.setState({ 'data': data });
}.bind(this);
componentdidMount
componentDidMount() {
this.timer = setInterval(this.getData.bind(this), 5000);
}
Test
- build the react client thanks to webpack
- restart the node proxy server
- in the browser, enter
http://localhost:3001/
in the address bar
You should have the following result after waiting several seconds (depending on the timer values in client and node proxy).
Final version
Now let’s add the builds results.
Modify App.js
import React from "react";
class App extends React.Component {
constructor() {
super();
this.timer;
this.state = { 'data': {} };
}
getData() {
let data = {};
let xhr = new XMLHttpRequest();
xhr.open('GET', encodeURI('/kpis'));
xhr.responseType = 'json';
xhr.onload = () => {
if (xhr.status === 200) { data = xhr.response; }
this.setState({ 'data': data });
};
xhr.send();
}
componentDidMount() {
this.timer = setInterval(() => this.getData(), 5000);
}
componentWillUnmount() {
clearInterval(this.timer);
}
render() {
let tests = [];
if (this.state.data.metrics) {
for (let i = 0; i < this.state.data.metrics.length; i++) {
let style = { 'color': 'green' };
let count = this.state.data.metrics[i].metrics.totalCount;
if (this.state.data.metrics[i].metrics.failCount > 0) {
style = { 'color': 'red' };
count = this.state.data.metrics[i].metrics.failCount;
}
tests.push(<div key={i} style={style}>
<div style={{ fontSize: "3em" }}>{this.state.data.metrics[i].project}</div>
<div style={{ fontSize: "2em" }}>{this.state.data.metrics[i].job}</div>
<div style={{ fontSize: "3em" }}>{count}</div>
</div>);
}
}
let builds = [];
if (this.state.data.builds) {
for (let i = 0; i < this.state.data.builds.length; i++) {
for (let j = 0; j < this.state.data.builds[i].builds.length; j++) {
let style;
switch (this.state.data.builds[i].builds[j].status) {
case 'success':
style = { 'color': 'green' };
break;
case 'error':
style = { 'color': 'red' };
break;
case 'running':
style = { 'color': 'black' };
break;
default:
}
builds.push(<div key={'bc'.concat(i)} style={style}>
<div style={{ fontSize: "3em" }}>{this.state.data.builds[i].project}</div>
<div style={{ fontSize: "2em" }}>
{this.state.data.builds[i].type} : {this.state.data.builds[i].builds[j].view}
</div>
<div style={{ fontSize: "1em" }}>({this.state.data.builds[i].builds[j].builds.join()})</div>
</div>);
}
}
}
return (
<div>
<div style={{ fontSize: "6em" }}>
<center>Jenkins Wallboard</center>
</div>
<div>
<div style={{ fontSize: "4em" }}>TESTS</div>
{tests}
</div>
<div>
<div style={{ fontSize: "4em" }}>BUILDS</div>
{builds}
</div>
</div>
);
}
}
export default App;
Test
- build the react client thanks to webpack
- restart the node proxy server
- in the browser, enter
http://localhost:3001/
in the address bar
You should have the following result after waiting several seconds.