This is a pure JavaScript applet for which no server is required. It displays the recent Git commits (change log), open issues, and closed issues for a public GitHub project. It was written for a client so they could track progress and status on a website built for them (and which is a private repo).
Introduction
I decided to have a little fun with a pure JavaScript applet--no server required. This applet displays the recent:
- Git commits (change log)
- Open issues
- Closed issues
for a public GitHub project. I ended up writing this for a client so they could track progress and status on a website I'm building for them (and which is a private repo). It turned out to be a fun little stand-alone HTML document.
Screenshots
The screenshots below are taken from the commits and issues for Joyent's node.js. You can put in any public repo name to view the logs, the applet (as you'll see in the JavaScript) defaults to this project.
Change Log
Here's a fragment showing the change log. Git returns the last 30 commits (not all shown here).
I'm only really interested in two columns, but there's a wealth of information that can be added to the commit history.
Open Issues
Git also returns 30 open/closed issues.
Again, there's more information that could be displayed.
Closed Issues
Obviously, identical (except for the content) to Open Issues. Notice the mouse-over tooltip box, available also in Open Issues.
The Code
The entire applet resides in a single HTML file. We'll look at the various pieces next.
Third Party Scripts and Styles
Because it made life simpler, I'm using the following third party components:
- jQuery
- Knockout-js
- Bootstrap
- Moment-js
<script type="text/javascript" src="http://code.jquery.com/jquery-1.11.2.min.js"></script>
<script type="text/javascript" src="http://knockoutjs.com/downloads/knockout-3.2.0.js">
</script>
<script type="text/javascript"
src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/js/bootstrap.min.js"></script>
<script type="text/javascript"
src ="http://momentjs.com/downloads/moment-with-locales.min.js"></script>
<link rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css">
- jQuery is used primarily for the AJAX calls.
- Knockout is used for the minimal data binding.
- Bootstrap is used primarily for the tabs.
- Moment-js is used for formatting the date and time.
Styles
Not much needs to be said about the styles other than that I've made some minor attempts at improving the spacing of controls and columns and color-banded the rows:
<style>
.log tr:nth-child(even) {background: rgb(230, 255, 230)}
.log tr:nth-child(odd) {background: #FFF}
input {padding-left: 5px}
.container {padding-top: 20px; padding-bottom: 20px; margin-left: 10px;}
.datefield {padding-left:20px; width:170px}
.nav-tabs, .tab-content {padding-left:20px}
.data-list th {padding-left:20px}
.data-list td {padding-left:20px}
.data-list td:last-child {padding-right:20px}
#gitRepoUrl {width:25%}
#btnGo {padding-left:10px}
</style>
JavaScript
Document Ready
Here, we create a global variable for the Knockout view-model, initialize it, and wait for a click on the Go button:
var viewModel
$(document).ready(function () {
viewModel = dataBind()
$('#btnGo').on('click', function() {
getCommits()
getOpenIssues()
getClosedIssues()
})
})
Initializing the View Model
The view model is really quite simple, consisting of databinding the URL input and the three tables:
function dataBind() {
var vm = new function() {
this.gitRepoUrl = ko.observable("joyent/node")
this.commits = ko.observable([])
this.openIssues = ko.observable([])
this.closedIssues = ko.observable([])
}
ko.applyBindings(vm);
return vm
}
Get Commits and Issues
When the "Go" button is clicked, we issue three separate AJAX requests to GitHub:
function getCommits() {
$.ajax({
dataType: 'json',
url: 'https://api.github.com/repos/' + viewModel.gitRepoUrl() + '/commits',
success: showCommits,
error: function() {
viewModel.commits([])
viewModel.openIssues([])
viewModel.closedIssues([])
alert("Unable to connect to repo '" + viewModel.gitRepoUrl() +"'")
}
})
}
Note that an error is displayed only for getting commits -- if getting commits fails, the issue gets are expected to fail as well, so they don't have a specific error handler.
function getOpenIssues() {
$.ajax({
dataType: 'json',
url: 'https://api.github.com/repos/' + viewModel.gitRepoUrl() + '/issues?state=open',
success: showOpenIssues
})
}
function getClosedIssues() {
$.ajax({
dataType: 'json',
url: 'https://api.github.com/repos/' + viewModel.gitRepoUrl() + '/issues?state=closed',
success: showClosedIssues
})
}
Updating the View-Model
For each successful AJAX response, the view-model is updated, which updates the page:
function showCommits(data) {
viewModel.commits(data)
formatDates('.commit-datefield')
}
function showOpenIssues(data) {
viewModel.openIssues(data)
formatDates('.open-issues-datefield')
}
function showClosedIssues(data) {
viewModel.closedIssues(data)
formatDates('.closed-issues-datefield')
}
Format Date/Time
The date/time is formatted to the 'en
' local standard:
function formatDates(classId) {
moment.locale('en')
$(classId).each(function (index, dateElem) {
var formatted = moment(dateElem.textContent).format('lll')
dateElem.textContent = formatted;
})
}
The HTML
Last but not least is the HTML:
<div class="container">
<label>Git Repo Name:</label>
<input type='text' id='gitRepoUrl' data-bind='value: gitRepoUrl'/>
<input type='button' id='btnGo' value='Go'/>
</div>
<div id="content">
<ul id="tabs" class="nav nav-tabs" data-tabs="tabs">
<li class="active"><a href="#changeLog" data-toggle="tab">Change Log</a></li>
<li><a href="#openIssues" data-toggle="tab">Open Issues</a></li>
<li><a href="#closedIssues" data-toggle="tab">Closed Issues</a></li>
</ul>
<div id="my-tab-content" class="tab-content">
<div class="tab-pane active" id="changeLog">
<h1>Change Log</h1>
<table class="data-list">
<thead>
<tr>
<th>Date</th>
<th>Change</th>
</tr>
</thead>
<tbody class='log' data-bind="foreach: commits">
<tr>
<td class='commit-datefield datefield' data-bind="text: commit.author.date"></td>
<td data-bind="text: commit.message"></td>
</tr>
</tbody>
</table>
</div>
<div class="tab-pane" id="openIssues">
<h1>Open Issues</h1>
<table class="data-list">
<thead>
<tr>
<th>Number</th>
<th>Date</th>
<th>Issue</th>
</tr>
</thead>
<tbody class='log' data-bind="foreach: openIssues">
<tr>
<td data-bind="text: number"></td>
<td class='open-issues-datefield datefield' data-bind="text: created_at"></td>
<td data-bind="text: title, attr: { title: body }"></td>
</tr>
</tbody>
</table>
</div>
<div class="tab-pane" id="closedIssues">
<h1>Closed Issues</h1>
<table class="data-list">
<thead>
<tr>
<th>Number</th>
<th>Date</th>
<th>Issue</th>
</tr>
</thead>
<tbody class='log' data-bind="foreach: closedIssues">
<tr>
<td data-bind="text: number"></td>
<td class='closed-issues-datefield datefield' data-bind="text: created_at"></td>
<td data-bind="text: title, attr: { title: body }"></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
If You Need Authentication
If you're accessing a private repo, you can pass in your authentication access token as an Authorization header, for example:
function getCommits() {
$.ajax({
dataType: 'json',
url: 'https://api.github.com/repos/' + viewModel.gitRepoUrl() + '/commits',
headers: {
"Authorization": "token " + token
},
success: showCommits
})
}
The problem with this approach is that anyone can access your token through the browser's debugging facilities. In other words, security should not be handled by JavaScript - you certainly don't want to hardcode your token, but rather obtain it from the server. Technically, if the ability to view commits and issues is itself an authorized route, you at least know that the people viewing those pages are authorized to do so, but this is probably a weak defence. Therefore, at the end of the day, for private repos that require an access token, you should probably revert to a client-server hybrid, in which the server passes down the pre-acquired commits and issues.
Conclusion
This was a fun little applet to put together and I hope that it might be of some use to others!
History
- 7th May, 2015: Initial version