Commit 8b3c66eb authored by rniwa@webkit.org's avatar rniwa@webkit.org

perf-o-matic needs a better admin page

https://bugs.webkit.org/show_bug.cgi?id=79585

Reviewed by Sam Weinig.
        
Add admin/ to replace admin/create-models.html and admin/merge-tests.

Also update MergeTestHandler to accept JSON requests and add "Admin" navigation link on all pages.

* Websites/webkit-perf.appspot.com/admin_handlers.py: Added.
* Websites/webkit-perf.appspot.com/controller.py:
(RunsUpdateHandler.post): Fix a regression from r108399.
* Websites/webkit-perf.appspot.com/css/admin.css: Added.
* Websites/webkit-perf.appspot.com/js/admin.js: Added.
* Websites/webkit-perf.appspot.com/js/config.js:
* Websites/webkit-perf.appspot.com/main.py:
* Websites/webkit-perf.appspot.com/merge_tests.html: Removed.
* Websites/webkit-perf.appspot.com/merge_tests_handler.py:
(MergeTestsHandler):
(MergeTestsHandler.post):
* Websites/webkit-perf.appspot.com/static: Removed.
* Websites/webkit-perf.appspot.com/static/create-models.html: Removed.


git-svn-id: http://svn.webkit.org/repository/webkit/trunk@108917 268f45cc-cd09-0410-ab3c-d52691b4dbfc
parent c63f05dd
2012-02-25 Ryosuke Niwa <rniwa@webkit.org>
perf-o-matic needs a better admin page
https://bugs.webkit.org/show_bug.cgi?id=79585
Reviewed by Sam Weinig.
Add admin/ to replace admin/create-models.html and admin/merge-tests.
Also update MergeTestHandler to accept JSON requests and add "Admin" navigation link on all pages.
* Websites/webkit-perf.appspot.com/admin_handlers.py: Added.
* Websites/webkit-perf.appspot.com/controller.py:
(RunsUpdateHandler.post): Fix a regression from r108399.
* Websites/webkit-perf.appspot.com/css/admin.css: Added.
* Websites/webkit-perf.appspot.com/js/admin.js: Added.
* Websites/webkit-perf.appspot.com/js/config.js:
* Websites/webkit-perf.appspot.com/main.py:
* Websites/webkit-perf.appspot.com/merge_tests.html: Removed.
* Websites/webkit-perf.appspot.com/merge_tests_handler.py:
(MergeTestsHandler):
(MergeTestsHandler.post):
* Websites/webkit-perf.appspot.com/static: Removed.
* Websites/webkit-perf.appspot.com/static/create-models.html: Removed.
2012-02-24 Ryosuke Niwa <rniwa@webkit.org>
Increment perf-o-matic version.
......
#!/usr/bin/env python
# Copyright (C) 2012 Google Inc. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following disclaimer
# in the documentation and/or other materials provided with the
# distribution.
# * Neither the name of Google Inc. nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import webapp2
import json
from google.appengine.api import users
from google.appengine.ext.db import GqlQuery
from google.appengine.ext.webapp import template
from models import Branch
from models import Builder
from models import Platform
from models import ReportLog
from models import Test
class IsAdminHandler(webapp2.RequestHandler):
def get(self):
self.response.headers['Content-Type'] = 'application/json'
self.response.out.write('true' if users.is_current_user_admin() else 'false')
class AdminDashboardHandler(webapp2.RequestHandler):
def get(self, task):
task_method_name = 'get_' + task
if task_method_name in dir(self) and task_method_name not in dir(super):
method = getattr(self, task_method_name)
return method()
report_log_total = GqlQuery("SELECT __key__ FROM ReportLog").count()
report_log_committed = GqlQuery("SELECT __key__ FROM ReportLog WHERE commit = True").count()
self.response.out.write(template.render('admin.html',
{'report_log_total': report_log_total, 'report_log_committed': report_log_committed}))
def get_branches(self):
self.response.headers['Content-Type'] = 'application/json'
result = {}
for branch in Branch.all().fetch(limit=100):
result[branch.key().name()] = branch.name
self.response.out.write(json.dumps(result))
def get_platforms(self):
self.response.headers['Content-Type'] = 'application/json'
result = {}
for platform in Platform.all().fetch(limit=100):
result[platform.key().name()] = platform.name
self.response.out.write(json.dumps(result))
def get_builders(self):
self.response.headers['Content-Type'] = 'application/json'
self.response.out.write(json.dumps([builder.name for builder in Builder.all().fetch(limit=100)]))
def get_tests(self):
self.response.headers['Content-Type'] = 'application/json'
self.response.out.write(json.dumps([test.name for test in Test.all().fetch(limit=200)]))
......@@ -114,7 +114,7 @@ class RunsUpdateHandler(webapp2.RequestHandler):
def get(self):
self.post()
def get(self):
def post(self):
self.response.headers['Content-Type'] = 'text/plain; charset=utf-8'
test_id, branch_id, platform_id = _get_test_branch_platform_ids(self)
......
html {
padding: 0px;
margin: 0px;
}
#summary {
margin-top: 49px;
font-size: 14px;
line-height: 36px;
display: table;
border-collapse: collapse;
}
section {
display: table-cell;
background: white;
border: 1px solid lightgrey;
border-collapse: collapse;
min-height: 300px;
max-width: 400px;
}
h2 {
clear: both;
background: -moz-linear-gradient(-90deg, #f8f8f8, #e7e7e7);
background: -webkit-gradient(linear, left top, left bottom, from(#f8f8f8), to(#e7e7e7));
padding: 0 10px;
border-bottom: solid 1px lightgrey;
}
dl, section > ul > li {
padding: 0 10px;
}
dt, dd {
float: left;
}
dt {
text-align: right;
clear: both;
}
dt:after {
content: " : ";
}
dd {
text-indent: 0.5ex;
}
.counts dt {
width: 100px;
}
.counts dd {
width: 50px;
}
li.form {
padding: 0;
border-top: solid 1px lightgrey;
}
form {
text-align: right;
padding: 10px;
}
input {
width: 100%;
font-size: 14px;
border: solid 1px lightgrey;
}
form dt {
width: 70px;
}
form dd {
width: 150px;
}
#footer {
margin: 10px;
}
function removeNonFormListItems(list) {
list.children().each(function () {
if ($.inArray('form', this.classList))
$(this).remove();
});
}
function createKeyNameReloader(name) {
return function () {
$.getJSON(name, function (platforms) {
var list = $('#' + name + ' ul');
removeNonFormListItems(list);
$.each(platforms, function (key, name) {
list.append('<li>' + key + ' : ' + name + '</li>');
});
list.append($('#' + name + ' ul .form'));
});
}
}
$('#branches form').bind('reload', createKeyNameReloader('branches'));
$('#platforms form').bind('reload', createKeyNameReloader('platforms'));
$('#builders form').bind('reload', function () {
$.getJSON('builders', function (builders) {
var list = $('#builders ul');
removeNonFormListItems(list);
builders = builders.sort();
for (var i = 0; i < builders.length; i++)
list.append('<li><a href="http://build.webkit.org/builders/' + builders[i] + '">' + builders[i] + '</a></li>');
list.append($('#builders ul .form'));
});
});
$('#tests form').bind('reload', function () {
$.getJSON('tests', function (tests) {
var list = $('#tests ul');
removeNonFormListItems(list);
var select = $('#tests select');
select.children().remove();
tests = tests.sort();
for (var i = 0; i < tests.length; i++) {
list.append('<li>' + tests[i] + '</li>'); // FIXME: Add a link to trac page.
select.append('<option value="' + tests[i] + '">' + tests[i] + '</option>');
}
list.append($('#tests ul .form'));
});
});
$.ajaxSetup({
'error': function(xhr, e, message) { console.log(xhr); error('Failed with HTTP status: ' + xhr.status, e); },
cache: true,
});
$('form').trigger('reload');
$('form').bind('submit', function (event) {
event.preventDefault();
var contents = {}
for (var i = 0; i < this.elements.length; i++)
contents[this.elements[i].name] = this.elements[i].value;
var xhr = new XMLHttpRequest;
xhr.onreadystatechange = function () {
if (xhr.readyState != 4)
return;
if (xhr.status != 200)
error('HTTP status: ' + xhr.status);
else if (xhr.responseText != 'OK')
error(xhr.responseText);
}
xhr.open(this.method, this.action, true);
xhr.send(JSON.stringify(contents));
$(this).trigger('reload');
});
......@@ -99,3 +99,21 @@ function fetchDashboardManifest(callback)
callback(dashboardManifest);
});
}
(function() {
$.ajaxSetup({
'error': function(xhr, e, message) {
error('Could not determine the the login status', e);
},
cache: true,
});
$.getJSON('/api/user/is-admin', function (isAdmin) {
if (isAdmin) {
$('#header nav').append('<a href="/admin/">Admin</a>');
if (!$('#header nav .selected').length) {
$('#header nav a').last().addClass('selected')
}
}
})
})();
......@@ -20,6 +20,8 @@ from google.appengine.ext.webapp import util
import json
from admin_handlers import IsAdminHandler
from admin_handlers import AdminDashboardHandler
from controller import CachedDashboardHandler
from controller import CachedManifestHandler
from controller import CachedRunsHandler
......@@ -38,6 +40,8 @@ routes = [
('/admin/merge-tests/?', MergeTestsHandler),
('/admin/report-logs/?', ReportLogsHandler),
('/admin/create/(.*)', CreateHandler),
(r'/admin/([A-Za-z\-]*)', AdminDashboardHandler),
('/api/user/is-admin', IsAdminHandler),
('/api/test/?', CachedManifestHandler),
('/api/test/update', ManifestUpdateHandler),
('/api/test/report/?', ReportHandler),
......
<!DOCTYPE html>
<html>
<body>
<h1>Merge tests</h1>
<form method="post" action="/admin/merge-tests">
<label for="merge">Merge</label>:
<select id="merge" name="merge">
{% for test in tests %}
<option value="{{ test.name }}">{{ test.name }}</option>
{% endfor %}
</select>
<label for="into">Into</label>:
<select id="into" name="into">
{% for test in tests %}
<option value="{{ test.name }}">{{ test.name }}</option>
{% endfor %}
</select>
<button type="submit">Merge</button>
</form>
</body>
</html>
......@@ -30,6 +30,7 @@
import webapp2
from google.appengine.ext.webapp import template
import json
import os
from controller import schedule_runs_update
......@@ -41,14 +42,19 @@ from models import delete_model_with_numeric_id_holder
class MergeTestsHandler(webapp2.RequestHandler):
def get(self):
self.response.out.write(template.render('merge_tests.html', {'tests': Test.all()}))
def post(self):
self.response.headers['Content-Type'] = 'text/plain; charset=utf-8';
merge = Test.get_by_key_name(self.request.get('merge'))
into = Test.get_by_key_name(self.request.get('into'))
try:
payload = json.loads(self.request.body)
merge = payload.get('merge', '')
into = payload.get('into', '')
except:
self.response.out.write("Failed to parse the payload: %s" % self.request.body)
return
merge = Test.get_by_key_name(merge)
into = Test.get_by_key_name(into)
if not merge or not into:
self.response.out.write('Invalid test names')
return
......
<!DOCTYPE html>
<html>
<head>
<title>Create new models</title>
<style type="text/css">
em { font-style: normal; color: red; }
pre {border: solid 1px black; padding: 5px;}
h3 {font-size: 1em;}
</style>
</head>
<body>
<h1>Create new models</h1>
<p>Key: canonicalized name used by build bots and storage. Name: human friendly name</p>
<h2>Builder</h2>
<form method="post" action="/admin/create/builder" onsubmit="return submitByXHR(this, event)">
<label for="name">Name/Key</label><input type="text" name="name">
<label for="password">Password</label><input type="password" name="password">
<button type="submit">Create</button>
</form>
<h2>Branch</h2>
<form method="post" action="/admin/create/branch" onsubmit="return submitByXHR(this, event);">
<label for="key">Key</label><input type="text" name="key">
<label for="name">Name</label><input type="text" name="name">
<button type="submit">Create</button>
</form>
<h2>Platform</h2>
<form method="post" action="/admin/create/platform" onsubmit="return submitByXHR(this, event)">
<label for="key">Key</label><input type="text" name="key">
<label for="name">Name</label><input type="text" name="name">
<button type="submit">Create</button>
</form>
<h2>Result:</h2>
<h3>Status code</h3>
<p id="status"></p>
<h3>Headers</h3>
<pre id="headers"></pre>
<h3>Response</h3>
<pre id="response" name="response"></pre>
<script>
$ = function (id) { return document.getElementById(id); }
function submitByXHR(form, event) {
event.preventDefault();
var contents = {}
for (var i = 0; i < form.elements.length; i++)
contents[form.elements[i].name] = form.elements[i].value;
var xhr = new XMLHttpRequest;
xhr.onreadystatechange = function () {
if (xhr.readyState != 4)
return;
$('status').innerText = xhr.status;
$('headers').innerText = xhr.getAllResponseHeaders();
$('response').innerHTML = xhr.responseText;
}
xhr.open(form.method, form.action, true);
xhr.send(JSON.stringify(contents));
}
</script>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<title>Test submission of a build report</title>
<style type="text/css">
em { font-style: normal; color: red; }
pre {border: solid 1px black; padding: 5px;}
h3 {font-size: 1em;}
</style>
</head>
<body>
<h1>Test submission of a build report</h1>
<p>Specify the payload and submit:</p>
<textarea id="data" name="data" rows="20" cols="100"></textarea><br>
<em id="json_error"></em><br>
<button type="submit" onclick="submit()">Submit</button>
<h2>Result:</h2>
<h3>Status code</h3>
<p id="status"></p>
<h3>Headers</h3>
<pre id="headers"></pre>
<h3>Response</h3>
<pre id="response" name="response"></pre>
<script>
$ = function (id) { return document.getElementById(id); }
$('data').oninput = function () {
var payload = $('data').value;
try {
JSON.parse(payload);
$('json_error').innerText = '';
} catch (error) {
$('json_error').innerText = error;
}
}
function submit() {
var xhr = new XMLHttpRequest;
xhr.onreadystatechange = function () {
if (xhr.readyState != 4)
return;
$('status').innerText = xhr.status;
$('headers').innerText = xhr.getAllResponseHeaders();
$('response').innerHTML = xhr.responseText;
}
xhr.open('POST','/admin/report/', true);
xhr.send($('data').value);
}
$('data').value = JSON.stringify({
'branch': 'webkit-trunk',
'platform': 'chromium-mac',
'builder-name': 'Chromium Mac Release (Perf)',
'build-number': '123',
'timestamp': parseInt(Date.now() / 1000),
'webkit-revision': 104856,
'results':
{
'webkit_style_test': {'avg': 100, 'median': 102, 'stdev': 5, 'min': 90, 'max': 110},
'some_test': 54,
},
}, null, ' ');
</script>
</body>
</html>
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment