React - Jenkins Wallboard (2/2)

Simple React Wallboard for Jenkins engined by a Node Server

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

graph LR; A(React App) -.- B(Node Proxy) B -.- C(Fake Jenkins Server)

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

  1. run the fake jenkins server node jenkins (port 3999)
  2. run the node proxy server node server (default port 3001)
  3. 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

  1. build the react client thanks to webpack
  2. restart the node proxy server
  3. 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

  1. build the react client thanks to webpack
  2. restart the node proxy server
  3. in the browser, enter http://localhost:3001/ in the address bar

You should have the following result after waiting several seconds.

  • in green, tests or builds succeeded
  • in red, tests or builds failed
  • in black, running builds
comments powered by Disqus