Loading...
diff --git a/frontend/src/common.js b/frontend/src/common.js
index f0d0c7e25..4578955ac 100644
--- a/frontend/src/common.js
+++ b/frontend/src/common.js
@@ -1,39 +1,35 @@
import Mustache from 'mustache';
+import { buildRoute, readRoute, updateRoute } from './route.js';
+import {ZERO_COVERAGE_FILTERS} from './zero_coverage_report.js';
export const REV_LATEST = 'latest';
-function assert(condition, message) {
- if (!condition) {
- throw new Error(message || "Assertion failed");
- }
-}
-
function domContentLoaded() {
return new Promise(resolve => document.addEventListener('DOMContentLoaded', resolve));
}
export const DOM_READY = domContentLoaded();
-export async function main(load, display, opts) {
- // Immediately listen to DOM event
-
+export async function main(load, display) {
// Load initial data before DOM is available
let data = await load();
// Wait for DOM to be ready before displaying
await DOM_READY;
await display(data);
+ monitor_options();
// Full workflow, loading then displaying data
// used for following updates
let full = async function() {
let data = await load();
await display(data);
+ monitor_options();
};
- monitor_options(opts, full);
+
+ // React to url changes
window.onhashchange = full;
}
-
// Coverage retrieval.
const COVERAGE_BACKEND_HOST = process.env.BACKEND_URL;
@@ -64,8 +60,9 @@ function cache_set(cache, key, value) {
}
let path_coverage_cache = {};
-export async function get_path_coverage(path, changeset) {
- let data = cache_get(path_coverage_cache, `${changeset}_${path}`);
+export async function get_path_coverage(path, changeset, platform, suite) {
+ let cache_key = `${changeset}_${path}_${platform}_${suite}`;
+ let data = cache_get(path_coverage_cache, cache_key);
if (data) {
return data;
}
@@ -74,33 +71,47 @@ export async function get_path_coverage(path, changeset) {
if (changeset && changeset !== REV_LATEST) {
params += `&changeset=${changeset}`;
}
+ if (platform && platform !== 'all') {
+ params += `&platform=${platform}`;
+ }
+ if (suite && suite !== 'all') {
+ params += `&suite=${suite}`;
+ }
let response = await fetch(`${COVERAGE_BACKEND_HOST}/v2/path?${params}`).catch(alert);
if (response.status !== 200) {
throw new Error(response.status + ' - ' + response.statusText);
}
data = await response.json();
- cache_set(path_coverage_cache, `${changeset}_${path}`, data);
+ cache_set(path_coverage_cache, cache_key, data);
return data;
}
let history_cache = {};
-export async function get_history(path) {
+export async function get_history(path, platform, suite) {
// Backend needs path without trailing /
if (path && path.endsWith('/')) {
path = path.substring(0, path.length-1);
}
- let data = cache_get(history_cache, path);
+ let cache_key = `${path}_${platform}_${suite}`;
+ let data = cache_get(history_cache, cache_key);
if (data) {
return data;
}
- let response = await fetch(`${COVERAGE_BACKEND_HOST}/v2/history?path=${path}`);
+ let params = `path=${path}`;
+ if (platform && platform !== 'all') {
+ params += `&platform=${platform}`;
+ }
+ if (suite && suite !== 'all') {
+ params += `&suite=${suite}`;
+ }
+ let response = await fetch(`${COVERAGE_BACKEND_HOST}/v2/history?${params}`);
data = await response.json();
- cache_set(history_cache, path, data);
+ cache_set(history_cache, cache_key, data);
// Check data has coverage values
// These values are missing when going above 2 levels right now
@@ -131,20 +142,62 @@ export async function get_zero_coverage_data() {
}
-// Option handling.
+let filters_cache = {};
+export async function get_filters() {
+ let data = cache_get(filters_cache, '');
+ if (data) {
+ return data;
+ }
+
+ let response = await fetch(`${COVERAGE_BACKEND_HOST}/v2/filters`);
+ data = await response.json();
+
+ cache_set(filters_cache, '', data);
-function is_enabled(opt) {
- let elem = document.getElementById(opt);
- return elem.checked;
+ return data;
}
-function monitor_options(opts, callback) {
- for (let opt of opts) {
- let elem = document.getElementById(opt);
- elem.onchange = callback;
+
+// Option handling.
+
+export function is_enabled(opt) {
+ let route = readRoute();
+ let value = 'off';
+ if (route[opt]) {
+ value = route[opt];
+ } else if (ZERO_COVERAGE_FILTERS[opt]) {
+ value = ZERO_COVERAGE_FILTERS[opt].default_value;
}
+ return value === 'on';
}
+function monitor_options() {
+ // Monitor input & select changes
+ let fields = document.querySelectorAll('input, select');
+ for(let field of fields) {
+ if (field.type == 'text') {
+ // React on enter
+ field.onkeydown = async (evt) => {
+ if(evt.keyCode === 13) {
+ let params = {};
+ params[evt.target.name] = evt.target.value;
+ updateRoute(params);
+ }
+ }
+ } else {
+ // React on change
+ field.onchange = async (evt) => {
+ let value = evt.target.value;
+ if (evt.target.type == 'checkbox') {
+ value = evt.target.checked ? 'on' : 'off';
+ }
+ let params = {};
+ params[evt.target.name] = value;
+ updateRoute(params);
+ }
+ }
+ }
+}
// hgmo.
@@ -267,14 +320,14 @@ export function build_navbar(path, revision) {
let links = [
{
'name': 'mozilla-central',
- 'path': '',
+ 'route': buildRoute({path: '', revision})
}
];
return links.concat(path.split('/').map(file => {
base += (base ? '/' : '') + file;
return {
'name': file,
- 'path': base,
+ 'route': buildRoute({path: base, revision})
};
}));
}
diff --git a/frontend/src/index.js b/frontend/src/index.js
index 55185ad1b..352ba750f 100644
--- a/frontend/src/index.js
+++ b/frontend/src/index.js
@@ -1,5 +1,6 @@
-import {REV_LATEST, DOM_READY, main, show, hide, message, get_path_coverage, get_history, get_zero_coverage_data, build_navbar, render, get_source} from './common.js';
-import {zero_coverage_display} from './zero_coverage_report.js';
+import {REV_LATEST, DOM_READY, main, show, hide, message, get_path_coverage, get_history, get_zero_coverage_data, build_navbar, render, get_source, get_filters} from './common.js';
+import {buildRoute, readRoute, updateRoute} from './route.js';
+import {zero_coverage_display, zero_coverage_menu} from './zero_coverage_report.js';
import './style.css';
import Prism from 'prismjs';
import Chartist from 'chartist';
@@ -8,6 +9,26 @@ import 'chartist/dist/chartist.css';
const VIEW_ZERO_COVERAGE = 'zero';
const VIEW_BROWSER = 'browser';
+
+function browser_menu(revision, filters, route) {
+ let context = {
+ revision,
+ platforms: filters.platforms.map((p) => {
+ return {
+ 'name': p,
+ 'selected': p == route.platform,
+ }
+ }),
+ suites: filters.suites.map((s) => {
+ return {
+ 'name': s,
+ 'selected': s == route.suite,
+ }
+ }),
+ };
+ render('menu_browser', context, 'menu');
+}
+
async function graphHistory(history, path) {
if (history === null) {
message('warning', `No history data for ${path}`);
@@ -53,7 +74,7 @@ async function graphHistory(history, path) {
// Load revision from graph when a point is clicked
let revision = history[evt.index].changeset;
evt.element._node.onclick = function(){
- updateHash(revision, path);
+ updateRoute({revision});
};
// Display revision from graph when a point is overed
@@ -71,7 +92,12 @@ async function graphHistory(history, path) {
async function showDirectory(dir, revision, files) {
let context = {
navbar: build_navbar(dir, revision),
- files: files,
+ files: files.map(file => {
+ file.route = buildRoute({
+ path: file.path
+ });
+ return file;
+ }),
revision: revision || REV_LATEST,
file_name: function(){
// Build filename relative to current dir
@@ -125,49 +151,30 @@ async function showFile(file, revision) {
Prism.highlightAll(output);
}
-function readHash() {
- // Reads changeset & path from current URL hash
- let hash = window.location.hash.substring(1);
- let pos = hash.indexOf(':');
- if (pos === -1) {
- return ['', ''];
- }
- return [
- hash.substring(0, pos),
- hash.substring(pos+1),
- ]
-}
-
-function updateHash(newChangeset, newPath) {
- // Set the URL hash with both changeset & path
- let [changeset, path] = readHash();
- changeset = newChangeset || changeset || REV_LATEST;
- path = newPath || path || '';
- window.location.hash = '#' + changeset + ':' + path;
-}
-
async function load() {
- let [revision, path] = readHash();
+ let route = readRoute();
// Reset display, dom-safe
hide('history');
hide('output');
- message('loading', 'Loading coverage data for ' + (path || 'mozilla-central') + ' @ ' + (revision || REV_LATEST));
+ message('loading', 'Loading coverage data for ' + (route.path || 'mozilla-central') + ' @ ' + (route.revision || REV_LATEST));
// Load only zero coverage for that specific view
- if (revision === VIEW_ZERO_COVERAGE) {
+ if (route.view === VIEW_ZERO_COVERAGE) {
let zero_coverage = await get_zero_coverage_data();
return {
view: VIEW_ZERO_COVERAGE,
- path,
+ path: route.path,
zero_coverage,
+ route,
}
}
try {
- var [coverage, history] = await Promise.all([
- get_path_coverage(path, revision),
- get_history(path),
+ var [coverage, history, filters] = await Promise.all([
+ get_path_coverage(route.path, route.revision, route.platform, route.suite),
+ get_history(route.path, route.platform, route.suite),
+ get_filters(),
]);
} catch (err) {
console.warn('Failed to load coverage', err);
@@ -178,54 +185,39 @@ async function load() {
return {
view: VIEW_BROWSER,
- path,
- revision,
+ path: route.path,
+ revision: route.revision,
+ route,
coverage,
history,
+ filters,
};
}
async function display(data) {
- // Toggle menu per views
- if (data.view === VIEW_BROWSER) {
- show('menu_browser');
- hide('menu_zero');
- } else if (data.view === VIEW_ZERO_COVERAGE) {
- show('menu_zero');
- hide('menu_browser');
- } else {
- message('error', 'Invalid view : ' + data.view);
- }
-
- // Revision input management
- const revision = document.getElementById('revision');
- revision.onkeydown = async function(evt){
- if(evt.keyCode === 13) {
- updateHash(data.revision.value);
- }
- };
-
- // Also update the revision element
- if (data.revision && data.revision != REV_LATEST) {
- let input = document.getElementById('revision');
- input.value = data.revision;
- }
-
if (data.view === VIEW_ZERO_COVERAGE ) {
+ await zero_coverage_menu(data.route);
await zero_coverage_display(data.zero_coverage, data.path);
- } else if (data.view === VIEW_BROWSER && data.coverage.type === 'directory') {
- hide('message');
- await graphHistory(data.history, data.path);
- await showDirectory(data.path, data.revision, data.coverage.children);
+ } else if (data.view === VIEW_BROWSER) {
+ browser_menu(data.revision, data.filters, data.route);
- } else if (data.view === VIEW_BROWSER && data.coverage.type === 'file') {
- await showFile(data.coverage, data.revision);
+ if (data.coverage.type === 'directory') {
+ hide('message');
+ await graphHistory(data.history, data.path);
+ await showDirectory(data.path, data.revision, data.coverage.children);
+
+ } else if (data.coverage.type === 'file') {
+ await showFile(data.coverage, data.revision);
+
+ } else {
+ message('error', 'Invalid file type: ' + data.coverate.type);
+ }
} else {
- message('error', 'Invalid file type: ' + data.coverage.type);
+ message('error', 'Invalid view : ' + data.view);
}
}
-main(load, display, ['third_party', 'headers', 'completely_uncovered', 'cpp', 'js', 'java', 'rust', 'last_push'])
+main(load, display);
diff --git a/frontend/src/route.js b/frontend/src/route.js
new file mode 100644
index 000000000..518645496
--- /dev/null
+++ b/frontend/src/route.js
@@ -0,0 +1,43 @@
+import {REV_LATEST} from './common.js';
+
+export function readRoute() {
+ // Reads all filters from current URL hash
+ let hash = window.location.hash.substring(1);
+ let pairs = hash.split('&');
+ let out = {}
+ pairs.forEach(pair => {
+ let [key, value] = pair.split('=');
+ if(!key) {
+ return
+ }
+ out[decodeURIComponent(key)] = decodeURIComponent(value);
+ });
+
+ // Default values
+ if (!out.revision) {
+ out.revision = REV_LATEST;
+ }
+ if (!out.path) {
+ out.path = '';
+ }
+
+ return out;
+}
+
+export function buildRoute(params) {
+ // Add all params on top of current route
+ let route = readRoute();
+ if (params) {
+ route = {...route, ...params}
+ }
+
+ // Build query string from filters
+ return '#' + Object.keys(route)
+ .map(k => encodeURIComponent(k) + '=' + encodeURIComponent(route[k]))
+ .join('&');
+}
+
+export function updateRoute(params) {
+ // Update full hash with an updated url
+ window.location.hash = buildRoute(params);
+}
diff --git a/frontend/src/style.css b/frontend/src/style.css
index 71e33b677..cf8e9c454 100644
--- a/frontend/src/style.css
+++ b/frontend/src/style.css
@@ -24,11 +24,6 @@ header #browser input {
font-family: monospace;
}
-header > div {
- /* By default do not display any menu : managed by JS */
- display: none;
-}
-
#main {
background-color: white;
border-top: 1px solid darkgray;
diff --git a/frontend/src/zero_coverage_report.js b/frontend/src/zero_coverage_report.js
index c935e6e71..dabc908ea 100644
--- a/frontend/src/zero_coverage_report.js
+++ b/frontend/src/zero_coverage_report.js
@@ -1,4 +1,63 @@
-import {hide, message, build_navbar, render, filter_third_party, filter_languages, filter_headers, filter_completely_uncovered, filter_last_push_date} from './common.js';
+import {hide, message, build_navbar, render, filter_third_party, filter_languages, filter_headers, filter_completely_uncovered, filter_last_push_date, is_enabled} from './common.js';
+import {buildRoute} from './route.js';
+
+export const ZERO_COVERAGE_FILTERS = {
+ 'third_party': {
+ name: "Show third-party files",
+ default_value: 'on',
+ },
+ 'headers': {
+ name: 'Show headers',
+ default_value: 'off',
+ },
+ 'completely_uncovered': {
+ name: 'Show completely uncovered files only',
+ default_value: 'off',
+ },
+ 'cpp': {
+ name: 'C/C++',
+ default_value: 'on',
+ },
+ 'js': {
+ name: 'JavaScript',
+ default_value: 'on',
+ },
+ 'java': {
+ name: 'Java',
+ default_value: 'on',
+ },
+ 'rust': {
+ name: 'Rust',
+ default_value: 'on',
+ },
+};
+const ZERO_COVERAGE_PUSHES = {
+ 'all': 'All',
+ 'one_year': '0 < 1 year',
+ 'two_years': '1 < 2 years',
+ 'older_than_two_years': 'Older than 2 years',
+}
+
+
+export function zero_coverage_menu(route){
+ let context = {
+ filters: Object.entries(ZERO_COVERAGE_FILTERS).map(([key, filter]) => {
+ return {
+ key,
+ message: filter.name,
+ checked: is_enabled(key),
+ }
+ }),
+ last_pushes: Object.entries(ZERO_COVERAGE_PUSHES).map(([value, message]) => {
+ return {
+ value,
+ message,
+ selected: route['last_push'] === value,
+ }
+ }),
+ };
+ render('menu_zero', context, 'menu');
+}
function sort_entries(entries) {
@@ -104,9 +163,13 @@ export async function zero_coverage_display(data, dir) {
entry_url : function() {
let path = dir + this.dir;
if (this.stats.children != 0) {
- return `#zero:${path}`;
+ return buildRoute({
+ view: 'zero',
+ path,
+ })
} else {
- return `#${revision}:${path}`;
+ // Fully reset the url when moving back to browser view
+ return `#view=browser&revision=${revision}&path=${path}`;
}
},
navbar: build_navbar(dir),