Skip to content

Commit

Permalink
Web interface for product listing
Browse files Browse the repository at this point in the history
 - The CodeChecker Viewer now by default opens a product list. Users can
   select the product they want to view and will be routed to the run list.
 - If only ONE products exist on the server, simply opening
   http://host:port will automatically redirect the user to the run list
   of this single product.
    - This will only happen if this only product is properly connected.
 - Added a button to run lists to explicitly return to the product list
   page
 - The current product's name is now shown in the run list's title bar

[ci skip]
  • Loading branch information
whisperity committed Aug 23, 2017
1 parent 28b3e78 commit afde871
Show file tree
Hide file tree
Showing 15 changed files with 635 additions and 60 deletions.
5 changes: 5 additions & 0 deletions api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,8 @@ See [report_server.thrift](https://raw.githubusercontent.com/Ericsson/codechecke
## Authentication system API
The authentication layer is used for supporting privileged-access only access.
See [authentication.thrift](https://raw.githubusercontent.com/Ericsson/codechecker/master/thrift_api/authentication.thrift)

## Product management system API
The product management layer is responsible for handling requests about the
different products and their configuration. See
[products.thrift](https://raw.githubusercontent.com/Ericsson/codechecker/master/thrift_api/products.thrift)
19 changes: 17 additions & 2 deletions api/products.thrift
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,25 @@ typedef list<Product> Products

service codeCheckerProductService {

// *** Handling the add-modify-remove of products registered *** //
Products getProducts()
// Return the product management API version.
string getAPIVersion(),

// Returns the CodeChecker version that is running on the server.
string getPackageVersion(),

// *** Handling of product lists and metadata querying *** //

// Get the list of product that matches the display name and endpoint
// filters specified.
Products getProducts(1: string productEndpointFilter,
2: string productNameFilter)
throws (1: shared.RequestFailed requestError),

Product getCurrentProduct()
throws (1: shared.RequestFailed requestError),

// *** Handling the add-modify-remove of products registered *** //

bool addProduct(1: Product product)
throws (1: shared.RequestFailed requestError),

Expand Down
Binary file added docs/images/products.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion libcodechecker/server/client_db_access_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@

from run_db_model import *

LOG = LoggerFactory.get_new_logger('ACCESS HANDLER')
LOG = LoggerFactory.get_new_logger('RUN ACCESS HANDLER')


def conv(text):
Expand Down
76 changes: 53 additions & 23 deletions libcodechecker/server/client_db_access_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,23 @@

LOG = LoggerFactory.get_new_logger('DB ACCESS')

NON_PRODUCT_NAMES = ['index.html',
# A list of top-level path elements under the webserver root
# which should not be considered as a product route.
NON_PRODUCT_NAMES = ['products.html',
'index.html',
'fonts',
'images',
'scripts',
'style'
]

# A list of top-level path elements in requests (such as Thrift endpoints)
# which should not be considered as a product route.
NON_PRODUCT_NAMES += ['Authentication',
'Products',
'CodeCheckerService'
]


class RequestHandler(SimpleHTTPRequestHandler):
"""
Expand All @@ -60,9 +70,6 @@ class RequestHandler(SimpleHTTPRequestHandler):
"""

def __init__(self, request, client_address, server):
self.db_version_info = server.db_version_info
self.manager = server.manager

BaseHTTPRequestHandler.__init__(self,
request,
client_address,
Expand All @@ -80,7 +87,7 @@ def __check_auth_in_request(self):
present.
"""

if not self.manager.isEnabled():
if not self.server.manager.isEnabled():
return None

success = None
Expand All @@ -98,9 +105,10 @@ def __check_auth_in_request(self):
values = cookie.split("=")
if len(values) == 2 and \
values[0] == session_manager.SESSION_COOKIE_NAME:
if self.manager.is_valid(values[1], True):
if self.server.manager.is_valid(values[1], True):
# The session cookie contains valid data.
success = self.manager.get_session(values[1], True)
success = self.server.manager.get_session(values[1],
True)

if success is None:
# Session cookie was invalid (or not found...)
Expand All @@ -114,7 +122,8 @@ def __check_auth_in_request(self):
self.headers.getheader("Authorization").
replace("Basic ", ""))

session = self.manager.create_or_get_session(authString)
session = self.server.manager.create_or_get_session(
authString)
if session:
LOG.info("Client from " + client_host + ":" +
str(client_port) +
Expand Down Expand Up @@ -144,7 +153,8 @@ def __get_product_name(self):
# viewer client from the www/ folder.

# The split array looks like ['', 'product-name', ...].
return urlparse.urlparse(self.path).path.split('/', 2)[1]
first_part = urlparse.urlparse(self.path).path.split('/', 2)[1]
return first_part if first_part not in NON_PRODUCT_NAMES else None

def do_GET(self):
"""
Expand All @@ -167,12 +177,12 @@ def do_GET(self):
self.send_header("Content-length", str(len(error_body)))
self.send_header('Connection', 'close')
self.end_headers()
self.wfile.write(self.manager.getRealm()["error"])
self.wfile.write(error_body)
return
else:
product_name = self.__get_product_name()

if product_name not in NON_PRODUCT_NAMES and product_name != '':
if product_name is not None and product_name != '':
if not self.server.get_product(product_name):
LOG.info("Product named '{0}' does not exist."
.format(product_name))
Expand All @@ -189,13 +199,14 @@ def do_GET(self):
LOG.debug("Redirecting user from /{0} to /{0}/index.html"
.format(product_name))

# WARN: Browsers cache '301 Moved Permanently' responses,
# WARN: Browsers cache '308 Permanent Redirect' responses,
# in the event of debugging this, use Private Browsing!
self.send_response(301)
self.send_response(308)
self.send_header("Location",
self.path.replace(product_name,
product_name + '/', 1))
self.end_headers()
return
else:
# Serves the main page and the resources:
# /prod/(index.html) -> /(index.html)
Expand All @@ -205,24 +216,35 @@ def do_GET(self):
"{0}/".format(product_name), "", 1)
LOG.debug("Product routing after: " + self.path)
else:
self.send_response(200) # 200 OK
if auth_session is not None:
# Browsers get a standard cookie for session.
self.send_header(
"Set-Cookie",
"{0}={1}; Path=/".format(
session_manager.SESSION_COOKIE_NAME,
auth_session.auth_token))
if self.path in ['/', '/index.html']:
only_product = self.server.get_only_product()
if only_product and only_product.connected:
LOG.debug("Redirecting '/' to ONLY product '/{0}'"
.format(only_product.endpoint))

self.send_response(307) # 307 Temporary Redirect
self.send_header("Location",
'/{0}'.format(only_product.endpoint))
self.end_headers()
return

if self.path in ['/', '/index.html', '/products.html']:
# Route homepage queries to serving the product list.
LOG.debug("Serving product list as homepage.")
self.path = "/products.html"
self.path = '/products.html'
else:
# The path requested does not specify a product: it is most
# likely a resource file.
LOG.debug("Serving resource '{0}'".format(self.path))

self.send_response(200) # 200 OK
if auth_session is not None:
# Browsers get a standard cookie for session.
self.send_header(
"Set-Cookie",
"{0}={1}; Path=/".format(
session_manager.SESSION_COOKIE_NAME,
auth_session.token))

SimpleHTTPRequestHandler.do_GET(self)

def do_POST(self):
Expand Down Expand Up @@ -558,6 +580,14 @@ def get_product(self, endpoint):
"""
return self.__products.get(endpoint, None)

def get_only_product(self):
"""
Returns the Product object for the only product connected to by the
server, or None, if there are 0 or >= 2 products managed.
"""
return self.__products.items()[0][1] if len(self.__products) == 1 \
else None


def start_server(package_data, port, db_conn_string, suppress_handler,
listen_address, context, check_env):
Expand Down
2 changes: 2 additions & 0 deletions www/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@
<script type="text/javascript" src="scripts/codechecker-api/codeCheckerDBAccess.js"></script>
<script type="text/javascript" src="scripts/codechecker-api/authentication_types.js"></script>
<script type="text/javascript" src="scripts/codechecker-api/codeCheckerAuthentication.js"></script>
<script type="text/javascript" src="scripts/codechecker-api/products_types.js"></script>
<script type="text/javascript" src="scripts/codechecker-api/codeCheckerProductService.js"></script>

<!-- Configure Dojo -->

Expand Down
102 changes: 102 additions & 0 deletions www/products.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
<!DOCTYPE html>
<html>
<head>
<title>CodeChecker</title>

<meta charset="UTF-8">

<link rel="shortcut icon" href="images/favicon.ico" />

<!-- CSS -->

<link type="text/css" rel="stylesheet" href="scripts/plugins/dojo/dijit/themes/claro/claro.css" />
<link type="text/css" rel="stylesheet" href="scripts/plugins/dojo/dojox/grid/resources/claroGrid.css"/>
<link type="text/css" rel="stylesheet" href="style/codecheckerviewer.css" />
<link type="text/css" rel="stylesheet" href="style/productlist.css" />

<!-- Third party libraries -->

<script type="text/javascript" src="scripts/plugins/jsplumb/external/jquery-1.9.0-min.js"></script>
<script type="text/javascript" src="scripts/plugins/jsplumb/jquery.jsPlumb-1.7.6-min.js"></script>
<script type="text/javascript" src="scripts/plugins/thrift/thrift.js"></script>
<script type="text/javascript" src="scripts/plugins/marked/marked.min.js"></script>

<!-- Services -->

<script type="text/javascript" src="scripts/codechecker-api/shared_types.js"></script>
<script type="text/javascript" src="scripts/codechecker-api/products_types.js"></script>
<script type="text/javascript" src="scripts/codechecker-api/codeCheckerProductService.js"></script>
<script type="text/javascript" src="scripts/codechecker-api/authentication_types.js"></script>
<script type="text/javascript" src="scripts/codechecker-api/codeCheckerAuthentication.js"></script>

<!-- Configure Dojo -->

<script>
var dojoConfig = {
baseUrl : '',
async : true,
packages : [
{ name : 'dojo', location : 'scripts/plugins/dojo/dojo' },
{ name : 'dijit', location : 'scripts/plugins/dojo/dijit' },
{ name : 'dojox', location : 'scripts/plugins/dojo/dojox' },
{ name : 'codechecker', location : 'scripts/codecheckerviewer' },
{ name : 'products', location : 'scripts/productlist' }
]
};
</script>

<script type="text/javascript" src="scripts/plugins/dojo/dojo/dojo.js"></script>

<!-- End loading custom scripts -->
</head>
<body class="claro">
<script>
// http://stackoverflow.com/questions/5916900/how-can-you-detect-the-version-of-a-browser
var browserVersion = (function(){
var ua = navigator.userAgent, tem,
M = ua.match(/(opera|chrome|safari|firefox|msie|trident(?=\/))\/?\s*(\d+)/i) || [];

if (/trident/i.test(M[1])) {
tem = /\brv[ :]+(\d+)/g.exec(ua) || [];
return 'IE ' + (tem[1] || '');
}

if (M[1] === 'Chrome') {
tem = ua.match(/\b(OPR|Edge)\/(\d+)/);
if (tem != null) return tem.slice(1).join(' ').replace('OPR', 'Opera');
}

M = M[2] ? [M[1], M[2]] : [navigator.appName, navigator.appVersion, '-?'];
if ((tem = ua.match(/version\/(\d+)/i)) != null) M.splice(1, 1, tem[1]);
return M.join(' ');
})();

var pos = browserVersion.indexOf(' ');
var browser = browserVersion.substr(0, pos);
var version = parseInt(browserVersion.substr(pos + 1));

var browserCompatible
= browser === 'Firefox'
? version >= 4
: browser === 'IE'
? version >= 9
: true;

if (browserCompatible) {
require([
'products/productlist'],
function (productlist) {
productlist();
});
} else {
document.body.innerHTML =
'<h2 style="margin-left: 20px;">Your browser is not compatible with CodeChecker Viewer!</h2> \
<p style="margin-left: 20px;">The version required for the following browsers are:</p> \
<ul style="margin-left: 20px;"> \
<li>Internet Explorer: version 9 or newer</li> \
<li>Firefox: version 4 or newer</li> \
</ul>';
}
</script>
</body>
</html>
8 changes: 4 additions & 4 deletions www/scripts/codecheckerviewer/CommentView.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ define([
'dijit/tree/ObjectStoreModel',
'codechecker/HtmlTree',
'codechecker/util'],
function (declare, dom, style, topic, Memory, Observable, ConfirmDialog, Dialog,
ContentPane, Button, SimpleTextarea, TextBox, ObjectStoreModel, HtmlTree,
util) {
function (declare, dom, style, topic, Memory, Observable, ConfirmDialog,
Dialog, ContentPane, Button, SimpleTextarea, TextBox, ObjectStoreModel,
HtmlTree, util) {

var Reply = declare(ContentPane, {
constructor : function () {
Expand Down Expand Up @@ -207,4 +207,4 @@ function (declare, dom, style, topic, Memory, Observable, ConfirmDialog, Dialog,
});
}
});
});
});
21 changes: 1 addition & 20 deletions www/scripts/codecheckerviewer/ListOfRuns.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,25 +20,6 @@ define([
function (declare, domConstruct, ItemFileWriteStore, topic, Dialog, Button,
RadioButton, TextBox, BorderContainer, ContentPane, DataGrid, util) {

function prettifyDuration(seconds) {
var prettyDuration = "--------";

if (seconds >= 0) {
var durHours = Math.floor(seconds / 3600);
var durMins = Math.floor(seconds / 60) - durHours * 60;
var durSecs = seconds - durMins * 60 - durHours * 3600;

var prettyDurHours = (durHours < 10 ? '0' : '') + durHours;
var prettyDurMins = (durMins < 10 ? '0' : '') + durMins;
var prettyDurSecs = (durSecs < 10 ? '0' : '') + durSecs;

prettyDuration
= prettyDurHours + ':' + prettyDurMins + ':' + prettyDurSecs;
}

return prettyDuration;
}

/**
* This function helps to format a data grid cell with two radio buttons.
* @param args {runData, listOfRunsGrid} - the value from the data store that
Expand Down Expand Up @@ -188,7 +169,7 @@ function (declare, domConstruct, ItemFileWriteStore, topic, Dialog, Button,
name : '<span class="link">' + runData.name + '</span>',
date : currItemDate[0] + ' ' + currItemDate[1],
numberofbugs : runData.resultCount,
duration : prettifyDuration(runData.duration),
duration : util.prettifyDuration(runData.duration),
runData : runData,
checkcmd : '<span class="link">Show</span>',
del : false,
Expand Down
Loading

0 comments on commit afde871

Please sign in to comment.