Author: humbedooh Date: Sun Jun 18 06:59:01 2017 New Revision: 1799040 URL: http://svn.apache.org/viewvc?rev=1799040&view=rev Log: Initial stab at a next gen UI for reporter.a.o. Still much to do...but time to test it on live data.
Added: comdev/reporter.apache.org/trunk/site/css/tabs.css comdev/reporter.apache.org/trunk/site/js/coffee/ comdev/reporter.apache.org/trunk/site/js/coffee/html.coffee comdev/reporter.apache.org/trunk/site/js/coffee/main.coffee comdev/reporter.apache.org/trunk/site/js/coffee/misc.coffee comdev/reporter.apache.org/trunk/site/js/coffee/preload.coffee comdev/reporter.apache.org/trunk/site/js/coffee/tabs.coffee comdev/reporter.apache.org/trunk/site/js/tabs.js comdev/reporter.apache.org/trunk/site/ng.html Added: comdev/reporter.apache.org/trunk/site/css/tabs.css URL: http://svn.apache.org/viewvc/comdev/reporter.apache.org/trunk/site/css/tabs.css?rev=1799040&view=auto ============================================================================== --- comdev/reporter.apache.org/trunk/site/css/tabs.css (added) +++ comdev/reporter.apache.org/trunk/site/css/tabs.css Sun Jun 18 06:59:01 2017 @@ -0,0 +1,171 @@ +html { + height: 100%; +} +html { + background: #645; + font-family: "Helvetica Neue",Roboto,Arial,"Droid Sans",sans-serif; + color: #222; + height: 100%; + margin: 0; + background-repeat: no-repeat; + background-attachment: fixed; + background-size: cover; +} + + +a { + color: #224; +} + +#dialog { + position: relative; + display: inline-block; + background: #eee; + width: 90%; + max-width: 1600px; + min-width: 1200px; + height: 700px; + border-radius: 3px; +} + +.tabs { + border-top-left-radius: 3px; + border-top-right-radius: 3px; + float: left; + height: 36px; + width: 100%; + text-align: center; + background: #645; +} + +.tablink { + background: linear-gradient(to bottom, #eeeeee 0%,#d5d5d5 100%); + color: #555; + padding-top: 10px; + float: left; + height: 26px; + min-width: 110px; + text-align: center; + border-right: 4px solid #645; + box-shadow: inset 0px -1px 2px rgba(0,0,0,0.75); +} + +.tablink:first-child { + border-top-left-radius: 5px; +} + +.tablink:hover:not(.tablink_selected) { + color: #333; + cursor: pointer; + background: #EEC; +} + +.tablink_selected { + border-bottom: none; + box-shadow: none; + cursor: default; + color: #333; + background: #eee; +} + +.bread { + + margin: 0 auto; + padding:8px; + height: 600px; + width: calc(100% - 20px); + overflow: auto; + background: #eee; + text-align: left; +} + +h2 { + text-align: center; + padding-top: 28px; + margin-bottom: 8px; +} + + +.logo { + position: absolute; + top: 20px; + left: calc(50% - 225px); +} + + +/* Snagged from Foundation */ +.sub-nav { + display: block; + width: auto; + overflow: hidden; + margin-bottom: -0.25rem 0 1.125rem; + padding-top: 0.25rem; } + .sub-nav dt { + text-transform: uppercase; } + .sub-nav dt, + .sub-nav dd, + .sub-nav li { + float: left; + margin-left: 1rem; + margin-bottom: 0; + font-family: "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif; + font-weight: normal; + font-size: 0.875rem; + color: #999999; } + .sub-nav dt a, + .sub-nav dd a, + .sub-nav li a { + text-decoration: none; + color: #999999; + padding: 0.1875rem 1rem; } + .sub-nav dt a:hover, + .sub-nav dd a:hover, + .sub-nav li a:hover { + color: #737373; } + .sub-nav dt.active a, + .sub-nav dd.active a, + .sub-nav li.active a { + border-radius: 3px; + font-weight: normal; + background: #2ba6cb; + padding: 0.1875rem 1rem; + cursor: default; + color: #FFFFFF; } + .sub-nav dt.active a:hover, + .sub-nav dd.active a:hover, + .sub-nav li.active a:hover { + background: #258faf; } + +.label { + font-weight: normal; + font-family: "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif; + text-align: center; + text-decoration: none; + line-height: 1; + white-space: nowrap; + display: inline-block; + position: relative; + margin-bottom: auto; + padding: 0.25rem 0.5rem 0.25rem; + font-size: 0.6875rem; + background-color: #2ba6cb; + color: #FFFFFF; } + .label.radius { + border-radius: 3px; } + .label.round { + border-radius: 1000px; } + .label.alert { + background-color: #c60f13; + color: #FFFFFF; } + .label.warning { + background-color: #f08a24; + color: #FFFFFF; } + .label.success { + background-color: #5da423; + color: #FFFFFF; } + .label.secondary { + background-color: #e9e9e9; + color: #333333; } + .label.info { + background-color: #a0d3e8; + color: #333333; } Added: comdev/reporter.apache.org/trunk/site/js/coffee/html.coffee URL: http://svn.apache.org/viewvc/comdev/reporter.apache.org/trunk/site/js/coffee/html.coffee?rev=1799040&view=auto ============================================================================== --- comdev/reporter.apache.org/trunk/site/js/coffee/html.coffee (added) +++ comdev/reporter.apache.org/trunk/site/js/coffee/html.coffee Sun Jun 18 06:59:01 2017 @@ -0,0 +1,65 @@ + +class HTML + constructor: (type, params, children) -> + ### create the raw element, or clone if passed an existing element ### + if typeof type is 'object' + @element = type.cloneNode() + else + @element = document.createElement(type) + + ### If params have been passed, set them ### + if isHash(params) + for key, val of params + ### Standard string value? ### + if typeof val is "string" or typeof val is 'number' + @element.setAttribute(key, val) + else if isArray(val) + ### Are we passing a list of data to set? concatenate then ### + @element.setAttribute(key, val.join(" ")) + else if isHash(val) + ### Are we trying to set multiple sub elements, like a style? ### + for subkey,subval of val + if not @element[key] + throw "No such attribute, #{key}!" + @element[key][subkey] = subval + + ### If any children have been passed, add them to the element ### + if children + ### If string, convert to textNode using txt() ### + if typeof children is "string" + @element.inject(txt(children)) + else + ### If children is an array of elems, iterate and add ### + if isArray children + for child in children + ### String? Convert via txt() then ### + if typeof child is "string" + @element.inject(txt(child)) + else + ### Plain element, add normally ### + @element.inject(child) + else + ### Just a single element, add it ### + @element.inject(children) + return @element +###* +# prototype injector for HTML elements: +# Example: mydiv.inject(otherdiv) +### +HTMLElement.prototype.inject = (child) -> + if isArray(child) + for item in child + # Convert to textNode if string + if typeof item is 'string' + item = txt(item) + this.appendChild(item) + else + # Convert to textNode if string + if typeof child is 'string' + child = txt(child) + this.appendChild(child) + return child + +txt = (a) -> + return document.createTextNode(a) + Added: comdev/reporter.apache.org/trunk/site/js/coffee/main.coffee URL: http://svn.apache.org/viewvc/comdev/reporter.apache.org/trunk/site/js/coffee/main.coffee?rev=1799040&view=auto ============================================================================== --- comdev/reporter.apache.org/trunk/site/js/coffee/main.coffee (added) +++ comdev/reporter.apache.org/trunk/site/js/coffee/main.coffee Sun Jun 18 06:59:01 2017 @@ -0,0 +1,829 @@ +jsdata = {} + +templates = {} +nproject = null; +animals = ['hedgehogs', 'cows', 'geese', 'pigs', 'fluffy kittens', 'puppies', 'rabid dogs', 'ponies', 'weevils'] + +# This is faster than parseInt, and it's more obvious what is being done +toInt = (number) -> + return number | 0 + + + +makeSelect = (name, arr) -> + sel = new HTML('select', { name: name}) + for val in arr + opt = new HTML('option', { value: val}) + opt.inject(val) + sel.inject(opt) + return sel + + +getWednesdays = (mo, y) -> + d = new Date(); + if mo + d.setMonth(mo); + if y + d.setFullYear(y, d.getMonth(), d.getDate()) + + month = d.getMonth() + wednesdays = [] + + d.setDate(1) + + # Get the first Wednesday (day 3 of week) in the month + while d.getDay() != 3 + d.setDate(d.getDate() + 1) + + + # Get all the other Wednesdays in the month + while d.getMonth() == month + wednesdays.push(new Date(d.getTime())) + d.setDate(d.getDate() + 7) + + return wednesdays; + +# check if the entry is a wildcard month + +everyMonth = (s) -> + if ('Next month' not in s) + return true + return s == 'Every month' + + +m = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'] + +# Format the report month array. Assumes that non-month values appear first + +formatRm = (array) -> + first = array[0] + if array.length == 1 + return first + if first not in m + return first.concat('; (default: ', array.slice(1).join(', '),')') + return array.join(', ') + +# Called by: GetAsyncJSON("reportingcycles.json?" + Math.random(), [pmc, reportdate, json.pdata[pmc].name], setReportDate) + +setReportDate = (json, x) -> + pmc = x[0] + reportdate = x[1] + fullname = (x[2] or "Unknown").replace(/Apache /, "") + today = new Date() + + # the entries must be in date order + dates = [] + if not pmc in json + pmc = fullname + + #reporting months for the pmc + rm = json[pmc] + + # First check if the list contains an every month indicator + # This is necessary to ensure that the dates are added to the list in order + for i in json[pmc] + sm = json[pmc][i] + if everyMonth(sm) + # Reset to every month + rm = m + break + + # Check the months in order, so it does not matter if the data is unordered + for x in m + for i in rm + if (m[x] == rm[i]) + dates.push(getWednesdays(x)[2]) + + # Cannot combine with the code above because that would destroy the order + ny = today.getFullYear() + 1 + for x in m + for i in rm + if (m[x] == rm[i]) + dates.push(getWednesdays(x, ny)[2]) + + nextdate = dates[0]; + while (nextdate < today) + nextdate = dates.shift() + + + reportdate.innerHTML += "<b>Reporting schedule:</b> " + (if json[pmc] then formatRm(json[pmc]) else "Unknown(?)") + "<br>" + reportdate.innerHTML += "<b>Next report date: " + (if nextdate then nextdate.toDateString() else "Unknown(?)") + "</b>" + if (nextdate) + link = "https://svn.apache.org/repos/private/foundation/board/board_agenda_" + nextdate.getFullYear() + + "_" + (nextdate.getMonth() < 9 ? "0" : "") + (nextdate.getMonth() + 1) + "_" + nextdate.getDate() + ".txt" + reportdate.innerHTML += "<br>File your report in <a href='" + link + "'>" + link + "</a> when it has been seeded." + + +buildPanel = (pmc, title) -> + parent = document.getElementById('tab_' + pmc); + + toc = document.getElementById('toc_' + pmc); + if (!toc) + toc = document.createElement('cl') + toc.setAttribute("class", "sub-nav") + toc.setAttribute("id", "toc_" + pmc) + if (parent.firstChild.nextSibling) + parent.insertBefore(toc, parent.firstChild.nextSibling); + else + parent.appendChild(toc) + + linkname = title.toLowerCase().replace(/[^a-z0-9]+/, "") + li = document.createElement('dd'); + li.setAttribute("role", "menu-item") + li.innerHTML = "<a href='#" + linkname + "_" + pmc + "'>" + title + "</a>" + toc.appendChild(li) + + div = document.createElement('div') + div.setAttribute("id", linkname + "_" + pmc); + parent.appendChild(div) + + titlebox = document.createElement('div'); + titlebox.innerHTML = "<h3 style='background: #666; color: #EEE; border: 1px solid #66A; margin-top: 30px;'>" + title + " <small> <b>↑</b> <a href='#tab_" + pmc + "'>Back to top</a></small></h3>" + div.appendChild(titlebox); + return div + + +addLine = (pmc, line) -> + line = if line then line else " " + lines = line.split(/\n/) + for xline in lines + words = xline.split(" ") + len = 0 + out = "" + for word, i in words + len += word.replace(/<.+?>/, "").length + (i == words.length - 1 ? 0 : 1) + if (len >= 78) + out += "\n " + len = words[i].replace(/<.+?>/, "").length + (i == words.length - 1 ? 0 : 1) + out += words[i] + " " + templates[pmc] += out + "\n" + + +isNewPMC = (json,pmc,after) -> + return json.pmcdates[pmc].pmc[1] >= (after.getTime() / 1000) + + +PMCchanges = (json, pmc, after) -> + changes = buildPanel(pmc, "PMC changes (from committee-info)"); + + roster = json.pmcdates[pmc].roster + nc = 0; # newest committer start date + np = 0; # newest pmc member start date + ncn = null; # newest committer name + npn = null; # newest pmc name + afterTime = after.getTime() / 1000 + pmcStartTime = json.pmcdates[pmc].pmc[2] + + addLine(pmc, "## PMC changes:") + addLine(pmc) + if (pmcStartTime > afterTime) + afterTime = pmcStartTime + changes.innerHTML += "<h5>Changes since PMC creation:</h5>" + else + changes.innerHTML += "<h5>Changes within the last 3 months:</h5>" + + + # pre-flight check + c = 0; # total number of pmc members + npmc = 0; # number of recent pmc members + for k, entry of roster + c++ + if (entry[1] > afterTime) + npmc++; + addLine(pmc, " - Currently " + c + " PMC members.") + if (npmc > 1) + addLine(pmc, " - New PMC members:") + + + for k, entry of roster + if (entry[1] > np) # find most recent member + np = entry[1] # start date + npn = entry[0]; # full name + + if (entry[1] > afterTime) + changes.innerHTML += "→ " + entry[0] + " was added to the PMC on " + new Date(entry[1] * 1000).toDateString() + "<br>"; + addLine(pmc, (npmc > 1 ? " " : "") + " - " + entry[0] + " was added to the PMC on " + new Date(entry[1] * 1000).toDateString()) + + if (npmc == 0) + addLine(pmc, " - No new PMC members added in the last 3 months") + changes.innerHTML += "→ <font color='red'><b>No new PMC members in the last 3 months.</b></font><br>"; + + if (npn) + if (np < afterTime) + addLine(pmc, " - Last PMC addition was " + npn + " on " + new Date(np * 1000).toDateString()) + changes.innerHTML += "→ " + "<b>Last PMC addition: </b>" + new Date(np * 1000).toDateString() + " (" + npn + ")<br>" + + changes.innerHTML += "→ " + "<b>Currently " + c + " PMC members.<br>" + changes.innerHTML += "<br>PMC established: " + json.pmcdates[pmc].pmc[0] + if (pmcStartTime > 0) # don't use missing time + changes.innerHTML += " (assumed actual date: " + epochSecsYYYYMMDD(pmcStartTime) + ")" + addLine(pmc) + + +epochSecsYYYYMMDD = (t) => + new Date(t * 1000).toISOString().slice(0, 10) + + +renderFrontPage = (tpmc) -> + thisHour = toInt(new Date().getTime() / (3600*1000)) # this changes once per hour + container = document.getElementById('contents') + top = document.createElement('div'); + container.appendChild(top) + json = jsdata + sproject = tpmc; + hcolors = ["#000070", "#007000", "#407000", "#70500", "#700000", "#A00000"] + hvalues = ["Super Healthy", "Healthy", "Mostly Okay", "Unhealthy", "Action required!", "URGENT ACTION REQUIRED!"] + for pmc, i in json.pmcs + if pmc == tpmc or not tpmc + # Stuff has broken, check that we have dates! + if (not pmc in json.pmcdates) + continue + + templates[pmc] = "" + + addLine(pmc, "## Description:") + if ('shortdesc' in jsdata.pdata[pmc]) + addLine(pmc, " " + json.pdata[pmc].shortdesc) + else + addLine(pmc, " - <font color='red'>Description goes here</font>") + + addLine(pmc) + + a = animals[Math.floor(Math.random()*animals.length*0.999)] + addLine(pmc, "## Issues:") + addLine(pmc, " - <font color='red'>TODO - list any issues that require board attention, \n or say \"there are no issues requiring board attention at this time\" - if not, the " + a + " will get you.</font>") + addLine(pmc) + + addLine(pmc, "## Activity:") + addLine(pmc, " - <font color='red'>TODO - the PMC <b><u>MUST</u></b> provide this information</font>") + addLine(pmc) + + + a = animals[Math.floor(Math.random()*animals.length*0.999)] + addLine(pmc, "## Health report:") + addLine(pmc, " - <font color='red'>TODO - Please use this paragraph to elaborate on why the current project activity (mails, commits, bugs etc) is at its current level - Maybe " + a + " took over and are now controlling the project?</font>") + addLine(pmc) + + obj = document.createElement('div'); + obj.setAttribute("id", "tab_" + pmc) + obj.style = "padding: 10px; text-align: left !important;" + obj.setAttribute("aria-hidden", "true") + title = document.createElement('h2') + title.innerHTML = json.pdata[pmc].name or pmc + obj.appendChild(title) + health = document.createElement('p'); + if (json.health[pmc] && !isNaN(json.health[pmc]['cscore'])) + health.style.marginTop = "10px" + health.innerHTML = "<b>Committee Health score:</b> <a href='chi.py#" + pmc + "'><u><font color='" + hcolors[json.health[pmc]['cscore']] + "'>" + (6.33 + (json.health[pmc]['score'] * -1.00 * (20 / 12.25))).toFixed(2) + " (" + hvalues[json.health[pmc]['cscore']] + ")</u></font></a> <i>(This is an automatically generated score, it is NOT authoritative in any way!)</i>" + obj.appendChild(health) + + container.appendChild(obj) + + + + # Report date + + reportdate = buildPanel(pmc, "Report date") + if (json.pdata[pmc].chair) + reportdate.innerHTML += "<b>Committee Chair: </b>" + json.pdata[pmc].chair + "<br>" + + + fetch("reportingcycles.json?" + thisHour, [pmc, reportdate, json.pdata[pmc].name], setReportDate) + + + # LDAP committee + Committer changes + + mo = new Date().getMonth() - 3; + after = new Date(); + after.setMonth(mo); # This also works if mo is negative + PMCchanges(json, pmc, after); + + changes = buildPanel(pmc, "PMC changes (From LDAP)"); + + c = 0; # total number of committer + pmc changes since establishment + cu = 0; # total number of committer (user) changes + for x,y of json.changes[pmc].committer + cu++; + c++; + for x,y of json.changes[pmc].pmc + c++; + nc = 0; # newest committer date + np = 0; # newest pmc date + ncn = null; # newest committer name + npn = null; # newest pmc name + + addLine(pmc, "## Committer base changes:") + addLine(pmc) + addLine(pmc, " - Currently " + json.count[pmc][1] + " committers.") + if (cu == 0) # no new committers + if (isNewPMC(json,pmc,after)) + addLine(pmc, " - No changes (the PMC was established in the last 3 months)") + else + addLine(pmc, " - No new changes to the committer base since last report.") + addLine(pmc) + + if (c == 0) # no changes at all + if (isNewPMC(json,pmc,after)) + changes.innerHTML += "No changes - the PMC was established in the last 3 months." + else + changes.innerHTML += "<font color='red'><b>No new changes to the PMC or committer base detected - (LDAP error or no changes for >2 years)</b></font>" + + else + changes.innerHTML += "<h5>Changes within the last 3 months:</h5>" + + # pre-flight check + npmc = 0; # recent committee group additions + for k, entry of json.changes[pmc].pmc + if (entry[1] > after.getTime() / 1000) + npmc++; + + for k, entry of json.changes[pmc].pmc + if (entry[1] > np) # latest pmc member date + np = entry[1] + npn = entry[0]; # latest pmc member name + + if (entry[1] > after.getTime() / 1000) + changes.innerHTML += "→ " + entry[0] + " was added to the PMC on " + new Date(entry[1] * 1000).toDateString() + "<br>" + + if (npmc == 0) # PMC older than 3 months itself + if (isNewPMC(json,pmc,after)) + changes.innerHTML += "→ No new PMC members in the 3 months since the PMC was established<br>"; + else + changes.innerHTML += "→ <font color='red'><b>No new PMC members in the last 3 months.</b></font><br>"; + + if (npn) + changes.innerHTML += "→ " + "<b>Last PMC addition: </b>" + new Date(np * 1000).toDateString() + " (" + npn + ")<br>" + + + + # pre-flight check + ncom = 0; # number of new committers + for k, entry of json.changes[pmc].committer + if (entry[1] > after.getTime() / 1000) # entry[1] is the first seen timestamp + ncom++; + + if (ncom > 1) + addLine(pmc, " - New commmitters:") + + for k, entry of json.changes[pmc].committer + if (entry[1] > nc) # find the most recent entry + nc = entry[1] # the timestamp + ncn = entry[0];# full name + + if (entry[1] > after.getTime() / 1000) + changes.innerHTML += "→ " + entry[0] + " was added as a committer on " + new Date(entry[1] * 1000).toDateString() + "<br>" + addLine(pmc, (ncom > 1 ? " " : "") + " - " + entry[0] + " was added as a committer on " + new Date(entry[1] * 1000).toDateString()) + + if (ncom == 0) + changes.innerHTML += "→ <font color='red'><b>No new committers in the last 3 months.</b></font><br>"; + addLine(pmc, " - No new committers added in the last 3 months") + + + if (ncn) + if (nc < after.getTime() / 1000) + addLine(pmc, " - Last committer addition was " + ncn + " at " + new Date(nc * 1000).toDateString()) + changes.innerHTML += "→ " + "<b>Last committer addition: </b>" + new Date(nc * 1000).toDateString() + " (" + ncn + ")<br>" + else + addLine(pmc, " - Last committer addition was more than 2 years ago") + changes.innerHTML += "→ " + "<b>Last committer addition: </b><font color='red'>more than two years ago (not in the archive!)</font><br>" + + changes.innerHTML += "→ " + "<b>Currently " + json.count[pmc][1] + " committers and " + json.count[pmc][0] + " PMC members." + addLine(pmc) + + + # Release data + + releases = buildPanel(pmc, "Releases") + addLine(pmc, "## Releases:") + addLine(pmc) + nr = 0; + lr = null; + lrn = 0; + tr = 0 + for version, date of json.releases[pmc] + tr++; + if (date > lrn) + lrn = date + lr = version + + if (date >= after.getTime() / 1000) + err = "" + if (new Date(date * 1000) > new Date()) + err = " (<font color='red'>This seems wrong?!</font>)" + + releases.innerHTML += "→ " + "<b>" + version + "</b> was released on " + new Date(date * 1000).toDateString() + err + "<br>" + addLine(pmc, " - " + version + " was released on " + new Date(date * 1000).toDateString() + err) + nr++; + + if (nr == 0) + if (lr) + releases.innerHTML += "→ " + "<b>Last release was " + lr + ", released on </b>" + new Date(lrn * 1000).toDateString() + "<br>" + addLine(pmc, " - Last release was " + lr + " on " + new Date(lrn * 1000).toDateString()) + if (lr.match("incubat") && !isNewPMC(json,pmc,after)) + releases.innerHTML += "<br><font color='red'><b>No release since graduation</b></font><br><br>" + addLine(pmc, " - <font color='red'><b>No release since graduation??? [FIX!]</b></font>") + else + releases.innerHTML += "No release data could be found.<br>" + addLine(pmc, " - <font color='red'>No release data could be found [FIX!]</font>") + + releases.innerHTML += "<i>(A total of " + (tr - nr) + " older release(s) were found for " + pmc + " in our db)</i><br>" + releases.innerHTML += "<br><a href='javascript:void(0);' onclick=\"$('#rdialog_" + pmc + "').dialog({minWidth: 450, minHeight: 240});\">Add a release</a>" + releases.innerHTML += " - <a href='javascript:void(0);' onclick=\"$('#dialog_" + pmc + "').dialog({minWidth: 450, minHeight: 240});\">Fetch releases from JIRA</a>" + releases.innerHTML += " - <a href='addrelease.html?" + pmc + "'>Manage release versions</a><br>" + + if (tr > 0) + div = renderReleaseChart(json.releases[pmc], pmc, releases); + releases.appendChild(div) + + addLine(pmc) + + mlbox = buildPanel(pmc, "Mailing lists"); + + ul = document.createElement('ul') + ul.style.textAlign = "left;" + mlbox.appendChild(ul) + prev = "" + f = 0 + addLine(pmc, "## Mailing list activity:") + addLine(pmc) + addLine(pmc, " - <font color='red'>TODO Please explain what the following statistics mean for the project." + + " If there is nothing significant in the figures, omit this section.</font>") + addLine(pmc) + + first = ['users', 'dev', 'commits', 'private', 'bugs', 'modules-dev']; + + + for i,x of first + ml = pmc + ".apache.org-" + first[i] + if (ml != prev && ml.search("infra") < 0 && json.mail[pmc] && json.mail[pmc][ml]) + f++; + prev = ml + d = ml.split(".org-"); + mlname = d[1] + "@" + d[0] + ".org" + lookup = d[0].split(/\./)[0] + "-" + d[1] + + x = renderChart(json.mail[pmc], ml, obj, if (json.delivery[pmc] && json.delivery[pmc][lookup]) then json.delivery[pmc][lookup].weekly else {}) + total = x[0] + diff = x[1] + div = x[2] + + add = "" + if (json.delivery[pmc] && json.delivery[pmc][lookup]) + add = ":\n - " + json.delivery[pmc][lookup].quarterly[0] + " emails sent to list (" + json.delivery[pmc][lookup].quarterly[1] + " in previous quarter)"; + + text = "Currently: " + total + " subscribers <font color='green'>(up " + diff + " in the last 3 months)</font>" + if (diff < 0) + text = "Currently: " + total + " subscribers <font color='red'>(down " + diff + " in the last 3 months)</font>" + if (d[1] != "private" && d[1] != "security" && d[1] != "commits") + addLine(pmc, " - " + mlname + ": ") + addLine(pmc, " - " + total + " subscribers (down " + diff + " in the last 3 months)" + add) + addLine(pmc) + else + if (d[1] != "private" && d[1] != "security" && d[1] != "commits") + addLine(pmc, " - " + mlname + ": ") + addLine(pmc, " - " + total + " subscribers (up " + diff + " in the last 3 months)" + add) + addLine(pmc) + + if (json.delivery[pmc] && json.delivery[pmc][lookup]) + text += " (" + json.delivery[pmc][lookup].quarterly[0] + " emails sent in the past 3 months, " + json.delivery[pmc][lookup].quarterly[1] + " in the previous cycle)" + + p = document.createElement('li'); + p.innerHTML = "<h5>" + mlname + ":</h5>" + text + p.appendChild(div) + ul.appendChild(p) + + for ml of json.mail[pmc] + skip = false + for i in first + xml = pmc + ".apache.org-" + first[i] + if (ml.search(xml) == 0) + skip = true + if (!skip) + + f++; + if (ml != prev && ml.search("infra") < 0) + prev = ml + d = ml.split(".org-"); + mlname = d[1] + "@" + d[0] + ".org" + lookup = d[0].split(/\./)[0] + "-" + d[1] + x = renderChart(json.mail[pmc], ml, obj, if (json.delivery[pmc] && json.delivery[pmc][lookup]) then json.delivery[pmc][lookup].weekly else {}) + total = x[0] + diff = x[1] + div = x[2] + + add = "" + if (json.delivery[pmc] && json.delivery[pmc][lookup]) + add = ":\n - " + json.delivery[pmc][lookup].quarterly[0] + " emails sent to list (" + json.delivery[pmc][lookup].quarterly[1] + " in previous quarter)"; + + text = "Currently: " + total + " subscribers <font color='green'>(up " + diff + " in the last 3 months)</font>" + if (diff < 0) + text = "Currently: " + total + " subscribers <font color='red'>(down " + diff + " in the last 3 months)</font>" + if (d[1] != "private" && d[1] != "security" && d[1] != "commits") + addLine(pmc, " - " + mlname + ": ") + addLine(pmc, " - " + total + " subscribers (down " + diff + " in the last 3 months)" + add) + addLine(pmc) + else + if (d[1] != "private" && d[1] != "security" && d[1] != "commits") + addLine(pmc, " - " + mlname + ": ") + addLine(pmc, " - " + total + " subscribers (up " + diff + " in the last 3 months)" + add) + addLine(pmc) + + if (json.delivery[pmc] && json.delivery[pmc][lookup]) + text += " (" + json.delivery[pmc][lookup].quarterly[0] + " emails sent in the past 3 months, " + json.delivery[pmc][lookup].quarterly[1] + " in the previous cycle)" + + p = document.createElement('li'); + p.innerHTML = "<h5>" + mlname + ":</h5>" + text + p.appendChild(div) + ul.appendChild(p) + + addLine(pmc) + + if json.bugzilla[pmc][0] or json.bugzilla[pmc][1] > 0 + renderBZ(pmc) + + + if json.jira[pmc][0] > 0 or json.jira[pmc][1] > 0 + renderJIRA(pmc) + + + + # Reporting example + template = buildPanel(pmc, "Report template"); + template.innerHTML += "<pre style='border: 2px dotted #444; padding: 10px; background: #FFD;' contenteditable='true'>" + templates[pmc] + "</pre>" + + # Fetch from JIRA dialog + dialog = document.createElement('div'); + dialog.setAttribute("id", "dialog_" + pmc); + dialog.setAttribute("title", "Fetch data from JIRA for " + pmc) + dialog.setAttribute("style", "display: none;") + if (jsdata.keys[pmc] && jsdata.keys[pmc].length > 0) + dialog.innerHTML = "<p>Suggested JIRA Keys: <kbd>" + jsdata.keys[pmc].join(", ") + "</kbd></p>" + else + dialog.innerHTML = "<p>No JIRA keys found - are you sure this project uses JIRA?</p>" + + dialog.innerHTML += "<form><b>JIRA Project:</b><input type='text' name='jira' placeholder='FOO'><br><b>Optional prepend:</b> <input name='prepend' type='text' placeholder='Foo'/><br>"+ + "<input type='button' value='Fetch from JIRA' onclick='fetchJIRA(\"" + pmc + "\", this.form[\"jira\"].value, this.form[\"prepend\"].value);'></form>"+ + "<p>If you have multiple JIRA projects and they only have the version number in their release versions, please enter the component name in the 'prepend' field.</p>" + document.getElementById('tab_' + pmc).appendChild(dialog) + + # Manually add release dialog + rdialog = document.createElement('div'); + rdialog.setAttribute("id", "rdialog_" + pmc); + rdialog.setAttribute("title", "Add a release for " + pmc) + rdialog.setAttribute("style", "display: none;") + rdialog.innerHTML = "<form><b>Version:</b><input type='text' name='version' placeholder='1.2.3'><br>"+ + "<b>Date:</b> <input name='date' type='text' placeholder='YYYY-MM-DD'/><br>"+ + "<input type='button' value='Add release' onclick='addRelease(\"" + pmc + "\", this.form[\"version\"].value, this.form[\"date\"].value);'></form>" + container.appendChild(rdialog) + + + + + + +renderJIRA = (pmc) -> + obj = buildPanel(pmc, "JIRA Statistics") + + addLine(pmc, "## JIRA activity:") + addLine(pmc) + addLine(pmc, " - " + jsdata.jira[pmc][0] + " JIRA tickets created in the last 3 months"); + addLine(pmc, " - " + jsdata.jira[pmc][1] + " JIRA tickets closed/resolved in the last 3 months"); + addLine(pmc) + obj.innerHTML += jsdata.jira[pmc][0] + " JIRA tickets created in the last 3 months<br>"; + obj.innerHTML += jsdata.jira[pmc][1] + " JIRA tickets closed/resolved in the last 3 months<br>"; + if (jsdata.keys[pmc]) + obj.innerHTML += "Keys used: <kbd>" + jsdata.keys[pmc].join(", ") + "</kbd><br>" + + obj.innerHTML += "Keys with tickets: <kbd>" + jsdata.jira[pmc][2].join(", ") + "</kbd>" + + +renderBZ = (pmc) -> + obj = buildPanel(pmc, "Bugzilla Statistics") + + addLine(pmc, "## Bugzilla Statistics:") + addLine(pmc) + addLine(pmc, " - " + jsdata.bugzilla[pmc][0] + " Bugzilla tickets created in the last 3 months"); + addLine(pmc, " - " + jsdata.bugzilla[pmc][1] + " Bugzilla tickets resolved in the last 3 months"); + addLine(pmc) + obj.innerHTML += jsdata.bugzilla[pmc][0] + " Bugzilla tickets created in the last 3 months<br>"; + obj.innerHTML += jsdata.bugzilla[pmc][1] + " Bugzilla tickets resolved in the last 3 months<br>"; + obj.innerHTML += "Tickets were found for the following products:<br><kbd>" + Object.keys(jsdata.bugzilla[pmc][2]).sort().join(", ") + "</kbd>" + + +renderChart = (json, name, container, delivery) -> + + chartDiv = document.createElement('div') + chartDiv.setAttribute("id", name + "_chart") + dates = [] + noweekly = 0; + for date, count of json[name] + dates.push(date) + for date, count of delivery + noweekly++; + d = name.split(".org-"); + mlname = d[1] + "@" + d[0] + ".org" + dates.sort(); + cu = 0; + narr = [] + hitFirst = false + + dp = new Date(); + dp.setMonth(dp.getMonth() - 3); + + odp = new Date(); + odp.setMonth(odp.getMonth() - 6); + + difference = 0 + for date in dates + dateString = new Date(parseInt(date) * 1000); + if (dateString > dp) + difference += json[name][date] + + cu = cu + json[name][date]; + if (cu > 0) + hitFirst = true + + if ((cu > 0 || hitFirst) && dateString >= odp) + if (noweekly > 0) + narr.push([dateString, cu, delivery[date] or 0]) + else + narr.push([dateString, cu]) + + data = new google.visualization.DataTable(); + data.addColumn('date', 'Date'); + data.addColumn('number', "List members"); + if (noweekly > 0) + data.addColumn('number', "Emails sent per week"); + + data.addRows(narr); + + + options = { + title: 'Mailing list stats for ' + mlname, + backgroundColor: 'transparent', + width: 900, + height: 260, + legend: { + position: 'none', + maxLines: 3 + }, + vAxis: { + format: "#" + }, + vAxes: if (noweekly > 0) then [ + + { + title: 'Emails per week', + titleTextStyle: { + color: '#DD0000' + }, + min: 0 + }, { + title: 'Subscribers', + titleTextStyle: { + color: '#0000DD' + }, + min: 0, + minValue: 0 + }, + ] else [{ + title: 'Subscribers', + titleTextStyle: { + color: '#0000DD' + } + }, + ], + series: { + 0: { + type: "line", + pointSize: 4, + lineWidth: 2, + targetAxisIndex: if (noweekly > 0) then 1 else null + }, + 1: { + type: "bars", + targetAxisIndex: if (noweekly > 0) then 0 else [0, 1] + } + }, + seriesType: "bars", + tooltip: { + isHtml: true + }, + }; + + chart = new google.visualization.ComboChart(chartDiv); + + chart.draw(data, options); + return [cu, difference, chartDiv]; + + + +renderReleaseChart = (releases, name, container) -> + chartDiv = null + if (document.getElementById(name + "_releasechart")) + chartDiv = document.getElementById(name + "_releasechart") + else + chartDiv = document.createElement('div') + chartDiv.setAttribute("id", name + "_releasechart") + + narr = [] + maxLen = 1; + for version, date of releases + x = version.match(/(\d+)\.(\d+)/) + if (x && x[2].length > maxLen) + maxLen = x[2].length; + + for version, date of releases + if (new Date(releases[version] * 1000).getFullYear() >= 1999) + major = parseFloat(version) or 1 + x = version.match(/(\d+)\.(\d+)/) + if (x) + while (x[2].length < maxLen) + x[2] = "0" + x[2] + major = parseFloat(x[1] + "." + x[2]) + + narr.push([new Date(releases[version] * 1000), major, version + " - " + new Date(releases[version] * 1000).toDateString()]) + + data = new google.visualization.DataTable(); + + data.addColumn('datetime', 'Date'); + data.addColumn('number', 'Version') + data.addColumn('string', 'tooltip'); + data.setColumnProperty(2, 'role', 'tooltip'); + + data.addRows(narr); + + + options = { + title: 'Release timeline for ' + name, + height: 240, + width: 800, + backgroundColor: 'transparent', + series: [{ + pointSize: 15 + }, + ], + pointShape: { + type: 'star', + sides: 5 + } + }; + + chart = new google.visualization.ScatterChart(chartDiv); + chartDiv.style.marginLeft = "50px"; + + chart.draw(data, options); + return chartDiv + + +fetchJIRA = (pmc, project, prepend) -> + if (project && project.length > 1) + fetch("jiraversions.py?project=" + pmc + "&jiraname=" + project + "&prepend=" + prepend, null, + (json) -> + if json && json.versions + for version in json.versions + jsdata.releases[pmc][version] = json.versions[version] + + $('#dialog_' + pmc).dialog("close") + nproject = pmc + alert("Fetched " + json.added + " releases from JIRA!") + renderFrontPage(jsdata) + else if (json && json.status) + alert(json.status) + else if (json) + alert(JSON.stringify(json)) + else + alert("Couldn't find any release data :(") + ) + + +addRelease = (pmc, version, date) -> + if (version && version.length > 1 && date.match(/^(\d\d\d\d)-(\d\d)-(\d\d)$/)) + x = date.split("-"); + y = new Date(x[0], parseInt(x[1]) - 1, parseInt(x[2])); + nn = parseInt(y.getTime() / 1000); + now = (new Date().getTime()) / 1000; + if (nn >= now) + alert("Date is in the future!") + return + + fetch("addrelease.py?json=true&committee=" + pmc + "&version=" + escape(version) + "&date=" + nn, null, + (json) -> + if (json && json.versions) + n = 0; + for version in json.versions + n++; + jsdata.releases[pmc][version] = json.versions[version] + + $('#rdialog_' + pmc).dialog("close") + nproject = pmc + alert("Release added!") + renderFrontPage(jsdata) + else if (json && json.status) + alert(json.status) + else if (json) + alert(JSON.stringify(json)) + else + alert("Couldn't add release data :(") + ) + Added: comdev/reporter.apache.org/trunk/site/js/coffee/misc.coffee URL: http://svn.apache.org/viewvc/comdev/reporter.apache.org/trunk/site/js/coffee/misc.coffee?rev=1799040&view=auto ============================================================================== --- comdev/reporter.apache.org/trunk/site/js/coffee/misc.coffee (added) +++ comdev/reporter.apache.org/trunk/site/js/coffee/misc.coffee Sun Jun 18 06:59:01 2017 @@ -0,0 +1,117 @@ + +Number.prototype.pretty = (fix) -> + if (fix) + return String(this.toFixed(fix)).replace(/(\d)(?=(\d{3})+\.)/g, '$1,'); + return String(this.toFixed(0)).replace(/(\d)(?=(\d{3})+$)/g, '$1,'); + + +fetch = (url, xstate, callback, snap) -> + xmlHttp = null; + # Set up request object + if window.XMLHttpRequest + xmlHttp = new XMLHttpRequest(); + else + xmlHttp = new ActiveXObject("Microsoft.XMLHTTP"); + xmlHttp.withCredentials = true + # GET URL + xmlHttp.open("GET", url, true); + xmlHttp.send(null); + + xmlHttp.onreadystatechange = (state) -> + if xmlHttp.readyState == 4 and xmlHttp.status == 500 + if snap + snap(xstate) + if xmlHttp.readyState == 4 and xmlHttp.status == 200 + if callback + # Try to parse as JSON and deal with cache objects, fall back to old style parse-and-pass + try + response = JSON.parse(xmlHttp.responseText) + callback(response, xstate); + catch e + callback(JSON.parse(xmlHttp.responseText), xstate) + +post = (url, args, xstate, callback, snap) -> + xmlHttp = null; + # Set up request object + if window.XMLHttpRequest + xmlHttp = new XMLHttpRequest(); + else + xmlHttp = new ActiveXObject("Microsoft.XMLHTTP"); + xmlHttp.withCredentials = true + # Construct form data + ar = [] + for k,v of args + if v and v != "" + ar.push(k + "=" + escape(v)) + fdata = ar.join("&") + + + # POST URL + xmlHttp.open("POST", url, true); + xmlHttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); + xmlHttp.send(fdata); + + xmlHttp.onreadystatechange = (state) -> + if xmlHttp.readyState == 4 and xmlHttp.status == 500 + if snap + snap(xstate) + if xmlHttp.readyState == 4 and xmlHttp.status == 200 + if callback + # Try to parse as JSON and deal with cache objects, fall back to old style parse-and-pass + try + response = JSON.parse(xmlHttp.responseText) + callback(response, xstate); + catch e + callback(JSON.parse(xmlHttp.responseText), xstate) + + +postJSON = (url, json, xstate, callback, snap) -> + xmlHttp = null; + # Set up request object + if window.XMLHttpRequest + xmlHttp = new XMLHttpRequest(); + else + xmlHttp = new ActiveXObject("Microsoft.XMLHTTP"); + xmlHttp.withCredentials = true + # Construct form data + fdata = JSON.stringify(json) + + # POST URL + xmlHttp.open("POST", url, true); + xmlHttp.setRequestHeader("Content-type", "application/json"); + xmlHttp.send(fdata); + + xmlHttp.onreadystatechange = (state) -> + if xmlHttp.readyState == 4 and xmlHttp.status == 500 + if snap + snap(xstate) + if xmlHttp.readyState == 4 and xmlHttp.status == 200 + if callback + # Try to parse as JSON and deal with cache objects, fall back to old style parse-and-pass + try + response = JSON.parse(xmlHttp.responseText) + if response && response.loginRequired + location.href = "/oauth.html" + return + callback(response, xstate); + catch e + callback(JSON.parse(xmlHttp.responseText), xstate) + + + +isArray = ( value ) -> + value and + typeof value is 'object' and + value instanceof Array and + typeof value.length is 'number' and + typeof value.splice is 'function' and + not ( value.propertyIsEnumerable 'length' ) + + +### isHash: function to detect if an object is a hash ### +isHash = (value) -> + value and + typeof value is 'object' and + not isArray(value) + + Added: comdev/reporter.apache.org/trunk/site/js/coffee/preload.coffee URL: http://svn.apache.org/viewvc/comdev/reporter.apache.org/trunk/site/js/coffee/preload.coffee?rev=1799040&view=auto ============================================================================== --- comdev/reporter.apache.org/trunk/site/js/coffee/preload.coffee (added) +++ comdev/reporter.apache.org/trunk/site/js/coffee/preload.coffee Sun Jun 18 06:59:01 2017 @@ -0,0 +1,49 @@ + + +# Tabs above in the menu +tabs = [ + +] + +jsdata = {} + +mergeData = (json, pmc) -> + if not pmc + jsdata = json + return + if pmc in jsdata.pmcs + return + if (nproject && nproject.length > 0) + for i in jsdata.pmcs + if (jsdata.pmcs[i] == nproject) + jsdata.pmcs.splice(i, 1); + break + + todo = new Array('count', 'mail', 'delivery', 'bugzilla', 'jira', 'changes', 'pmcdates', 'pdata', 'releases', 'keys', 'health') + for i in todo + key = todo[i] + jsdata[key][pmc] = json[key][pmc]; + jsdata.pmcs.push(pmc) + nproject = pmc + + +preloadTabs = (json, state) -> + cpmc = if isHash(state) and state.pmc then state.pmc else null + mergeData(json, cpmc) + tabs = [] + a = 0 + ctab = 0 + for pmc in jsdata.pmcs + tab = { + id: pmc, + title: pmc, + renderer: renderFrontPage, + state: pmc + } + tabs.push(tab) + if cpmc == pmc + ctab = a + a++ + loadTabs(ctab) + + Added: comdev/reporter.apache.org/trunk/site/js/coffee/tabs.coffee URL: http://svn.apache.org/viewvc/comdev/reporter.apache.org/trunk/site/js/coffee/tabs.coffee?rev=1799040&view=auto ============================================================================== --- comdev/reporter.apache.org/trunk/site/js/coffee/tabs.coffee (added) +++ comdev/reporter.apache.org/trunk/site/js/coffee/tabs.coffee Sun Jun 18 06:59:01 2017 @@ -0,0 +1,46 @@ + +currentTab = 0 + + +# Display tabs +loadTabs = (stab) -> + main = new HTML('div', { id: 'dialog'}) + document.getElementById('wrapper').innerHTML = "" + document.getElementById('wrapper').appendChild(main) + + tdiv = new HTML('div', {class: 'tabs'}) + main.inject(tdiv) + k = 0 + for v in tabs + if (stab and stab == k) or (not stab and k == 0) + currentTab = k + history.pushState(null, null, "?#{k}") + tab = new HTML('div', {class: 'tablink tablink_selected'}, v.title) + title = new HTML('h2', {}, v.title+":") + main.inject(title) + else + tab = new HTML('div', {class: 'tablink', onclick: "loadTabs(" + k + ");"}, v.title) + tdiv.inject(tab) + k++ + + all = ['Add a tab:', '---------------'] + for pmc in jsdata.all or [] + all.push(pmc) + sel = makeSelect('project', all) + sel.setAttribute("onchange", "addTab(this.value);") + tdiv.inject(sel) + bread = new HTML('div', { class: 'bread', id: 'contents'}) + main.inject(bread) + loadBread(stab) + +addTab = (pmc) -> + fetch("getjson.py?only="+pmc, {pmc: pmc}, preloadTabs) + +loadBread = (which) -> + if tabs[which] and tabs[which].renderer + # Clear bread div + document.getElementById('contents').innerHTML = "" + # Render tab using render function + tabs[which].renderer(tabs[which].state) + +