-
Notifications
You must be signed in to change notification settings - Fork 9.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Report: header, footer, leftnav, share, oh my #2000
Conversation
lighthouse-core/report/v2/logger.js
Outdated
* Logs messages via a UI butter. | ||
* @class | ||
*/ | ||
class Logger { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is a straight port from v1.
/** | ||
* @param {!Document} document | ||
*/ | ||
constructor(document) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is a port from v1. Only difference is that the constructor does not automatically attache event listeners. You have to call attach()
now to add the functionality to the DOM.
*/ | ||
onCopyButtonClick() { | ||
if (window.ga) { | ||
window.ga('send', 'event', 'report', 'copy'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would love to keep GA for viewer. Should be fine since this is a noop for contexts that don't load ga.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we just remove for now since it's non-functional code?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's always been functional in viewer (only context that loads GA atm).
// const reportGenerator = new ReportGeneratorV2(); | ||
// htmlStr = reportGenerator.generateReportHTML(this.json); | ||
// } | ||
// TODO: fix viewer. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Need to make this one work again.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
when we do, instead of generating it on the fly I'd be interested to publish the bundled JS with our releases instead that the viewer can just reference. This way the viewer can actually start supporting all versions going forward too
* @param {string} date | ||
* @return {string} | ||
*/ | ||
function formatDateTime(date) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
port from v1
*/ | ||
renderReport(report) { | ||
renderReport(report, container) { | ||
container.innerHTML = ''; // Remove previous report. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm hoping this one isn't contentious :)
document.body.appendChild(renderer.renderReport(window.__LIGHTHOUSE_JSON__)); | ||
window.addEventListener('DOMContentLoaded', _ => { | ||
window.renderer = new ReportRenderer(document); | ||
const container = document.querySelector('#lighthouse-content'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
made sense to render into a container rather than replace the whole world for every call to render.
|
||
<!-- Lighthouse header --> | ||
<template id="tmpl-lighthouse-heading"> | ||
<style> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For these one-time-use partials....instead of creating separate .css file for each (or adding to the main stylesheet), I chose to keep related styles together with the markup that requires them. One nice thing about this is that these styles don't get added to the page unless a renderer uses the template. So if devtools renders its own header, we're not penalized.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know this is mostly ports, but there's a lot going on without any tests :(
* @return {!DocumentFragment} A clone of the template content. | ||
* @throws {Error} | ||
*/ | ||
cloneTemplate(selector) { | ||
const template = this._document.querySelector(selector); | ||
cloneTemplate(selector, context = this._document) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nice
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i have a conflict with this in flight: https://github.com/GoogleChrome/lighthouse/pull/2002/files#diff-67023326fb4e20403076afc5d18d67f2R51
I can adopt yours pretty easily though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
are you comfortable with using this.templatesContext
instead?
cloneTemplate(selector) {
const template = this.templatesContext.querySelector(selector);
this would be ideal for me. I'd set it right after instantiating the instance.
it would default to this._document
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i have this implemented in #2002.
wdyt?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
|
||
'use strict'; | ||
|
||
/* eslint-env browser */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've been avoiding setting this and preferring explicit globals instead, so when we test in node it's really explicit which globals it depends on, thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, we ca do explicit globals.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
// const reportGenerator = new ReportGeneratorV2(); | ||
// htmlStr = reportGenerator.generateReportHTML(this.json); | ||
// } | ||
// TODO: fix viewer. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
when we do, instead of generating it on the fly I'd be interested to publish the bundled JS with our releases instead that the viewer can just reference. This way the viewer can actually start supporting all versions going forward too
month: 'numeric', day: 'numeric', year: 'numeric', | ||
hour: 'numeric', minute: 'numeric', timeZoneName: 'short' | ||
}; | ||
let formatter = new Intl.DateTimeFormat('en-US', options); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe there's an issue outstanding for our permanent use of en-US
, since we're starting fresh, thoughts on just using .toISOString()
for now and not bringing our baggage with us yet? :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sgtm!
* @param {Blob|File} blob The file to save. | ||
*/ | ||
_saveFile(blob) { | ||
const filename = window.getFilenamePrefix({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we should really get around to fixing getFilenamePrefix
haha
Working on some tests... |
Added tests for the non-interactive stuff (newer header/footer renders). Also added tests for some of the format helpers. |
@@ -44,11 +44,12 @@ class DOM { | |||
|
|||
/** | |||
* @param {string} selector | |||
* @param {Element=} context Parent node to query for templates within. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
{!Element=}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
@@ -0,0 +1,71 @@ | |||
/** |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
does devtools want a logger? This PR might need to adjust to the model in #2002
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IDK. I'm just porting :)
Knowing the separation of LH proper and integrations of LH is still unclear. Right now the operating assumption is that we're keeping everything we currently have b/c #1937 doesn't capture these types of details.
For me, the dream is that there will be all sorts of existing tools that adopt a LH report. It's impossible to foresee all those integration touch points. Rather than designing our system around a particular integration , it might be easier to flip it. IOW, the integration (e.g. devtools) decides what parts of LH it wants to throw away or replace. Removing handlebars, adding templateContext
are a step towards that.
For the features stuff, I've updated it to take the approach in b12f2ea. Devtools can decide not to load the feature (including logger).
Some open thoughts/questions for everyone. "Integration X" can be substituted for "devtools".
- The core report should contain the features we want to show up in the most places. For example, if you run LH in the CLI/extension/WPT/Gulliver/LHaaS, you'll get the UI we vend (logger, export, etc.).
- What happens if you generate an html report in Integration X and open it later? Do you see LH's base UI or their version?
- What happens if you open that same report in Viewer?
- CSS is the easiest way to get rid of base UI:
#lh-log { display: none}
. We're already doing that today.- Alternatively, Integration X loads its own stylesheets and effectively changes the entire report.
- If you also want to nuke code, don't inject
ReportUIFeature
(here), or stub files:class Logger { log() {} }
. We could even provide those files for people if it makes sense.
@@ -0,0 +1,272 @@ | |||
/** |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same for this file and devtools. Is there stuff in here that won't be wanted there?
|
||
/* globals document window URL Blob Logger */ | ||
|
||
class ReportFeatures { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
definitely need a better name. Might as well call it ReportThings
while you're at it :P ReportUIFeatures
? I don't know, but if I wanted to know where copy/paste was handled it definitely wouldn't be here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ReportUIFeatures
sgtm.
|
||
/** | ||
* Adds export button and print functionality to the report. | ||
* @param {ReportJSON=} report |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
{!ReportJSON}
(I don't think it's optional?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not anymore without viewer. Done.
} | ||
} | ||
|
||
warn(msg) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
need jsdoc on these
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
* @return {!Element} | ||
*/ | ||
_renderReport(report) { | ||
const element = this._dom.createElement('div', 'lh-report'); | ||
const container = this._dom.createElement('div', 'lh-content'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
there's a .lh-content
div inside a #lh-content
div? Or is this a old code? Appears to be discarded since element
is returned and attached elsewhere in the DOM
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yea old code. Removed.
/** | ||
* Click handler for export button. | ||
*/ | ||
onExportButtonClick(e) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
need jsdoc on these e
s
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
*/ | ||
onCopyButtonClick() { | ||
if (window.ga) { | ||
window.ga('send', 'event', 'report', 'copy'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we just remove for now since it's non-functional code?
case 'copy': | ||
this.onCopyButtonClick(); | ||
break; | ||
case 'open-viewer': |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
remove?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Disabled this in the UI for now. Left in the code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Disabled this in the UI for now. Left in the code
this is classic YAGNI, though. If usage doesn't remain the same, there's double duty of removing old implementation and adding the new one. Even if usage remains exactly the same, non-functional code and out-of-date comments are a maintenance burden. Meanwhile a pristine copy is over in report v1 ready for copy/paste when that happens.
rebased for ya @ebidel, biggest change was extracting the format helpers to a new file |
Alright, brendan and I walked through the features and components in this PR and identified how we'll handle for the devtools case. 🎅 Will include and use as-is in DevTools
🤶 Will include, but subclass in DevTools
contextMenu.appendItem(menuItemTitleStr, handlerFn);
// ... later on ...
contextMenu.show(); I think this also means we don't have declarative menu items over in 🙍 Will not include in devtools right now
actual action item summary (because so many words (sorry)):
aside from these bits, i'm happy with the rest and looking forward to some nice looks on the report! :) |
I'm also supportive of putting this on as static methods. |
turning on closure checking produced a number of errors in here, some legitimate, some from just things like |
@brendankenny I need your help with the remaining closure issues. Casting didn't take care of
I've added a few other methods that can be overridden. See if that works. See example. below. Instead of worrying about potential (and future) security concerns, Lighthouse should really just go through a full security review before we launch DT. There are more questionable areas of the codebase than opening a tab and sending JSON to it :) I think it's also good to keep features that have already landed in the project. If you really want to omit features from DT, can the DT integration just remove them?
Easy to do already. See example below.
Done! Although, kinda silly that something trivial has turned into something more complex. But hopefully we can use Before if (self.ga) {
self.ga('send', 'event', 'report', 'copy');
} After /**
* Fires a custom DOM event on target.
* @param {string} name Name of the event.
* @param {!Document|!Element} target DOM node to fire the event on.
* @param {Object<{detail: Object<string, *>}>=} detail Custom data to include.
*/
_fireEventOn(name, target = this._document, detail) {
const event = new CustomEvent(name, detail ? {detail} : null);
this._document.dispatchEvent(event);
}
// Elsewhere in the app.
document.addEventListener('lh-analytics', e => {
if (self.ga) {
self.ga(e.detail.cmd, e.detail.fields);
}
});
In the example. Although, the Ok, so here was my thinking for the feature stuff. Choosing what features go in and out of DT is going to be really adhoc. The code separation is only going to get more confusing. What I did with Proof is in the pudding. All of the tweaks suggested above can be done like so: class ReportUIFeaturesDevTools extends ReportUIFeatures {
/**
* Routes Logger to console instead of the UI toast.
* @override
*/
_setupLogger() {
this.logger = console;
this.logger.hide = function() {};
}
// @override
initFeatures(...args) {
super.initFeatures(...args);
// ... devtools adds more stuff at runtime ...
}
// @override
_setupExportButton() {
super._setupExportButton();
// ... devtools uses its API to build the context menu.
// const dropdown = this._dom.find('.lh-export__dropdown', this._document);
// dropdown.textContent = '';
// const menu = UI.ContextMenu();
// menu.appendItem('Save HTML', this.);
// menu.appendItem('Item 2', function handler {});
// menu.appendItem('Item 3', function handler {});
}
// @override
_saveFile(blob) {
// ... devtools calls its own API ...
}
// @override
sendJSONReport() {
// super.sendJSONReport = null; // nuke it if you're paranoid.
console.warn('DT: not implemented yet');
}
// @override
onExport(e) {
// ... devtools does its own thing ...
}
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is a really long PR :( We should split things up next time
|
||
/* globals self URL Blob Logger CustomEvent */ | ||
|
||
class ReportUIFeatures { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should move this file to report-ui-features.js
@@ -0,0 +1,80 @@ | |||
/* | |||
* Copyright 2015 The Closure Compiler Authors. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
2015?
function formatNumber(number) { | ||
return number.toLocaleString(undefined, {maximumFractionDigits: 1}); | ||
} | ||
/* globals self, formatNumber, calculateRating */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this global/module.exports
thing is increasingly difficult to follow. If the export is just for jsdom and tests, I wonder if we should just do a proxied require for testing the report
@@ -50,7 +50,7 @@ class DOM { | |||
|
|||
/** | |||
* @param {string} selector | |||
* @param {!Document|!Element} context | |||
* @param {!Document|!DocumentFragment|!Element} context |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should we just switch this to {!Node}
?
@@ -113,7 +113,7 @@ class DOM { | |||
* Guaranteed context.querySelector. Always returns an element or throws if | |||
* nothing matches query. | |||
* @param {string} query | |||
* @param {!DocumentFragment|!Element} context | |||
* @param {!Document|!DocumentFragment|!Element} context |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
would {!Node}
work? (and for findAll
, below)
self.ReportUIFeatures = ReportUIFeatures; | ||
|
||
/** @type {function({url: string, generatedTime: string}): string} */ | ||
self.getFilenamePrefix; // eslint-disable-line no-unused-expressions |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't this already declared in the global scope?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh, I see the issue, file-namer.js
needs to be added to the closure compiled files list (and should use this one's {url: string, generatedTime: string}
instead of Object
like in that file)
} | ||
|
||
return /** @type {!Element} **/ (element); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can also split into element = this._renderReport(report); container.appendChild(element);
lines if you don't want to cast
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
actually, does this even need to return anything anymore? Return value isn't being used in report-template.html
anymroe, and if adding to the container now, not even needed for testing
url.textContent = report.url; | ||
|
||
const env = this._dom.find('.lh-env__items', header); | ||
report.runtimeConfig.environment.forEach(runtime => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/runtime/env, maybe? (would need to rename env
above to envContainer
or whatever)
}); | ||
|
||
document.addEventListener('lh-analytics', e => { | ||
if (self.ga) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no need for self
, I don't think
* @param {...?} var_args | ||
* @return {?} | ||
*/ | ||
function ga(methodName, var_args) {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think putting functions on this function will actually work. Is the rest of this file needed right now?
Definitely should avoid unknown type and do ...*
for var_args
and no return type if it's not needed. Right now the second argument is being used as !Object<string, *>
and not any more arguments after that. Is that better?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I took this entire thing from closure's common externs.
we definitely need some sanity around dependency injection vs overriding. There's basically no help right now on how to use stuff or how stuff works in these report files except reading all of them. Things that are injected need stuff that's overridden and vice versa, so some clarity with very clear decoupling boundaries would be amazing |
(that would be through a group effort, not expecting you to solve this eric :) |
even easier would be #2000 (comment) :) viewer code doesn't work, it's self contained, and can easily be added once we figure out viewer and get it working |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
okay eric and i just discussed this over chat. the summary:
- move logger to reuse the customEvent stuff.. logging
"lh-log"
events (or whatever) that any one with addEventListener can listen to. - report-helpers -> util.js and install on a UTIL.namespace.
self.UTIL = UTIL
assignment at the bottom. We discussed a better future where these formatting methods are exposed on DetailsRenderer via aformatItem()
withtype == 'datetime'
or'number'
.. but let's move ahead with UTIL in the meantime and refactor afterwards. - nuke viewer stuff from report-features
with those 3 items taken care of i'm good and we can move ahead.
Changes are in and we're 💚 . Dropping the mic 🎤 on this one. Any other changes, ya'll can do in followup prs. |
merging to unblock the merge of other stuff. |
also update closure for mid-month release
R: all
renderReport
(e.g. future where we have re-run audit)Most of the code is porting. I'll try to annotate what's what.