Paul Hammant's Blog: Hacking Confluence With Javascript
Hacking in a good way, to make a little app. Experimenting with what an on-call app would look like inside Confluence.
I’ve chosen some contrived things that might need emergency replenishment, and teams that can handle specific types of replenishment in an ‘incident’ 24x7:
Main On-Call Page
Page showing three critical types of things that require on-call rotations in order to replenish when the run low:
Note the three teams in the links above s: TeamOne, A-Team, and Resevior+Dogs. By the way, nobody wants to be in the b-team, team2, etc. There was a study years ago about the advantage from the lofty team name, but my google fu is letting me down.
Child Pages
The three child pages referred to the table above.
Each team gets to maintain their own small page. They could even ‘watch’ it and get email updates as it is changed. They probably would delete entries from that past, so the page stays small.
JavaScript
Some JavaScript in the page below thw table, via Confluence HTML-Include plugin:
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.13.0/moment.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment-timezone/0.5.4/moment-timezone-with-data-2010-2020.js"></script>
<span><button onclick="prevDay()">< Previous Day</button><button onclick="nextDay()">Next Day ></button></span>
<br/>
<h1 id="ocHeadline">xx</h1>
<script type="text/javascript">
// Columns are zero indexed, of course
var TEAMNAME_COLUMN = 1;
var ONCALLEE_COLUMN = 2;
var TEAM_COLHDG = "Team";
function findOncallee(who, dict, onCallDay, team, pOrS) {
var MDYY = onCallDay.format("M/D/YY");
result = who[dict[MDYY]];
if (result === undefined) {
result = who[dict["OTHER DATES"]];
}
if (result === undefined) {
console.log(pOrS + " on-callee can't be found for " + MDYY + " for " + team);
result = {
"who": "unknown", "details": ""
};
}
return result;
}
var onCallDay = moment.utc().tz("America/New_York").subtract(7, 'hour');
prevDay = function() {
onCallDay = onCallDay.subtract(1, "day");
console.log("prev day");
renderDay();
};
nextDay = function() {
onCallDay = onCallDay.add(1, "day");
console.log("next day" );
renderDay();
};
function renderDay() {
var onCallDayMDYY = onCallDay.format("M/D/YY");
var endOfOnCallShift = onCallDay.clone();
endOfOnCallShift.add(1, "day");
endOfOnCallShift.set({
'hour': 6,
'minute': 59,
'second': 59
});
var onCallDayEndsMDYYHmmssa = endOfOnCallShift.format("M/D/YY H:mm:ss");
console.log("onCallDay: " + onCallDayMDYY);
console.log("onCallDayEnds: " + onCallDayEndsMDYYHmmssa);
AJS.$('#ocHeadline').html("On-Call for " + onCallDayMDYY + ", ending " + onCallDayEndsMDYYHmmssa + " ET");
AJS.$('#main-content').find("tbody tr").each(function (index, value) {
// #1 (zero rooted) is the Name of the Child Page (Routing Group)
var team = AJS.$(value.children[TEAMNAME_COLUMN]).text().trim().replace(" ", "+");
if (team !== TEAM_COLHDG) {
request = AJS.$.get(team + '?printable=true');
request.success(function (data) {
var primary = {};
var secondary = {};
var who = {};
var el = AJS.$('<div/>');
var match = data.match(/.*confluenceTable.*/);
var trs = el.append("<div>" + match + "</div>").find("tr");
for (var ix = 0; ix < trs.length; ix++) {
var tr = trs[ix];
var tds = AJS.$("td", tr);
// Two tables here - OnCallee and their cell number
// and On-call dates, the primary, and the secondary oncallee.
if (tds.length > 0) {
var k = AJS.$(tds[0]).text();
var v = AJS.$(tds[1]).text();
var s = k.charAt(0);
if (AJS.$.isNumeric(s) || k.toUpperCase() === "OTHER DATES") {
// On-call dates, the primary, and the secondary oncallee.
primary[k.toUpperCase()] = v;
secondary[k] = AJS.$(tds[2]).text();
} else {
// OnCallee and their cell number
who[k] = {
"who": k,
"details": v
};
// Second entry with first-name only
// hope there's never a team with two Pauls in it (etc)
who[k.split(" ")[0]] = {
"who": k,
"details": v
};
}
}
}
console.log(team + " who: " + JSON.stringify(who));
console.log(team + " when: " + JSON.stringify(primary));
var primaryToday = findOncallee(who, primary, onCallDay, team, "Primary");
var secondaryToday = findOncallee(who, secondary, onCallDay, team, "Secondary");
value.children[ONCALLEE_COLUMN].innerHTML =
"Primary: <strong>" + primaryToday.who + "</strong> " + primaryToday.details + "<br/>"
+ "Secondary: <strong>" + secondaryToday.who + "</strong> " + secondaryToday.details + "";
});
request.error(function (jqXHR, textStatus, errorThrown) {
value.children[ONCALLEE_COLUMN].innerHTML = "<-- Routing group does not have a confluence page. Can't work out primary or secondary without that.";
});
}
});
}
AJS.toInit(function () {
renderDay();
});
</script>
The JavaScript, using the JQuery built in to Confluence, finds the child pages, and plucks a primary and secondary on-callee from the tables, then updates the blank cell in the main page, with data.
Resulting Page
The page even prints with the JavaScript added cells. Don’t expect to “watch” the page though, and get emails from confluence when the child pages change.
What I’d Rather Do
I’d like to use the “Confluence content properties” from the Hosted data storage facility built in to Confluence. There’s no sample app that shows me how though. Until I see a cohesive example, I’m not going really to get it. I sure hope that, in use, it feels like a Firebase built-in to Confluence.
If I had that, I’d make an AngularJS mini-app that would allow confluence users to change whos on call, easily. The calculator from Angular Embedded in Jekyll-Markdown Blog Entries similarly embeds quite easily in Confluence.
Anvil
Then again there is Anvil which has the promise of being an easy internal application development platform, and aimed at quick in-house applications like this.
Updates
June 6th, 2016 - updated to include prev/next button so that preparation and communication for a weekend (etc) can happen in a single go. Teams are going to want to copy from Confluence and paste into email. Why? If you’re solving an incident AND confluence is down, do you want to bring back confluence first, or would you like a copy of who’s on call in email as a backup. Also useful if you’re on foot somewhere.