Skip to content
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(flow): report api #13374

Merged
merged 12 commits into from
Nov 18, 2021
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions flow-report/assets/standalone-flow-template.html
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,10 @@
<body>
<noscript>Lighthouse report requires JavaScript. Please enable.</noscript>

<main class="flow-vars lh-root lh-vars"><!-- report populated here --></main>
<main><!-- report populated here --></main>

<script>window.__LIGHTHOUSE_FLOW_JSON__ = %%LIGHTHOUSE_FLOW_JSON%%;</script>
<script>%%LIGHTHOUSE_FLOW_JAVASCRIPT%%
__initLighthouseFlowReport__();
</script>
<script>%%LIGHTHOUSE_FLOW_JAVASCRIPT%%</script>
<script>console.log('window.__LIGHTHOUSE_FLOW_JSON__', __LIGHTHOUSE_FLOW_JSON__);</script>
</body>
</html>
17 changes: 7 additions & 10 deletions flow-report/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import {FunctionComponent} from 'preact';
import {useLayoutEffect, useRef, useState} from 'preact/hooks';

import {ReportRendererProvider} from './wrappers/report-renderer';
import {Sidebar} from './sidebar/sidebar';
import {Summary} from './summary/summary';
import {classNames, FlowResultContext, useHashState} from './util';
Expand Down Expand Up @@ -52,15 +51,13 @@ export const App: FunctionComponent<{flowResult: LH.FlowResult}> = ({flowResult}
const [collapsed, setCollapsed] = useState(false);
return (
<FlowResultContext.Provider value={flowResult}>
<ReportRendererProvider>
<I18nProvider>
<div className={classNames('App', {'App--collapsed': collapsed})} data-testid="App">
<Topbar onMenuClick={() => setCollapsed(c => !c)} />
<Sidebar/>
<Content/>
</div>
</I18nProvider>
</ReportRendererProvider>
<I18nProvider>
<div className={classNames('App', {'App--collapsed': collapsed})} data-testid="App">
<Topbar onMenuClick={() => setCollapsed(c => !c)} />
<Sidebar/>
<Content/>
</div>
</I18nProvider>
</FlowResultContext.Provider>
);
};
11 changes: 4 additions & 7 deletions flow-report/src/topbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,17 @@ import {getFilenamePrefix} from '../../report/generator/file-namer';
import {useLocalizedStrings} from './i18n/i18n';
import {HamburgerIcon, InfoIcon} from './icons';
import {useFlowResult} from './util';
import {useReportRenderer} from './wrappers/report-renderer';
import {saveFile} from '../../report/renderer/api';

import type {DOM} from '../../report/renderer/dom';

