-
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
proposal: report api #12772
proposal: report api #12772
Conversation
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.
wasn't clear if this API was meant to be long-lived or a temporary transition but I reviewed as if it were the final answer :)
thanks for compiling the use cases!
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.
wasn't clear if this API was meant to be long-lived or a temporary transition
meant to be long-lived yup!
PTAL. It's all been revised |
types/report-renderer.d.ts
Outdated
mainEl: HTMLElement; | ||
headerEl: HTMLElement; | ||
categoriesEl: HTMLElement; | ||
footerEl: HTMLElement; |
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.
ah yea these are all components as found in #12803 . can we use just the component name for the key here?
{
topbar: HTMLElement;
main: HTMLElement;
header: HTMLElement;
category: HTMLElement[];
footer: HTMLElement;
}
except lh-category
is made via JS and isn't one of our "components" according to #12803 ...
instead of any of this, using a different name here would work for me. ReportPartials
? and then rename renderReportComponents
to renderPartialReport
or renderReportParts
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.
stepping back a bit... why would a client not just be able to call renderFullReport
and pluck out what they need?
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.
renderReportPartials
SGTM
why would a client not just be able to call renderFullReport and pluck out what they need?
Because the contract is that the element that is returned is a live, LH-owned element that will update itself and if you break it up, all bets are off. If don't want to take over ownership of all interactivity in the report, but need to rearrange some layout, renderReportComponents
allows you to place individual live elements wherever you wish.
Given our current embedding system, I think the difference is relatively minor, but that won't be true for flow reports.
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.
+1 on partials. great call. these are not 1:1 with our new "components" concept so i love using a different term.
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.
Because the contract is that the element that is returned is a live, LH-owned element that will update itself and if you break it up, all bets are off. If don't want to take over ownership of all interactivity in the report, but need to rearrange some layout,
renderReportComponents
allows you to place individual live elements wherever you wish.
The makes total sense, but this also makes me question how much these components can actually be live and update themselves. A footer element I get, but recently we've trended toward more interactivity, and we've had difficulty keeping them encapsulated. Easy examples off the top of my head:
- coordinating scrolling in the categories with the indicator on the sticky header
- we have these individual components, but the screenshot lightbox still goes into
overlayParentEl
passed in the options and clicking on screenshots opens it? Or shouldrenderReportComponents
not get fullpage screenshots? - (I'm sure there are other examples)
That's what I was getting at above with asking if renderFullReport
is going to call this internally: are we going to have two versions of these components, independent mode and in-an-web-app mode? Or everyone uses independent mode and renderFullReport
layers on JS between components on top of that (report-ui-features
++)?
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.
makes me question how much these components can actually be live and update themselves
coordinating scrolling in the categories with the indicator on the sticky header
Is this a technical feasibility question? Or rhetorical/an objection to an API that returns separate elements that share some global state?
we have these individual components, but the screenshot lightbox still goes into overlayParentEl passed in the options and clicking on screenshots opens it
Is this a request for screenshotOverlay
as a partial instead? If so, I like that :)
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.
makes me question how much these components can actually be live and update themselves
coordinating scrolling in the categories with the indicator on the sticky header
Is this a technical feasibility question? Or rhetorical/an objection to an API that returns separate elements that share some global state?
Feasibility and/or asking for specifics :) How are we handling inter-component interactions with this API?
Is this a request for
screenshotOverlay
as a partial instead? If so, I like that :)
That might make the most sense, yeah. But again with how are the inter-component interactions going to be/should be set up?
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.
The specifics vary depending on what deps we can use, but if we're sticking with imperative onlistener changes approach for example...
function renderHeader(containerEl, rootState, setState, onRender) {
containerEl.innerHTML = `<h1>Header y:<span>${rootState.y}</span></h1>`
onRender(rootState => containerEl.querySelector('span').textContent = rootState.x)
return containerEl
}
function renderCategories(containerEl, rootState, setState, onRender) {
containerEl.innerHTML = `...`
containerEl.onscroll = () => setState({y: containerEl.scrollTop})
return containerEl
}
function renderPartials() {
const rootState = {y: 0}
const headerEl = document.createElement('div')
const categoriesEl = document.createElement('div')
const listeners = []
const onRender = listener => listeners.push(listener)
const setState = state => {
for (const listener of listeners) listener(state)
}
return {
header: renderHeader(headerEl, rootState, setState, onRender),
categories: renderCategories(categoriesEl, rootState, setState, onRender),
}
}
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.
Right, so is the intention that renderReportPartials()
would return elements bound to each other to replicate the interactions present in what's generated by renderReport()
? That's what I'm asking. If so, can we write that down as part of this API? :)
I think that could absolutely suit us just fine, but this does increase testing surface area, e.g. if the overlay becomes a partial, with the current renderer the screenshot lightbox is removed on click. If someone doesn't use the overlay, every click on a thumbnail would just keep appending new lightboxes to the detached overlay element.
Obviously there can be some DIY/use at your own risk aspect to this, but the more automatic bindings there are, the greater the chance of this type of thing, and the more combinatoric testing we need to do (vs a system of user-controlled hooks between the components where we can get away with relatively more unit and less integration testing).
types/report-renderer.d.ts
Outdated
disableAutoDarkModeAndFireworks?: boolean; | ||
|
||
/** If defined, the 'Save as Gist' item in the topbar dropdown will be shown and when clicked, will run this function. */ | ||
onSaveGist?: (lhr: LH.Result) => string; |
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.
currently there's a some indirection with how "save as gist" code runs ... in base ReportUIFeatures a saveAsGist
fn simply throws and error, and Viewer overrides it https://github.com/GoogleChrome/lighthouse/blob/2d796ea/lighthouse-viewer/app/src/viewer-ui-features.js#L54 but that still defers to a passed-in callback function. the impl is _onSaveJson
https://github.com/GoogleChrome/lighthouse/blob/2d796ea/lighthouse-viewer/app/src/lighthouse-report-viewer.js#L286
ideally the report/renderer
code would just know how to save as gist and clients can enable as they wish, but we wouldn't want to ship firebase and idb to support GithubApi
...
perhaps a better approach would be providing a way to make a custom action in the dropdown menu. then in the options here, instead of a specific "give us a saveGist callback and we will wire it to an otherwise stubbed out and hidden dropdown action", we allow clients to define any action in that menu they want.
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.
perhaps a better approach would be providing a way to make a custom action in the dropdown menu. then in the options here, instead of a specific "give us a saveGist callback and we will wire it to an otherwise stubbed out and hidden dropdown action", we allow clients to define any action in that menu they want.
I like this a lot, but I wonder if that opens us up to more flexibility (and complexity) than we need. The current implementation of dropdown actions calls e.preventDefault
and manually handles each data-action
. I could see an eventual reportToolsMenuItems: Array<{label: string, iconUrl: string, onClick: () => void, position?: number}>
but without additional compelling use cases, I'm in favor of the onSaveGist
approach 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.
I agree that providing an API to define custom topbar menu actions would be ideal.. but i also agree with patrick that it's overkill for now. so i'd also like to keep this solution low key.
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 like this API! so happy to see it taking shape :)
types/report-renderer.d.ts
Outdated
mainEl: HTMLElement; | ||
headerEl: HTMLElement; | ||
categoriesEl: HTMLElement; | ||
footerEl: HTMLElement; |
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.
renderReportPartials
SGTM
why would a client not just be able to call renderFullReport and pluck out what they need?
Because the contract is that the element that is returned is a live, LH-owned element that will update itself and if you break it up, all bets are off. If don't want to take over ownership of all interactivity in the report, but need to rearrange some layout, renderReportComponents
allows you to place individual live elements wherever you wish.
Given our current embedding system, I think the difference is relatively minor, but that won't be true for flow reports.
types/report-renderer.d.ts
Outdated
disableAutoDarkModeAndFireworks?: boolean; | ||
|
||
/** If defined, the 'Save as Gist' item in the topbar dropdown will be shown and when clicked, will run this function. */ | ||
onSaveGist?: (lhr: LH.Result) => string; |
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.
perhaps a better approach would be providing a way to make a custom action in the dropdown menu. then in the options here, instead of a specific "give us a saveGist callback and we will wire it to an otherwise stubbed out and hidden dropdown action", we allow clients to define any action in that menu they want.
I like this a lot, but I wonder if that opens us up to more flexibility (and complexity) than we need. The current implementation of dropdown actions calls e.preventDefault
and manually handles each data-action
. I could see an eventual reportToolsMenuItems: Array<{label: string, iconUrl: string, onClick: () => void, position?: number}>
but without additional compelling use cases, I'm in favor of the onSaveGist
approach 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.
excited about this, but a little unclear about how some of it matches up to implementation. Don't let my skepticism on some particulars sound too negative for the overall approach :)
types/report-renderer.d.ts
Outdated
overlayParentEl?: HTMLElement; | ||
|
||
/** Callback running after a DOM element (like .lh-node or .lh-source-location) has been created */ | ||
onDetailsItemRendered?: (type: string, el: HTMLElement, value: any) => void; |
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 type
and value
audit details types?
types/report-renderer.d.ts
Outdated
mainEl: HTMLElement; | ||
headerEl: HTMLElement; | ||
categoriesEl: HTMLElement; | ||
footerEl: HTMLElement; |
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.
Because the contract is that the element that is returned is a live, LH-owned element that will update itself and if you break it up, all bets are off. If don't want to take over ownership of all interactivity in the report, but need to rearrange some layout,
renderReportComponents
allows you to place individual live elements wherever you wish.
The makes total sense, but this also makes me question how much these components can actually be live and update themselves. A footer element I get, but recently we've trended toward more interactivity, and we've had difficulty keeping them encapsulated. Easy examples off the top of my head:
- coordinating scrolling in the categories with the indicator on the sticky header
- we have these individual components, but the screenshot lightbox still goes into
overlayParentEl
passed in the options and clicking on screenshots opens it? Or shouldrenderReportComponents
not get fullpage screenshots? - (I'm sure there are other examples)
That's what I was getting at above with asking if renderFullReport
is going to call this internally: are we going to have two versions of these components, independent mode and in-an-web-app mode? Or everyone uses independent mode and renderFullReport
layers on JS between components on top of that (report-ui-features
++)?
types/report-renderer.d.ts
Outdated
|
||
declare global { | ||
module LH.Renderer { | ||
export function renderReport(lhr: LH.Result, options?: ReportRendererOptions): HTMLElement; |
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.
is the plan for this to internally call renderReportComponents()
instead of building everything in its own flow?
types/report-renderer.d.ts
Outdated
): HTMLElement; | ||
|
||
// Extra convience if you have just a category score. | ||
export function renderGaugeForScore(num0to1: number): HTMLElement; |
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.
who is this for?
We can close this now as the proposal's design was improved here and most of it implemented. A few items from it are not yet implemented… taking status… /** If defined, the 'Save as Gist' item in the topbar dropdown will be shown and when clicked, will run this function. */
onSaveGist?: (lhr: LH.Result) => string;
/**
* DOM element that will the overlay DOM should be a child of.
* Between stacking contexts and z-index, the overlayParentEl should have a stacking/paint order high enough to cover all elements that the overlay should paint above.
* Defaults to the containerEl, but will be set in PSI to avoid being under the sticky header.
* @see https://philipwalton.com/articles/what-no-one-told-you-about-z-index/ */
overlayParentEl?: HTMLElement;
/** Callback running after a DOM element (like .lh-node or .lh-source-location) has been created */
onDetailsItemRendered?: (
type: LH.Audit.Details['type'],
el: HTMLElement,
value: LH.Audit.Details
) => void;
|
proposing a unified API for the report renderer. (no more subclassing or other weird hooks)
original planning doc: http://go/akqfh 🔒
reviewers: scroll to the bottom of the diff, where the .d.ts is.
Basic idea:
getContainerEl()
andgetTopBarEl()
onDetailsItemRendered
handles DevTools' lh-node linkifying and source-location resolvinggetContainerEl
and if it decides to move the scoregauge and footer to different parts of the DOM, sure. But that's on them.