Stackalytics widget is implemented

The widget shows summary on contribution for companies or modules. Options are
limited to project type, release and metric. Stats is shown as chart. Widget is
designed to be included into any web page, the data is loaded via jsonp.

Also:
* Options for project type, release and metrics are loaded in AJAX, not coded in template
* Moved code that handles drop-down selectors into stackalytics-ui.js

Change-Id: I3373ab1a627099f380070c0e3d90164ec0096039
This commit is contained in:
Ilya Shakhat 2013-11-12 17:47:59 +04:00
parent fdaa41ef5c
commit 24816f7704
6 changed files with 465 additions and 174 deletions

View File

@ -233,6 +233,14 @@ a[href^="https://launchpad"]:after {
color: #4bb2c5;
}
.project_group {
font-weight: bold;
}
.project_group_item {
padding-left: 0.5em;
}
.review_mark {
font-weight: bold;
}

12
dashboard/static/js/jquery-ui.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -89,7 +89,7 @@ function renderTableAndChart(url, container_id, table_id, chart_id, link_param,
$.ajax({
url: make_uri(url),
dataType: "json",
dataType: "jsonp",
success: function (data) {
var tableData = [];
@ -143,7 +143,7 @@ function renderTableAndChart(url, container_id, table_id, chart_id, link_param,
var tableColumns = [];
var sort_by_column = 0;
for (i = 0; i < table_column_names.length; i++) {
tableColumns.push({"mData": table_column_names[i]})
tableColumns.push({"mData": table_column_names[i]});
if (table_column_names[i] == "metric") {
sort_by_column = i;
}
@ -272,3 +272,236 @@ function make_uri(uri, options) {
return (str == "") ? uri : uri + "?" + str;
}
function make_std_options() {
var options = {};
options['release'] = $('#release').val();
options['metric'] = $('#metric').val();
options['project_type'] = $('#project_type').val();
options['module'] = $('#module').val() || '';
options['company'] = $('#company').val() || '';
options['user_id'] = $('#user').val() || '';
return options;
}
function reload() {
window.location.search = $.map(make_std_options(),function (val, index) {
return index + "=" + val;
}).join("&")
}
function init_selectors(base_url) {
var release = getUrlVars()["release"];
if (!release) {
release = "_default";
}
$("#release").val(release).select2({
ajax: {
url: make_uri(base_url + "/api/1.0/releases"),
dataType: 'jsonp',
data: function (term, page) {
return {
query: term
};
},
results: function (data, page) {
return {results: data["releases"]};
}
},
initSelection: function (element, callback) {
var id = $(element).val();
$.ajax(make_uri(base_url + "/api/1.0/releases/" + id), {
dataType: "jsonp"
}).done(function (data) {
callback(data["release"]);
$("#release").val(data["release"].id)
});
}
});
$('#release')
.on("change", function (e) {
reload();
});
var metric = getUrlVars()["metric"];
if (!metric) {
metric = "_default";
}
$("#metric").val(metric).select2({
ajax: {
url: make_uri(base_url + "/api/1.0/metrics"),
dataType: 'jsonp',
data: function (term, page) {
return {
query: term
};
},
results: function (data, page) {
$("#metric").val(data["default"]);
return {results: data["metrics"]};
}
},
initSelection: function (element, callback) {
var id = $(element).val();
$.ajax(make_uri(base_url + "/api/1.0/metrics/" + id), {
dataType: "jsonp"
}).done(function (data) {
callback(data["metric"]);
$("#metric").val(data["metric"].id);
});
}
});
$('#metric')
.on("change", function (e) {
reload();
});
var project_type = getUrlVars()["project_type"];
if (!project_type) {
project_type = "_default";
}
$("#project_type").val(project_type).select2({
ajax: {
url: make_uri(base_url + "/api/1.0/project_types"),
dataType: 'jsonp',
data: function (term, page) {
return {
query: term
};
},
results: function (data, page) {
const project_types = data["project_types"];
var result = [];
result.push({id: "all", text: "all", group: true});
for (var key in project_types) {
result.push({id: project_types[key].id, text: project_types[key].text, group: true});
for (var i in project_types[key].items) {
result.push({id: project_types[key].items[i], text: project_types[key].items[i]});
}
}
return {results: result};
}
},
initSelection: function (element, callback) {
var id = $(element).val();
$.ajax(make_uri(base_url + "/api/1.0/project_types/" + id), {
dataType: "jsonp"
}).done(function (data) {
callback(data["project_type"]);
$("#project_type").val(data["project_type"].id);
});
},
formatResultCssClass: function (item) {
if (item.group) {
return "project_group"
} else {
return "project_group_item";
}
}
});
$('#project_type')
.on("change", function (e) {
$('#module').val('');
reload();
});
$("#company").select2({
allowClear: true,
ajax: {
url: make_uri(base_url + "/api/1.0/companies"),
dataType: 'jsonp',
data: function (term, page) {
return {
company_name: term
};
},
results: function (data, page) {
return {results: data["companies"]};
}
},
initSelection: function (element, callback) {
var id = $(element).val();
if (id !== "") {
$.ajax(make_uri(base_url + "/api/1.0/companies/" + id), {
dataType: "jsonp"
}).done(function (data) {
callback(data["company"]);
});
}
}
});
$('#company')
.on("change", function (e) {
reload();
});
$("#module").select2({
allowClear: true,
ajax: {
url: make_uri(base_url + "/api/1.0/modules"),
dataType: 'jsonp',
data: function (term, page) {
return {
module_name: term
};
},
results: function (data, page) {
return {results: data["modules"]};
}
},
initSelection: function (element, callback) {
var id = $(element).val();
if (id !== "") {
$.ajax(make_uri(base_url + "/api/1.0/modules/" + id), {
dataType: "jsonp"
}).done(function (data) {
callback(data["module"]);
});
}
},
formatResultCssClass: function (item) {
if (item.group) {
return "select_group"
}
return "";
}
});
$('#module')
.on("change", function (e) {
reload();
});
$("#user").select2({
allowClear: true,
ajax: {
url: make_uri(base_url + "/api/1.0/users"),
dataType: 'jsonp',
data: function (term, page) {
return {
user_name: term
};
},
results: function (data, page) {
return {results: data["users"]};
}
},
initSelection: function (element, callback) {
var id = $(element).val();
if (id !== "") {
$.ajax(make_uri(base_url + "/api/1.0/users/" + id), {
dataType: "json"
}).done(function (data) {
callback(data["user"]);
});
}
}
});
$('#user')
.on("change", function (e) {
reload();
});
}

View File

@ -42,155 +42,9 @@
<script type="text/javascript">
$(document).ready(function () {
$('#metric').val('{{ metric }}');
$('#release').val('{{ release }}');
$('#project_type')
.val('{{ project_type }}')
.on("change", function(e) {
$('#module').val('');
reload();
});
$("#release").select2();
$("#metric").select2();
$("#project_type").select2();
$("#company").select2({
allowClear: true,
ajax: {
url: make_uri("/api/1.0/companies"),
dataType: 'json',
data: function (term, page) {
return {
company_name: term
};
},
results: function (data, page) {
return {results: data["companies"]};
}
},
initSelection: function (element, callback) {
var id = $(element).val();
if (id !== "") {
$.ajax(make_uri("/api/1.0/companies/" + id), {
dataType: "json"
}).done(function (data) {
callback(data["company"]);
});
}
}
});
$('#company')
.on("change", function(e) { reload(); });
$("#module").select2({
allowClear: true,
ajax: {
url: make_uri("/api/1.0/modules"),
dataType: 'json',
data: function (term, page) {
return {
module_name: term
};
},
results: function (data, page) {
return {results: data["modules"]};
}
},
initSelection: function (element, callback) {
var id = $(element).val();
if (id !== "") {
$.ajax(make_uri("/api/1.0/modules/" + id), {
dataType: "json"
}).done(function (data) {
callback(data["module"]);
});
}
},
formatResultCssClass: function (item) {
if (item.group) {
return "select_group"
}
return "";
}
});
$('#module')
.on("change", function(e) { reload(); });
$("#user").select2({
allowClear: true,
ajax: {
url: make_uri("/api/1.0/users"),
dataType: 'json',
data: function (term, page) {
return {
user_name: term
};
},
results: function (data, page) {
return {results: data["users"]};
}
},
initSelection: function (element, callback) {
var id = $(element).val();
if (id !== "") {
$.ajax(make_uri("/api/1.0/users/" + id), {
dataType: "json"
}).done(function (data) {
callback(data["user"]);
});
}
}
});
$('#user')
.on("change", function(e) { reload(); });
init_selectors("");
});
function make_std_options() {
var options = {};
options['release'] = getRelease();
options['metric'] = getMetric();
options['project_type'] = getProjectType();
options['module'] = $('#module').val();
options['company'] = $('#company').val();
options['user_id'] = $('#user').val();
return options;
}
function reload() {
window.location.search = $.map(make_std_options(),function (val, index) {
return index + "=" + val;
}).join("&")
}
$(document).on('change', '#metric', function (evt) {
reload();
});
$(document).on('change', '#release', function (evt) {
reload();
});
$(document).on('change', '#project_type', function (evt) {
reload();
});
function getRelease() {
return $('#release').val()
}
function getMetric() {
return $('#metric').val()
}
function getProjectType() {
return $('#project_type').val()
}
</script>
{% block scripts %}{% endblock %}
@ -213,30 +67,12 @@
<div class="drop">
<label for="release">Release</label>
<select id="release" name="release" style="min-width: 140px;" data-placeholder="Select release">
<option></option>
<option value="all">All times</option>
{% for release in release_options %}
<option value="{{ release.release_name }}">{{ release.release_name | capitalize }}</option>
{% endfor %}
</select>
<input type="hidden" id="release" style="width: 140px" data-placeholder="Select release"/>
</div>
<div class="drop">
<label for="project_type">Projects</label>
<select id="project_type" name="project_type" style="min-width: 140px;">
<option value="All">all</option>
{% for option_type, option_groups in project_type_options.iteritems() %}
<optgroup label="{{ option_type }}">
<option value="{{ option_type }}">{{ option_type }}
- all
</option>
{% for option_group in option_groups %}
<option value="{{ option_group }}">{{ option_group }}</option>
{% endfor %}
</optgroup>
{% endfor %}
</select>
<input type="hidden" id="project_type" style="width: 140px" data-placeholder="Select project type"/>
</div>
<div class="drop">
@ -256,11 +92,7 @@
<div class="drop">
<label for="metric">Metric</label>
<select id="metric" name="metric" style="width: 140px">
{% for metric in metric_options %}
<option value="{{ metric[0] }}">{{ metric[1] }}</option>
{% endfor %}
</select>
<input type="hidden" id="metric" style="width: 140px" data-placeholder="Select metric"/>
</div>
</div>

View File

@ -0,0 +1,139 @@
<!DOCTYPE html>
<html>
<head>
<title>Stackalytics Widget</title>
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
<title>Stackalytics Widget</title>
<meta name="description" content="OpenStack contribution dashboard collects and processes development activity data such as commits, lines of code changed, and code reviews"/>
<meta name="keywords" content="openstack, contribution, community, review, commit, {{ company }}"/>
<link href='http://fonts.googleapis.com/css?family=PT+Sans:400,700,400italic&subset=latin,cyrillic' rel='stylesheet' type='text/css' />
<link href='http://fonts.googleapis.com/css?family=PT+Sans+Caption&subset=latin,cyrillic' rel='stylesheet' type='text/css' />
<link href='http://fonts.googleapis.com/css?family=PT+Sans+Narrow:400,700&subset=latin,cyrillic' rel='stylesheet' type='text/css' />
<link rel="icon" href="{{ url_for('static', filename='images/favicon.png') }}" type="image/png"/>
<link rel=stylesheet type=text/css href="{{ url_for('static', filename='css/jquery.jqplot.min.css') }}">
<link rel=stylesheet type=text/css href="{{ url_for('static', filename='css/jquery.dataTables.css') }}">
<link rel=stylesheet type=text/css href="{{ url_for('static', filename='css/select2.css') }}">
<link rel=stylesheet type=text/css href="{{ url_for('static', filename='css/style.css') }}">
<link rel="stylesheet" href="http://code.jquery.com/ui/1.10.3/themes/smoothness/jquery-ui.css" />
<script type="text/javascript" src="{{ url_for('static', filename='js/jquery-1.9.1.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/jquery-ui.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/jquery.dataTables.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/jquery.jqplot.min.js') }}"></script>
<!--[if lt IE 9]><script type="text/javascript" src="{{ url_for('static', filename='js/excanvas.min.js') }}"></script><![endif]-->
<script type="text/javascript" src="{{ url_for('static', filename='js/jqplot.json2.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/jqplot.pieRenderer.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/jqplot.dateAxisRenderer.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/jqplot.canvasTextRenderer.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/jqplot.canvasAxisTickRenderer.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/jqplot.cursor.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/jqplot.highlighter.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/select2.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/jquery.tmpl.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/stackalytics-ui.js') }}"></script>
<script type="text/javascript">
var base_url = "http://localhost:5000";
$(document).ready(function () {
init_selectors(base_url);
renderTableAndChart(base_url + "/api/1.0/stats/companies", null, null, "company_chart", null);
});
$(function () {
$("#tabs").tabs({
activate: function( event, ui ) {
if (ui.newTab.index() == 0) {
renderTableAndChart(base_url + "/api/1.0/stats/companies", null, null, "company_chart", null);
} else {
renderTableAndChart(base_url + "/api/1.0/stats/modules", null, null, "module_chart", null);
}
}
});
});
$(function () {
$("#more")
.button()
.click(function (event) {
event.preventDefault();
window.open('http://stackalytics.com/', 'stackalytics');
return false;
});
});
</script>
<style type="text/css">
html, body, p {
font-size: 12px;
}
h2 {
font-size: 20px;
}
.drop {
margin: 0;
height: auto;
}
.jqplot-target {
font-size: 10px;
}
table.jqplot-table-legend, table.jqplot-cursor-legend {
font-size: 10px;
}
.ui-tabs .ui-tabs-panel {
padding: 0;
}
</style>
</head>
<body>
<div class="page" style="width: 320px; height: 400px; padding: 10px;">
<h2>OpenStack&reg; Contribution Tracker</h2>
<div id="tabs" style="position: relative;">
<ul>
<li><a href="#tabs-1">By companies</a></li>
<li><a href="#tabs-2">By modules</a></li>
</ul>
<div id="tabs-1">
<div id="company_chart"
style="width: 100%; height: 260px; background-color: white"></div>
</div>
<div id="tabs-2">
<div id="module_chart" style="width: 100%; height: 260px;"></div>
</div>
<div style="position: absolute; left: 20px; top: 240px;"><a id="more" href="#">More</a>
</div>
</div>
<div>
<div class="drop" style="margin-right: 15px;">
<label for="release">Release</label>
<input type="hidden" id="release" style="width: 95px" data-placeholder="Select release"/>
</div>
<div class="drop" style="margin-right: 15px;">
<label for="project_type">Projects</label>
<input type="hidden" id="project_type" style="width: 95px" data-placeholder="Select project type"/>
</div>
<div class="drop">
<label for="metric">Metric</label>
<input type="hidden" id="metric" style="width: 95px" data-placeholder="Select metric"/>
</div>
</div>
</div>
</body>
</html>

View File

@ -61,6 +61,11 @@ def overview():
pass
@app.route('/widget')
def widget():
return flask.render_template('widget.html')
@app.errorhandler(404)
@decorators.templated('404.html', 404)
def page_not_found(e):
@ -293,6 +298,68 @@ def get_user(user_id):
return user
@app.route('/api/1.0/releases')
@decorators.jsonify('releases')
@decorators.exception_handler()
def get_releases_json():
query = (flask.request.args.get('query') or '').lower()
return [{'id': r['release_name'], 'text': r['release_name'].capitalize()}
for r in vault.get_release_options()
if r['release_name'].find(query) >= 0]
@app.route('/api/1.0/releases/<release>')
@decorators.jsonify('release')
def get_release_json(release):
if release != 'all':
if release not in vault.get_vault()['releases']:
release = parameters.get_default('release')
return {'id': release, 'text': release.capitalize()}
@app.route('/api/1.0/metrics')
@decorators.jsonify('metrics')
@decorators.exception_handler()
def get_metrics_json():
query = (flask.request.args.get('query') or '').lower()
return sorted([{'id': m, 'text': t}
for m, t in parameters.METRIC_LABELS.iteritems()
if t.lower().find(query) >= 0],
key=operator.itemgetter('text'))
@app.route('/api/1.0/metrics/<metric>')
@decorators.jsonify('metric')
@decorators.exception_handler()
def get_metric_json(metric):
if metric not in parameters.METRIC_LABELS:
metric = parameters.get_default('metric')
return {'id': metric, 'text': parameters.METRIC_LABELS[metric]}
@app.route('/api/1.0/project_types')
@decorators.jsonify('project_types')
@decorators.exception_handler()
def get_project_types_json():
return [{'id': m, 'text': m, 'items': list(t)}
for m, t in vault.get_project_type_options().iteritems()]
@app.route('/api/1.0/project_types/<project_type>')
@decorators.jsonify('project_type')
@decorators.exception_handler()
def get_project_type_json(project_type):
if project_type != 'all':
for pt, groups in vault.get_project_type_options().iteritems():
if (project_type == pt) or (project_type in groups):
break
else:
project_type = parameters.get_default('project_type')
return {'id': project_type, 'text': project_type}
@app.route('/api/1.0/stats/timeline')
@decorators.jsonify('timeline')
@decorators.exception_handler()