function saveHtml(flowResult: LH.FlowResult, dom: DOM) {
function saveHtml(flowResult: LH.FlowResult) {
const htmlStr = document.documentElement.outerHTML;
const blob = new Blob([htmlStr], {type: 'text/html'});

const lhr = flowResult.steps[0].lhr;
const name = flowResult.name.replace(/\s/g, '-');
const filename = getFilenamePrefix(name, lhr.fetchTime);

dom.saveFile(blob, filename);
saveFile(blob, filename);
}

/* eslint-disable max-len */
Expand Down Expand Up @@ -78,7 +76,6 @@ const TopbarButton: FunctionComponent<{
export const Topbar: FunctionComponent<{onMenuClick: JSX.MouseEventHandler<HTMLButtonElement>}> =
({onMenuClick}) => {
const flowResult = useFlowResult();
const {dom} = useReportRenderer();
const strings = useLocalizedStrings();
const [showHelpDialog, setShowHelpDialog] = useState(false);

Expand All @@ -92,7 +89,7 @@ export const Topbar: FunctionComponent<{onMenuClick: JSX.MouseEventHandler<HTMLB
</div>
<div className="Topbar__title">{strings.title}</div>
<TopbarButton
onClick={() => saveHtml(flowResult, dom)}
onClick={() => saveHtml(flowResult)}
label="Button that saves the report as HTML"
>{strings.save}</TopbarButton>
<div style={{flexGrow: 1}} />
Expand Down
29 changes: 28 additions & 1 deletion flow-report/src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*/

import {createContext} from 'preact';
import {useContext, useEffect, useMemo, useState} from 'preact/hooks';
import {useContext, useEffect, useLayoutEffect, useMemo, useRef, useState} from 'preact/hooks';

import type {UIStringsType} from './i18n/ui-strings';

Expand Down Expand Up @@ -121,6 +121,32 @@ function useHashState(): LH.FlowResult.HashState|null {
}, [indexString, flowResult, anchor]);
}

/**
* Creates a DOM subtree from non-preact code (e.g. LH report renderer).
* @param renderCallback Callback that renders a DOM subtree.
* @param inputs Changes to these values will trigger a re-render of the DOM subtree.
* @return Reference to the element that will contain the DOM subtree.
*/
function useExternalRenderer<T extends Element>(
renderCallback: () => Node,
inputs?: ReadonlyArray<unknown>
) {
const ref = useRef<T>(null);

useLayoutEffect(() => {
if (!ref.current) return;

const root = renderCallback();
ref.current.appendChild(root);

return () => {
if (ref.current?.contains(root)) ref.current.removeChild(root);
};
}, inputs);

return ref;
}

export {
FlowResultContext,
classNames,
Expand All @@ -131,4 +157,5 @@ export {
useFlowResult,
useHashParams,
useHashState,
useExternalRenderer,
};
33 changes: 9 additions & 24 deletions flow-report/src/wrappers/category-score.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,37 +5,22 @@
*/

import {FunctionComponent} from 'preact';
import {useEffect, useLayoutEffect, useRef} from 'preact/hooks';

import {useReportRenderer} from './report-renderer';
import {renderCategoryScore} from '../../../report/renderer/api';
import {useExternalRenderer} from '../util';

export const CategoryScore: FunctionComponent<{
category: LH.ReportResult.Category,
href: string,
gatherMode: LH.Result.GatherMode,
}> = ({category, href, gatherMode}) => {
const {categoryRenderer} = useReportRenderer();
const ref = useRef<HTMLDivElement>(null);

useLayoutEffect(() => {
const el = categoryRenderer.renderCategoryScore(category, {}, {gatherMode});

// Category label is displayed in the navigation header.
const label = el.querySelector('.lh-gauge__label,.lh-fraction__label');
if (label) label.remove();

if (ref.current) ref.current.append(el);
return () => {
if (ref.current && ref.current.contains(el)) {
ref.current.removeChild(el);
}
};
}, [categoryRenderer, category]);

useEffect(() => {
const anchor = ref.current && ref.current.querySelector('a') as HTMLAnchorElement;
if (anchor) anchor.href = href;
}, [href]);
const ref = useExternalRenderer<HTMLDivElement>(() => {
return renderCategoryScore(category, {
gatherMode,
omitLabel: true,
hrefOverride: href,
});
}, [category, href]);

return (
<div ref={ref} data-testid="CategoryScore"/>
Expand Down
17 changes: 4 additions & 13 deletions flow-report/src/wrappers/markdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,13 @@
*/

import {FunctionComponent} from 'preact';
import {useLayoutEffect, useRef} from 'preact/hooks';

import {useReportRenderer} from '../wrappers/report-renderer';
import {convertMarkdownCodeSnippets} from '../../../report/renderer/api';
import {useExternalRenderer} from '../util';

export const Markdown: FunctionComponent<{text: string}> = ({text}) => {
const {dom} = useReportRenderer();
const ref = useRef<HTMLSpanElement>(null);

useLayoutEffect(() => {
if (ref.current) {
const md = dom.convertMarkdownCodeSnippets(text);
ref.current.appendChild(md);
}
return () => {
if (ref.current) ref.current.innerHTML = '';
};
const ref = useExternalRenderer<HTMLSpanElement>(() => {
return convertMarkdownCodeSnippets(text);
}, [text]);

return <span ref={ref}/>;
Expand Down
53 changes: 0 additions & 53 deletions flow-report/src/wrappers/report-renderer.tsx

This file was deleted.

82 changes: 25 additions & 57 deletions flow-report/src/wrappers/report.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,73 +5,41 @@
*/

import {FunctionComponent} from 'preact';
import {useLayoutEffect, useRef} from 'preact/hooks';

import {ElementScreenshotRenderer} from '../../../report/renderer/element-screenshot-renderer';
import {getFullPageScreenshot} from '../util';
import {useReportRenderer} from './report-renderer';
import {renderReport} from '../../../report/renderer/api.js';
import {useExternalRenderer} from '../util';

/**
* The default behavior of anchor links is not compatible with the flow report's hash navigation.
* This function converts any anchor links under the provided element to a flow report link.
* This function converts a category score anchor link to a flow report link.
* e.g. <a href="#link"> -> <a href="#index=0&anchor=link">
*/
function convertChildAnchors(element: HTMLElement, index: number) {
const links = element.querySelectorAll('a[href]') as NodeListOf<HTMLAnchorElement>;
for (const link of links) {
// Check if the link destination is in the report.
const currentUrl = new URL(location.href);
currentUrl.hash = '';
currentUrl.search = '';
const linkUrl = new URL(link.href);
linkUrl.hash = '';
linkUrl.search = '';
if (currentUrl.href !== linkUrl.href || !link.hash) continue;

const nodeId = link.hash.substr(1);
link.hash = `#index=${index}&anchor=${nodeId}`;
link.onclick = e => {
e.preventDefault();
const el = document.getElementById(nodeId);
if (el) el.scrollIntoView();
};
}
function convertAnchor(link: HTMLAnchorElement, index: number) {
// Clear existing event listeners by cloning node.
const newLink = link.cloneNode(true) as HTMLAnchorElement;
if (!newLink.hash) return newLink;

const nodeId = link.hash.substr(1);
newLink.hash = `#index=${index}&anchor=${nodeId}`;
newLink.onclick = e => {
e.preventDefault();
const el = document.getElementById(nodeId);
if (el) el.scrollIntoView();
};
return newLink;
}

const Report: FunctionComponent<{hashState: LH.FlowResult.HashState}> =
export const Report: FunctionComponent<{hashState: LH.FlowResult.HashState}> =
({hashState}) => {
const {dom, reportRenderer} = useReportRenderer();
const ref = useRef<HTMLDivElement>(null);

useLayoutEffect(() => {
if (ref.current) {
dom.clearComponentCache();
reportRenderer.renderReport(hashState.currentLhr, ref.current);
convertChildAnchors(ref.current, hashState.index);
const fullPageScreenshot = getFullPageScreenshot(hashState.currentLhr);
if (fullPageScreenshot) {
ElementScreenshotRenderer.installOverlayFeature({
dom,
rootEl: ref.current,
overlayContainerEl: ref.current,
fullPageScreenshot,
});
}
const topbar = ref.current.querySelector('.lh-topbar');
if (topbar) topbar.remove();
}

return () => {
if (ref.current) ref.current.textContent = '';
};
}, [reportRenderer, hashState]);
const ref = useExternalRenderer<HTMLDivElement>(() => {
return renderReport(hashState.currentLhr, {
disableAutoDarkModeAndFireworks: true,
omitTopbar: true,
anchorTransform: link => convertAnchor(link, hashState.index),
});
}, [hashState]);

return (
<div ref={ref} className="lh-root" data-testid="Report"/>
<div ref={ref} data-testid="Report"/>
);
};

export {
convertChildAnchors,
Report,
};
4 changes: 2 additions & 2 deletions flow-report/standalone-flow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ import {render} from 'preact';

import {App} from './src/app';

// Used by standalone-flow.html
function __initLighthouseFlowReport__() {
// TODO(adamraine): add lh-vars, etc classes programmatically instead of in the HTML template
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Drive-by resolving this TODO

const container = document.body.querySelector('main');
if (!container) throw Error('Container element not found');
container.classList.add('flow-vars', 'lh-root', 'lh-vars');
render(<App flowResult={window.__LIGHTHOUSE_FLOW_JSON__} />, container);
}

window.__initLighthouseFlowReport__ = __initLighthouseFlowReport__;
window.__initLighthouseFlowReport__();
Loading