A new wallboard adventure!
During a conversation with our lead front end dev, we got chatting about what new technologies he was using on up-and-coming projects. One of those technologies was VueJS. VueJS is a progressive javascript framework for building snappy/responsive UI's and single page applications. I was intrigued, and set about researching this.
Front-End development is one of my hobbies, that I generally don't get to do during my day-to-day job (being a back-end developer, and all!). I needed a project to work on in my own time to work towards. One thing that drew me to Vue, is the fact that each component can contain the template, the script and the css.. making it truly self-contained. I'm not sure at the moment how I feel about having the CSS embedded into the component, but it's nice to know it's available.
Enter Wallboard v2.
I'd previously developed a wallboard for a previous company, so I figured I could do something like that with Vue.
We have a multitude of different things that could potentially be deplayed here, such as current builds, pull requests etc. We also monitor our live sites with Pingdom. Perfect.
Our Source control of choice is BitBucket, so after researching the BB Api, I figured that I could use that to get some of the information I wanted. Pingdom also has an API you can use for their services!
Upon realising that both BitBucket and Pingdom supported WebHooks, this gave me an idea to use those, as I'd not done anything with those before. The previous iteration of the wallboard polled an endpoint every x minutes.. not really that efficient. WebHooks push data to you, when it's needed.
Both Bitbucket and Pingdom have the ability to send a WebHook notification to an endpoint when something of interest happens, so I broke out my favourite editor (Visual Studio), and started planning what my endpoint services were to do.
I created a .Net Core 2.0 application that would server as the intermediary between the messages from BitBucket and Pingdom, and my Front-End view (get it?!).
This contained various controller endpoints that would accept the object from BB and Pingdom, then push it out via a WebSocket to any listening devices.
For instance:
/// <summary>
/// Created Pull Request
/// </summary>
/// <param name="pullRequest"></param>
/// <returns></returns>
[HttpPost]
[SwaggerOperation("Created Pull Request")]
[Route("CreatePullRequest")]
public IActionResult CreatePullRequest([FromBody]object pullRequest)
{
CreateWebSocketResponse("PullRequestCreated", pullRequest);
return Ok();
}
The CreatePullRequest endpoint was specified in BitBucket as a webhook, so that when a pull-request was created from a developer for peer-review / code-merge, this would fire.
This in turn would call
private void CreateWebSocketResponse(string action, object pullRequest)
{
WebSocketResponse wsr = new WebSocketResponse();
wsr.Event = action;
wsr.Payload = pullRequest;
string retJson = JsonConvert.SerializeObject(wsr);
serv.SendMessageToAllAsync(retJson);
}
Unfortunately, for webhooks to work properly, they need to be on a publicly-accessible. So a quick publish to Azure on their free tier for testing purposes, and away we go!
Using either the Swagger documentation (that I use for all my Web API needs), or the ever-trusty Postman app, I can begin faking some responses before I plug it all in to the relevant services.
A few more tweaks, and a few more endpoints created, and the back-end is done.
Onto the part that I was actually wanting to do! - The Front End with Vue.
(Now, granted, this might not be the greatest vue layout, but I'm still learning this!)
Vue
<template>
<div id="app">
<div class="grid-container">
<div class="grid-x grid-margin-x grid-margin-y">
<div class="cell large-12 header">
Logo <Clock />
</div>
</div>
<div class="grid-x grid-margin-x grid-margin-y">
<div class="cell large-6">
<Bitbucket webhook="ws://codeurl.azurewebsites.net/ws" />
</div>
<div class="cell large-6 ">
<Dynatrace webhook="ws://codeurl.azurewebsites.net/ws" />
</div>
</div>
<div class="grid-x grid-margin-x grid-margin-y">
<div class="cell large-6">
<VSTS webhook="ws://codeurl.azurewebsites.net/ws" />
</div>
</div>
<Pingdom webhook="ws://codeurl.azurewebsites.net/ws" />
</div>
</div>
</template>
<script>
import VSTS from './components/VSTS.vue'
import Pingdom from './components/Pingdom.vue'
import Clock from './components/Clock.vue'
import Bitbucket from './components/Bitbucket.vue'
import Dynatrace from './components/Dynatrace.vue'
export default {
name: 'app',
components: {
VSTS,
Pingdom,
Clock,
Bitbucket,
Dynatrace
}
}
</script>
<style>
body, html {
height: 100%;
overflow: hidden;
}
body {
background: #222426; /* Old browsers */
background: -moz-linear-gradient(top, #222426 0%, #000000 100%); /* FF3.6-15 */
background: -webkit-linear-gradient(top, #222426 0%,#000000 100%); /* Chrome10-25,Safari5.1-6 */
background: linear-gradient(to bottom, #222426 0%,#000000 100%); /* W3C, IE10+, FF16+, Chrome26+, Opera12+, Safari7+ */
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#222426', endColorstr='#000000',GradientType=0 ); /* IE6-9 */
color: #a0a0a0;
}
#app {
}
#app .grid-container {
}
.header {
border-bottom: 1px solid #a0a0a0;
color: #a0a0a0;
font-size: 30px;
}
.sidebar {
border-right: 1px solid #a0a0a0;
}
</style>
As you can see, I have a few Components that will do the actual listening for the websocket data.
The code for the Bitbucket component, looks like this:
<template>
<div class="" v-bind:class="{'align-center align-middle': bitbucketlist.length == 0 }">
<div v-if="bitbucketlist.length == 0">
<div class="nocontent">
<h1>{{ nodata }}</h1>
</div>
</div>
<div v-if="bitbucketlist.length > 0" class="pull-request-overview">
<span id="count">15</span>
Open Pull requests
</div>
<div v-for="item in bitbucketlist" class="cell large-4 pull-request" v-bind:id="item.check_id" v-bind:key="item.check_id">
<h3>{{ item.pullrequest.description}} </h3>
<span class="source">{{ item.pullrequest.source.branch.name}}</span> to
<span class="dest">{{ item.pullrequest.destination.branch.name }}</span>
<div class="author">
<img v-bind:src="item.pullrequest.author.links.avatar.href" width="50" height="50"/>
{{ item.pullrequest.author.display_name}}
</div>
</div>
</div>
</template>
<script>
var bitbucketlist = [];
var $isModalVisible = false;
export default {
name: 'Bitbucket',
methods: {
},
data: function () {
return {
nodata: "Waiting for Pull Requests",
bitbucketlist
}
},
props: {
webhook: String
},
created: function() {
//var uri = "ws://codeurl.azurewebsites.net/ws";
var socket = new FancyWebSocket(this.webhook);
socket.bind('PullRequestCreated', function (data) {
bitbucketlist.push(data);
data = {};
});
}
}
</script>
<style scoped>
.pull-request {
border: 1px solid red;
}
</style>
In the Created() method, it sets up the websocket, and binds the 'PullRequestCreated' action from the message to the inline function.
This then adds that new PR to an array of PR's. Because VueJS is two-way data-bound, I don't need to go anything to get it to render out.
I also hooked the Pingdom API up in the same way, but this time with a full-screen modal that displayed on-top of the other data when we recieved a "site down" notification.
<template>
<div class="grid-y" v-bind:class="{'grid-frame align-center align-middle': pingdomlist.length == 0 }">
<transition name="popin">
<modal v-if="pingdomlist.length">
<div slot="header">
</div>
<div slot="footer"></div>
<transition-group slot="body" name="popin" class="grid-x grid-margin-x grid-margin-y" tag="div">
<div v-if="pingdomlist.length <= 6" v-for="item in pingdomlist" :key="item.check_id" class="cell large-12 pingdom-item" v-bind:id="item.check_id">
<div class="grid-x grid-padding-x" v-bind:class="item.current_state">
<div class="cell large-6">
<h2>{{ item.check_name }}</h2>
</div>
<div class="cell large-2" style="text-align: center">
<h2>{{ getDuration(item.state_changed_utc_time) }}</h2>
</div>
<div class="cell large-4 align-centre" style="text-align: center">
<h2 v-bind:class="item.current_state">{{ item.current_state}}</h2>
</div>
</div>
</div>
<div v-if="pingdomlist.length > 6" v-for="item in pingdomlist" :key="item.check_id" class="cell large-12 pingdom-item" v-bind:id="item.check_id">
<div class="grid-x grid-padding-x cell large-6" v-bind:class="item.current_state">
<div class="cell large-6">
<h2>{{ item.check_name }}</h2>
</div>
<div class="cell large-2" style="text-align: center">
<h2>{{ getDuration(item.state_changed_utc_time) }}</h2>
</div>
<div class="cell large-4 align-centre" style="text-align: center">
<h2 v-bind:class="item.current_state">{{ item.current_state}}</h2>
</div>
</div>
</div>
</transition-group>
</modal>
</transition>
</div>
</template>
<script>
import modal from './Modal.vue';
import moment from 'moment';
var pingdomlist = [];
var isModalVisible = false;
var $that = this;
export default {
name: 'Pingdom',
components: {
modal,
},
methods: {
showModal: function() {
isModalVisible = true;
console.log("showModal");
},
closeModal: function() {
isModalVisible = false;
},
getDuration: function (start) {
var startTime = moment(start);
var end = moment();
var diff = end.diff(startTime);
return moment.utc(diff).format("HH:mm:ss");
},
},
data: function () {
return {
isModalVisible,
nodata: "Waiting for Pingdom Info",
pingdomlist
}
},
props: {
webhook: String
},
created: function() {
//var uri = "ws://rbplayground.azurewebsites.net/ws";
var $this = this;
var socket = new FancyWebSocket(this.webhook);
socket.bind('pingdom', function (data) {
var currentState = data.current_state;
var index = pingdomlist.map(item => item.check_id).indexOf(data.check_id);
switch (currentState) {
case "UP":
pingdomlist.splice(index, 1);
break;
case "DOWN":
case "UNCONFIRMED_DOWN":
case "UNKNOWN":
if (index < 0) {
pingdomlist.push(data);
}
if (index > -1) {
$this.$set(pingdomlist, index, data);
}
break;
}
isModalVisible = pingdomlist.length > 0 ? true : false;
console.log("pingdomListCount = ", pingdomlist.length);
console.log("modal visible = ", isModalVisible);
data = {};
});
}
}
</script>
<style scoped>
.pingdom-item {
}
/*.pingdom-item > .DOWN {
border: 5px solid red;
}
.pingdom-item > .UNCONFIRMED {
border: 5px solid orange;
}*/
h2.DOWN {
color: red;
}
h2.UNCONFIRMED {
color: orange;
}
.popin-enter-active {
animation: fadeIn .5s;
}
.popin-leave-active {
animation: fadeOut .5s;
}
@keyframes fadeIn {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes fadeOut {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
</style>
The FE for this was done using Zurb Foundation 6. After I'd completed this, I discovered the CSS Grid, which in hindsight, would have served me better, without having to import css and javascript from a framework. Oh well, next time, CSS Grid all the way!
Footnote:
Initiallty, this was built with having integrations with our internal systems such as Jenkins, SonarQube, Jira etc. We migrated away from those to take advantage of the Azure platform, and their CI/CD solutions, so further modifications were done to hook into the CI/CD pipeline to get build status and deployment updates as well. This worked well for our testing team, who could see when a new component or fix was either building, or had completed for them to test.