diff --git a/frontend/app/components/Session_/BugReport/BugReportModal.tsx b/frontend/app/components/Session_/BugReport/BugReportModal.tsx index c649f46cd..414ef678c 100644 --- a/frontend/app/components/Session_/BugReport/BugReportModal.tsx +++ b/frontend/app/components/Session_/BugReport/BugReportModal.tsx @@ -2,14 +2,15 @@ import React from 'react'; import { connect } from 'react-redux'; import { countries } from 'App/constants'; import { useStore } from 'App/mstore'; +import { Button } from 'UI'; import { session as sessionRoute } from 'App/routes'; -import { ReportDefaults, EnvData } from './types' -import Session from './components/Session' -import MetaInfo from './components/MetaInfo' -import Title from './components/Title' -import Comments from './components/Comments' -import Steps from './components/Steps' -import { mapEvents } from './utils' +import { ReportDefaults, EnvData } from './types'; +import Session from './components/Session'; +import MetaInfo from './components/MetaInfo'; +import Title from './components/Title'; +import Comments from './components/Comments'; +import Steps from './components/Steps'; +import { mapEvents } from './utils'; interface Props { hideModal: () => void; @@ -23,11 +24,14 @@ interface Props { exceptionsList: Record[]; eventsList: Record[]; endTime: number; - } + }; } function BugReportModal({ hideModal, session, width, height, account, xrayProps }: Props) { - const { bugReportStore } = useStore() + const reportRef = React.createRef(); + const [isRendering, setRendering] = React.useState(false); + + const { bugReportStore } = useStore(); const { userBrowser, userDevice, @@ -43,7 +47,7 @@ function BugReportModal({ hideModal, session, width, height, account, xrayProps events, } = session; - console.log(session.toJS()) + console.log(session.toJS()); const envObject: EnvData = { Device: `${userDevice}${userDeviceType !== userDevice ? ` ${userDeviceType}` : ''}`, @@ -54,9 +58,13 @@ function BugReportModal({ hideModal, session, width, height, account, xrayProps Country: countries[userCountry], }; if (revId) { - Object.assign(envObject, { Rev: revId }) + Object.assign(envObject, { Rev: revId }); } - const sessionUrl = `${window.location.origin}/${window.location.pathname.split('/')[1]}${sessionRoute(sessionId)}` + + const sessionUrl = `${window.location.origin}/${ + window.location.pathname.split('/')[1] + }${sessionRoute(sessionId)}`; + const defaults: ReportDefaults = { author: account.name, env: envObject, @@ -65,32 +73,132 @@ function BugReportModal({ hideModal, session, width, height, account, xrayProps user: userDisplayName, id: sessionId, url: sessionUrl, - } - } - - bugReportStore.updateReportDefaults(defaults) - bugReportStore.setDefaultSteps(mapEvents(events)) + }, + }; React.useEffect(() => { - return () => bugReportStore.clearStore() - }, []) + bugReportStore.updateReportDefaults(defaults); + bugReportStore.setDefaultSteps(mapEvents(events)); + return () => bugReportStore.clearStore(); + }, []); + + const onGen = () => { + // @ts-ignore + import('html2canvas').then(({ default: html2canvas }) => { + // @ts-ignore + window.html2canvas = html2canvas; + + // @ts-ignore + import('jspdf').then(({ jsPDF }) => { + setRendering(true); + const doc = new jsPDF('p', 'mm', 'a4'); + const now = new Date().toISOString(); + + doc.addMetadata('Author', account.name); + doc.addMetadata('Title', 'OpenReplay Bug Report'); + doc.addMetadata('Subject', 'OpenReplay Bug Report'); + doc.addMetadata('Keywords', 'OpenReplay Bug Report'); + doc.addMetadata('Creator', 'OpenReplay'); + doc.addMetadata('Producer', 'OpenReplay'); + doc.addMetadata('CreationDate', now); + + // DO NOT DELETE UNUSED RENDER FUNCTION + // REQUIRED FOR FUTURE USAGE AND AS AN EXAMPLE OF THE FUNCTIONALITY + + function buildPng() { + html2canvas(reportRef.current, { + scale: 2, + ignoreElements: (e) => e.id.includes('pdf-ignore'), + }).then((canvas) => { + const imgData = canvas.toDataURL('img/png'); + + var imgWidth = 210; + var pageHeight = 295; + var imgHeight = (canvas.height * imgWidth) / canvas.width; + var heightLeft = imgHeight; + var position = 0; + + doc.addImage(imgData, 'PNG', 0, 0, imgWidth, imgHeight); + heightLeft -= pageHeight; + + while (heightLeft >= 0) { + position = heightLeft - imgHeight + 10; + doc.addPage(); + doc.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight); + heightLeft -= pageHeight; + } + + doc.link(5, 295 - Math.abs(heightLeft) - 25, 200, 30, { url: sessionUrl }); + + doc.save('Bug Report: ' + sessionId + '.pdf'); + setRendering(false); + }); + } + function buildText() { + doc + .html(reportRef.current, { + x: 0, + y: 0, + width: 210, + windowWidth: reportRef.current.getBoundingClientRect().width, + autoPaging: 'text', + html2canvas: { + ignoreElements: (e) => e.id.includes('pdf-ignore') || e instanceof SVGElement, + }, + }) + .save('html.pdf') + .then(() => { + setRendering(false); + }) + .catch((e) => { + console.error(e); + setRendering(false); + }); + } + // buildText(); + buildPng(); + }); + }); + }; + return (
- - <MetaInfo envObject={envObject} metadata={metadata} /> - <Steps xrayProps={xrayProps} /> - <Session user={userDisplayName} sessionId={sessionId} sessionUrl={sessionUrl} /> - <Comments /> + <div className="flex flex-col p-4 gap-4 bg-white h-auto relative" ref={reportRef}> + <Title userName={account.name} /> + <MetaInfo envObject={envObject} metadata={metadata} /> + <Steps xrayProps={xrayProps} /> + <Comments /> + <Session user={userDisplayName} sessionId={sessionId} sessionUrl={sessionUrl} /> + <div id="pdf-ignore" className="flex items-center gap-2 mt-4"> + <Button icon="file-pdf" variant="primary" onClick={onGen} loading={isRendering}> + Download Bug Report + </Button> + <Button variant="text-primary" onClick={hideModal}> + Close + </Button> + </div> + </div> + {isRendering ? ( + <div + className="fixed min-h-screen flex text-xl items-center justify-center top-0 right-0 cursor-wait" + style={{ background: 'rgba(0,0,0, 0.2)', zIndex: 9999, width: 620, maxWidth: '70vw' }} + id="pdf-ignore" + > + <div>Rendering PDF Report</div> + </div> + ) : null} </div> ); } -// @ts-ignore -const WithUIState = connect((state) => ({ session: state.getIn(['sessions', 'current']), account: state.getIn(['user', 'account']), }))( - BugReportModal -); +const WithUIState = connect((state) => ({ + // @ts-ignore + session: state.getIn(['sessions', 'current']), + // @ts-ignore + account: state.getIn(['user', 'account']), +}))(BugReportModal); -export default WithUIState +export default WithUIState; diff --git a/frontend/app/components/Session_/BugReport/components/Comments.tsx b/frontend/app/components/Session_/BugReport/components/Comments.tsx index bbb7a2d93..afcff8aea 100644 --- a/frontend/app/components/Session_/BugReport/components/Comments.tsx +++ b/frontend/app/components/Session_/BugReport/components/Comments.tsx @@ -24,7 +24,7 @@ function Comments() { : 'Expected results, additional steps or any other useful information for debugging.'; return ( - <div className="w-full"> + <div className="w-full" id={commentsEnabled ? '' : 'pdf-ignore'}> <div className="flex items-center gap-2"> <SectionTitle>Comments</SectionTitle> <div className="text-disabled-text mb-2">(Optional)</div> diff --git a/frontend/app/components/Session_/BugReport/components/MetaInfo.tsx b/frontend/app/components/Session_/BugReport/components/MetaInfo.tsx index bfb5baa68..7adcf4786 100644 --- a/frontend/app/components/Session_/BugReport/components/MetaInfo.tsx +++ b/frontend/app/components/Session_/BugReport/components/MetaInfo.tsx @@ -25,8 +25,10 @@ export default function MetaInfo({ <div key={envTag} className="flex items-center"> <div className="py-1 px-2">{envTag}</div> <div className="py-1 px-2 text-gray-medium bg-light-blue-bg rounded"> + <span className="text-base"> {/* @ts-ignore */} {envObject[envTag]} + </span> </div> </div> ))} diff --git a/frontend/app/components/Session_/BugReport/components/Session.tsx b/frontend/app/components/Session_/BugReport/components/Session.tsx index 97eb060d4..ab30012ff 100644 --- a/frontend/app/components/Session_/BugReport/components/Session.tsx +++ b/frontend/app/components/Session_/BugReport/components/Session.tsx @@ -11,7 +11,7 @@ export default function Session({ user, sessionId, sessionUrl }: { user: string, <div className="flex flex-col"> <div className="text-lg">{user}</div> <div className="text-disabled-text"> - {sessionUrl} + <a href={sessionUrl}>{sessionUrl}</a> </div> </div> <PlayLink newTab isAssist={false} viewed={false} sessionId={sessionId} /> diff --git a/frontend/app/components/Session_/BugReport/components/Steps.tsx b/frontend/app/components/Session_/BugReport/components/Steps.tsx index 3e0d81c35..f757de460 100644 --- a/frontend/app/components/Session_/BugReport/components/Steps.tsx +++ b/frontend/app/components/Session_/BugReport/components/Steps.tsx @@ -57,9 +57,9 @@ function Steps({ xrayProps }: Props) { <div className="mt-4 mb-2 text-gray-dark flex items-center gap-4"> STEPS - {timePointer > 0 ? <StepRadius pickRadius={stepPickRadius} setRadius={setRadius} /> : null} + <div id="pdf-ignore">{timePointer > 0 ? <StepRadius pickRadius={stepPickRadius} setRadius={setRadius} /> : null}</div> </div> - <div className="text-blue cursor-pointer" onClick={handleStepsSelection}> + <div className="text-blue cursor-pointer" id="pdf-ignore" onClick={handleStepsSelection}> {!shouldShowEventReset ? ( <span>Add {timePointer > 0 ? '' : 'All'} Steps</span> ) : ( diff --git a/frontend/app/components/Session_/BugReport/components/StepsComponents/XRay.tsx b/frontend/app/components/Session_/BugReport/components/StepsComponents/XRay.tsx index 81f4ff8d3..838cf1e2b 100644 --- a/frontend/app/components/Session_/BugReport/components/StepsComponents/XRay.tsx +++ b/frontend/app/components/Session_/BugReport/components/StepsComponents/XRay.tsx @@ -78,7 +78,7 @@ function XRay({ xrayProps, timePointer, stepPickRadius, clearEventSelection, set ) : null} </div> {!shouldShowPointerReset ? ( - <div className="flex items-center gap-2 px-2 py-1 rounded bg-light-blue-bg"> + <div className="flex items-center gap-2 px-2 py-1 rounded bg-light-blue-bg" id="pdf-ignore"> <Icon name="info-circle" size={16} /> <div> Click anywhere on <span className="font-semibold">X-RAY</span> to drilldown and add @@ -86,7 +86,7 @@ function XRay({ xrayProps, timePointer, stepPickRadius, clearEventSelection, set </div> </div> ) : ( - <div className="text-blue py-1 cursor-pointer" onClick={clearEventSelection}> + <div className="text-blue py-1 cursor-pointer" onClick={clearEventSelection} id="pdf-ignore"> Clear Selection </div> )} @@ -95,11 +95,12 @@ function XRay({ xrayProps, timePointer, stepPickRadius, clearEventSelection, set className="relative cursor-pointer" onClick={pickEventRadius} ref={xrayContainer} - style={{ background: timePointer > 0 ? 'rgba(57, 78, 255, 0.07)' : undefined }} > + <div id="pdf-ignore" style={{ pointerEvents: 'none', background: timePointer > 0 ? 'rgb(57, 78, 255)' : undefined, opacity: '0.07', position: 'absolute', top:0, left:0, width:'100%', height: '100%' }} /> {timePointer > 0 ? ( <div className="absolute h-full bg-white" + // id="pdf-ignore" style={{ zIndex: INDEXES.BUG_REPORT_PICKER, width: 41, @@ -111,15 +112,16 @@ function XRay({ xrayProps, timePointer, stepPickRadius, clearEventSelection, set style={{ height: '100%', width: 0, - border: '1px dashed rgba(0,0,0, 0.5)', + borderLeft: '2px dashed rgba(0,0,0, 0.5)', left: 20, position: 'absolute', + zIndex: INDEXES.BUG_REPORT + 1, }} /> </div> ) : null} {Object.keys(resources).map((feature) => ( - <div key={feature} className="border-b-2 last:border-none z-20"> + <div key={feature} className="border-b-2 last:border-none relative z-20"> <EventRow title={feature} // @ts-ignore diff --git a/frontend/app/components/Session_/BugReport/overload.module.css b/frontend/app/components/Session_/BugReport/overload.module.css new file mode 100644 index 000000000..4fc49cd3d --- /dev/null +++ b/frontend/app/components/Session_/BugReport/overload.module.css @@ -0,0 +1,3 @@ +body { + line-height: 0.5!important; +} diff --git a/frontend/app/components/Session_/OverviewPanel/components/TimelinePointer/TimelinePointer.tsx b/frontend/app/components/Session_/OverviewPanel/components/TimelinePointer/TimelinePointer.tsx index a1949c0ce..6c05e5739 100644 --- a/frontend/app/components/Session_/OverviewPanel/components/TimelinePointer/TimelinePointer.tsx +++ b/frontend/app/components/Session_/OverviewPanel/components/TimelinePointer/TimelinePointer.tsx @@ -58,7 +58,9 @@ const TimelinePointer = React.memo((props: Props) => { position="top" > <div onClick={createEventClickHandler(item, NETWORK)} className="cursor-pointer"> - <div className="h-4 w-4 rounded-full bg-red" /> + <div className="h-4 w-4 rounded-full bg-red text-white font-bold flex items-center justify-center text-sm"> + <span>!</span> + </div> </div> </Popup> ); @@ -138,12 +140,9 @@ const TimelinePointer = React.memo((props: Props) => { position="top" > <div onClick={createEventClickHandler(item, 'ERRORS')} className="cursor-pointer"> - <Icon - className="rounded-full bg-white" - name="funnel/exclamation-circle-fill" - color="red" - size="16" - /> + <div className="h-4 w-4 rounded-full bg-red text-white font-bold flex items-center justify-center text-sm"> + <span>!</span> + </div> </div> </Popup> ); diff --git a/frontend/app/styles/global.scss b/frontend/app/styles/global.scss index 014cfce5f..c25d0cc7b 100644 --- a/frontend/app/styles/global.scss +++ b/frontend/app/styles/global.scss @@ -15,4 +15,10 @@ input.no-focus:focus { .widget-wrapper { @apply rounded border bg-white; -} \ No newline at end of file +} + +@layer base { + img { + @apply inline-block; + } +} diff --git a/frontend/app/utils.ts b/frontend/app/utils.ts index 52bf7c6ad..d87f47e8e 100644 --- a/frontend/app/utils.ts +++ b/frontend/app/utils.ts @@ -270,10 +270,10 @@ export const positionOfTheNumber = (min, max, value, length) => { }; export const convertElementToImage = async (el) => { - const fontEmbedCss = await htmlToImage.getFontEmbedCSS(el); - const image = await htmlToImage.toJpeg(el, { + // const fontEmbedCss = await htmlToImage.getFontEmbedCSS(el); + const image = await htmlToImage.toPng(el, { pixelRatio: 2, - fontEmbedCss, + // fontEmbedCss, filter: function (node) { return node.id !== 'no-print'; }, diff --git a/frontend/package.json b/frontend/package.json index 22c97528f..a236b3c5f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -25,6 +25,7 @@ "copy-to-clipboard": "^3.3.1", "deep-diff": "^1.0.2", "html-to-image": "^1.9.0", + "html2canvas": "^1.4.1", "immutable": "^4.0.0-rc.12", "jsbi": "^4.1.0", "jshint": "^2.11.1", diff --git a/third-party.md b/third-party.md index 17bf76498..18ec3fa90 100644 --- a/third-party.md +++ b/third-party.md @@ -113,3 +113,4 @@ Below is the list of dependencies used in OpenReplay software. Licenses may chan | clickhouse | Apache2 | Infrastructure | | redis | BSD3 | Infrastructure | | yq | MIT | Infrastructure | +| html2canvas | MIT | JavaScript |