From 77a5e472d1e748b5ed757ecb363bddcc9261b9c8 Mon Sep 17 00:00:00 2001 From: Bastien Abadie Date: Fri, 6 Sep 2019 10:46:29 +0200 Subject: [PATCH 1/3] frontend: Store input states in url, fixes #101. --- frontend/src/base.html | 49 ++++++------ frontend/src/common.js | 55 +++++++++----- frontend/src/index.js | 108 +++++++++++---------------- frontend/src/route.js | 42 +++++++++++ frontend/src/style.css | 5 -- frontend/src/zero_coverage_report.js | 46 +++++++++++- 6 files changed, 187 insertions(+), 118 deletions(-) create mode 100644 frontend/src/route.js diff --git a/frontend/src/base.html b/frontend/src/base.html index afa39072a..3e565091f 100755 --- a/frontend/src/base.html +++ b/frontend/src/base.html @@ -10,7 +10,7 @@

: {{ total }} files @@ -47,7 +47,7 @@

@@ -73,7 +73,7 @@

: {{ files.length }} files @@ -88,7 +88,7 @@

{{#files}}
- {{ file_name }} + {{ file_name }} {{ children }} {{ coveragePercent }} %
@@ -102,29 +102,26 @@

Revision {{ revision }} from {{ date }} -
- + - -
+ + +
Loading...
diff --git a/frontend/src/common.js b/frontend/src/common.js index f0d0c7e25..b6e6a3d1e 100644 --- a/frontend/src/common.js +++ b/frontend/src/common.js @@ -1,39 +1,34 @@ import Mustache from 'mustache'; +import { buildRoute, readRoute, updateRoute } from './route.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; @@ -134,18 +129,38 @@ export async function get_zero_coverage_data() { // Option handling. function is_enabled(opt) { - let elem = document.getElementById(opt); - return elem.checked; + let route = readRoute(); + return route[opt] === 'on'; } -function monitor_options(opts, callback) { - for (let opt of opts) { - let elem = document.getElementById(opt); - elem.onchange = callback; +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. export async function get_source(file) { @@ -267,14 +282,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..8f9ce0502 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 {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,14 @@ import 'chartist/dist/chartist.css'; const VIEW_ZERO_COVERAGE = 'zero'; const VIEW_BROWSER = 'browser'; + +function browser_menu(revision) { + let context = { + revision, + }; + render('menu_browser', context, 'menu'); +} + async function graphHistory(history, path) { if (history === null) { message('warning', `No history data for ${path}`); @@ -53,7 +62,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 +80,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 +139,29 @@ 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), + get_path_coverage(route.path, route.revision), + get_history(route.path), ]); } catch (err) { console.warn('Failed to load coverage', err); @@ -178,8 +172,9 @@ async function load() { return { view: VIEW_BROWSER, - path, - revision, + path: route.path, + revision: route.revision, + route, coverage, history, }; @@ -187,45 +182,28 @@ async function load() { 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); + + 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.view === VIEW_BROWSER && data.coverage.type === 'file') { - await showFile(data.coverage, data.revision); + } 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..aff4ce377 --- /dev/null +++ b/frontend/src/route.js @@ -0,0 +1,42 @@ +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..2e69438a9 100644 --- a/frontend/src/zero_coverage_report.js +++ b/frontend/src/zero_coverage_report.js @@ -1,4 +1,42 @@ import {hide, message, build_navbar, render, filter_third_party, filter_languages, filter_headers, filter_completely_uncovered, filter_last_push_date} from './common.js'; +import {buildRoute} from './route.js'; + +const ZERO_COVERAGE_FILTERS = { + 'third_party': "Show third-party files", + 'headers': 'Show headers', + 'completely_uncovered': 'Show completely uncovered files only', + 'cpp': 'C/C++', + 'js': 'JavaScript', + 'java': 'Java', + 'rust': 'Rust', +}; +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, message]) => { + return { + key, + message, + checked: route[key] === 'on', + } + }), + 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 +142,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), From 9284072b1afb77f864411c13b817792cc134efc1 Mon Sep 17 00:00:00 2001 From: Bastien Abadie Date: Wed, 21 Aug 2019 17:14:31 +0200 Subject: [PATCH 2/3] frontend: Support suite & platform filters. --- frontend/src/base.html | 13 ++++++++++++ frontend/src/common.js | 45 +++++++++++++++++++++++++++++++++++------- frontend/src/index.js | 26 ++++++++++++++++++------ 3 files changed, 71 insertions(+), 13 deletions(-) diff --git a/frontend/src/base.html b/frontend/src/base.html index 3e565091f..fc3d312cc 100755 --- a/frontend/src/base.html +++ b/frontend/src/base.html @@ -106,6 +106,19 @@

View the zero coverage report + • + +