fix ui: remove bugreport modal code (#2198)

This commit is contained in:
Delirium 2024-05-21 14:22:14 +02:00 committed by GitHub
parent dacbb8aa54
commit ccaa67ae7a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 3 additions and 1633 deletions

View file

@ -1,224 +0,0 @@
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, Activity } 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 { fetchList as fetchMembers } from 'Duck/member';
interface Props {
hideModal: () => void;
session: Record<string, any>;
account: Record<string, any>;
width: number;
height: number;
xrayProps: {
currentLocation: Record<string, any>[];
resourceList: Record<string, any>[];
exceptionsList: Record<string, any>[];
eventsList: Record<string, any>[];
endTime: number;
};
fetchMembers: () => void
members: any;
}
function BugReportModal({ hideModal, session, width, height, account, xrayProps, fetchMembers, members }: Props) {
const reportRef = React.createRef<HTMLDivElement>();
const [isRendering, setRendering] = React.useState(false);
const { bugReportStore } = useStore();
const {
userBrowser,
userDevice,
userCountry,
userBrowserVersion,
userOs,
userOsVersion,
userDisplayName,
userDeviceType,
revId,
metadata,
sessionId,
events,
notes,
} = session;
const envObject: EnvData = {
Device: `${userDevice}${userDeviceType !== userDevice ? ` ${userDeviceType}` : ''}`,
Resolution: `${width}x${height}`,
Browser: `${userBrowser} v${userBrowserVersion}`,
OS: `${userOs} v${userOsVersion}`,
// @ts-ignore
Country: countries[userCountry],
};
if (revId) {
Object.assign(envObject, { Rev: revId });
}
const sessionUrl = `${window.location.origin}/${
window.location.pathname.split('/')[1]
}${sessionRoute(sessionId)}`;
const defaults: ReportDefaults = {
author: account.name,
env: envObject,
meta: metadata,
session: {
user: userDisplayName,
id: sessionId,
url: sessionUrl,
},
};
React.useEffect(() => {
fetchMembers()
bugReportStore.updateReportDefaults(defaults);
bugReportStore.setDefaultSteps(mapEvents(events));
}, []);
const onClose = () => {
hideModal();
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');
let imgWidth = 200;
let pageHeight = 295;
let imgHeight = (canvas.height * imgWidth) / canvas.width;
let heightLeft = imgHeight - pageHeight;
let position = 0;
doc.addImage(imgData, 'PNG', 5, 5, imgWidth, imgHeight);
doc.addImage('/assets/img/report-head.png', 'png', 210/2 - 40/2, 2, 45, 5);
if (position === 0 && heightLeft === 0) doc.addImage('/assets/img/report-head.png', 'png', 210/2 - 40/2, pageHeight - 5, 45, 5);
while (heightLeft >= 0) {
position = heightLeft - imgHeight;
doc.addPage();
doc.addImage(imgData, 'PNG', 5, position, imgWidth, imgHeight);
doc.addImage('/assets/img/report-head.png', 'png', 210/2 - 40/2, pageHeight - 5, 45, 5);
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();
const activity = {
network: xrayProps.resourceList,
console: xrayProps.exceptionsList,
clickRage: xrayProps.eventsList.filter((item: any) => item.type === 'CLICKRAGE'),
}
bugReportStore.composeReport(activity as unknown as Activity)
});
});
};
return (
<div
className="bg-white overflow-y-scroll"
style={{ height: '100vh' }}
>
<div className="flex flex-col p-4 gap-8 bg-white relative" ref={reportRef}>
<Title userName={account.name} />
<MetaInfo envObject={envObject} metadata={metadata} />
<Steps xrayProps={xrayProps} notes={notes} members={members} />
<Comments />
<Session user={userDisplayName} 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={onClose}>
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>
);
}
const WithUIState = connect((state) => ({
// @ts-ignore
session: state.getIn(['sessions', 'current']),
// @ts-ignore
account: state.getIn(['user', 'account']),
// @ts-ignore
members: state.getIn(['members', 'list']),
}), { fetchMembers })(BugReportModal);
export default WithUIState;

View file

@ -1,63 +0,0 @@
import React from 'react';
import { useStore } from 'App/mstore';
import { observer } from 'mobx-react-lite';
import cn from 'classnames';
import SectionTitle from './SectionTitle';
function Comments() {
const { bugReportStore } = useStore();
const inputRef = React.createRef<HTMLTextAreaElement>();
const toggleEdit = () => {
bugReportStore.toggleCommentEditing(true);
};
React.useEffect(() => {
if (inputRef.current && bugReportStore.isCommentEdit) {
inputRef.current?.focus();
}
}, [bugReportStore.isCommentEdit]);
const commentsEnabled = bugReportStore.comment.length > 0;
const commentStr = commentsEnabled
? bugReportStore.comment
: 'Expected results, additional steps or any other useful information for debugging.';
return (
<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>
</div>
{bugReportStore.isCommentEdit ? (
<textarea
ref={inputRef}
name="reportComments"
placeholder="Comment..."
rows={3}
autoFocus
className="text-area fluid border -mx-2 px-2 py-1 w-full -mt-2"
value={bugReportStore.comment}
onChange={(e) => bugReportStore.setComment(e.target.value)}
onBlur={() => bugReportStore.toggleCommentEditing(false)}
onFocus={() => bugReportStore.toggleCommentEditing(true)}
/>
) : (
<div
onClick={toggleEdit}
className={cn(
!commentsEnabled
? 'text-disabled-text border-dotted border-gray-medium'
: 'border-b-borderColor-transparent hover:border-dotted hover:border-gray-medium',
'pt-1 w-fit -mt-2',
'cursor-pointer select-none border-b'
)}
>
{commentStr}
</div>
)}
</div>
);
}
export default observer(Comments);

View file

@ -1,48 +0,0 @@
import React from 'react';
import SectionTitle from './SectionTitle';
interface EnvObj {
Device: string;
Resolution: string;
Browser: string;
OS: string;
Country: string;
Rev?: string;
}
export default function MetaInfo({
envObject,
metadata,
}: {
envObject: EnvObj;
metadata: Record<string, any>;
}) {
return (
<div className="flex gap-8">
<div className="flex flex-col gap-2">
<SectionTitle>Environment</SectionTitle>
{Object.keys(envObject).map((envTag) => (
<div key={envTag} className="flex items-center">
<div className="py-1 px-2 font-medium">{envTag}</div>
<div className="rounded text-base bg-active-blue px-2 py-1 whitespace-nowrap overflow-hidden text-clip">
{/* @ts-ignore */}
{envObject[envTag]}
</div>
</div>
))}
</div>
{Object.keys(metadata).length > 0 ? (
<div className="flex flex-col gap-2">
<SectionTitle>Metadata</SectionTitle>
{Object.keys(metadata).map((meta) => (
<div key={meta} className="flex items-center rounded overflow-hidden bg-gray-lightest">
<div className="bg-gray-light-shade py-1 px-2">{meta}</div>
<div className="py-1 px-2 text-gray-medium">{metadata[meta]}</div>
</div>
))}
</div>
) : null}
</div>
);
}

View file

@ -1,64 +0,0 @@
import React from 'react';
import { useStore } from 'App/mstore';
import { observer } from 'mobx-react-lite';
import cn from 'classnames';
import { Tooltip } from 'UI';
function ReportTitle() {
const { bugReportStore } = useStore();
const inputRef = React.createRef<HTMLInputElement>();
const toggleEdit = () => {
bugReportStore.toggleTitleEdit(true);
};
React.useEffect(() => {
const handler = (e: KeyboardEvent) => {
if (bugReportStore.isTitleEdit && e.key === 'Enter') {
inputRef.current?.blur();
bugReportStore.toggleTitleEdit(false);
}
}
document.addEventListener('keydown', handler, false)
return () => document.removeEventListener('keydown', handler)
})
React.useEffect(() => {
if (inputRef.current && bugReportStore.isTitleEdit) {
inputRef.current?.focus();
}
}, [bugReportStore.isTitleEdit])
return (
<div>
{bugReportStore.isTitleEdit ? (
<input
ref={inputRef}
name="reportTitle"
className="rounded fluid border-0 -mx-2 px-2 h-8 text-2xl"
value={bugReportStore.reportTitle}
onChange={(e) => bugReportStore.setTitle(e.target.value)}
onBlur={() => bugReportStore.toggleTitleEdit(false)}
onFocus={() => bugReportStore.toggleTitleEdit(true)}
/>
) : (
// @ts-ignore
<Tooltip delay={200} title="Double click to edit">
<div
onDoubleClick={toggleEdit}
className={cn(
'color-teal text-2xl h-8 flex items-center border-transparent',
'cursor-pointer select-none border-b border-b-borderColor-transparent hover:border-dotted hover:border-gray-medium'
)}
>
{bugReportStore.reportTitle}
</div>
</Tooltip>
)}
</div>
);
}
export default observer(ReportTitle);

View file

@ -1,5 +0,0 @@
import React from 'react'
export default function SectionTitle({ children }: { children: React.ReactNode }) {
return <div className="text-xl font-semibold mb-2">{children}</div>;
}

View file

@ -1,28 +0,0 @@
import React from 'react'
import SectionTitle from './SectionTitle';
import { Icon, Tooltip } from 'UI'
export default function Session({ user, sessionUrl }: { user: string, sessionUrl: string }) {
const onSessionClick = () => {
window.open(sessionUrl, '_blank').focus();
}
return (
<div>
<SectionTitle>Session recording</SectionTitle>
{/* @ts-ignore */}
<Tooltip title="Play session in new tab">
<div className="border hover:border-main hover:bg-active-blue cursor-pointer rounded flex items-center justify-between p-2" onClick={onSessionClick}>
<div className="flex flex-col">
<div className="text-lg">{user}</div>
<div className="text-disabled-text">
{sessionUrl}
</div>
</div>
<Icon name="play-fill" size={38} color="teal" />
</div>
</Tooltip>
</div>
);
}

View file

@ -1,104 +0,0 @@
import React from 'react';
import { Button } from 'UI'
import { useStore } from 'App/mstore';
import { observer } from 'mobx-react-lite';
import { RADIUS } from '../utils';
import SectionTitle from './SectionTitle';
import XRay from './StepsComponents/XRay';
import StepRenderer from './StepsComponents/StepRenderer';
import StepRadius from './StepsComponents/StepRadius';
import SubModal from './StepsComponents/SubModal';
import { Note } from 'App/services/NotesService';
interface Props {
xrayProps: {
currentLocation: Record<string, any>[];
resourceList: Record<string, any>[];
exceptionsList: Record<string, any>[];
eventsList: Record<string, any>[];
endTime: number;
};
notes: Note[];
members: Record<string, any>[];
}
function Steps({ xrayProps, notes, members }: Props) {
const { bugReportStore } = useStore();
const [stepPickRadius, setRadius] = React.useState(RADIUS);
const [timePointer, setPointer] = React.useState(0);
const shouldShowEventReset = bugReportStore.chosenEventSteps.length > 0;
const handleStepsSelection = () => {
if (shouldShowEventReset) {
return clearEventSelection();
}
if (timePointer > 0) {
// temp ?
return bugReportStore.setSteps(bugReportStore.sessionEventSteps);
} else {
bugReportStore.setSteps(bugReportStore.sessionEventSteps);
}
};
const clearEventSelection = () => {
setPointer(0);
bugReportStore.resetSteps();
};
React.useEffect(() => {
if (bugReportStore.sessionEventSteps.length < RADIUS && bugReportStore.sessionEventSteps.length > 0) {
setRadius(bugReportStore.sessionEventSteps.length);
}
}, [bugReportStore.sessionEventSteps])
return (
<div>
<SectionTitle>Steps to reproduce</SectionTitle>
<XRay
xrayProps={xrayProps}
timePointer={timePointer}
clearEventSelection={clearEventSelection}
setPointer={setPointer}
stepPickRadius={stepPickRadius}
/>
<div className="flex items-center justify-between">
<div className="mt-4 mb-2 text-gray-dark flex items-center gap-4">
STEPS
<div id="pdf-ignore">
{timePointer > 0 ? (
<StepRadius pickRadius={stepPickRadius} setRadius={setRadius} stepsNum={bugReportStore.sessionEventSteps.length}/>
) : null}
</div>
</div>
<Button id="pdf-ignore" variant="text-primary" onClick={handleStepsSelection}>
{!shouldShowEventReset ? (
<span>Add {timePointer > 0 ? '' : 'All'} Steps</span>
) : (
<span>Reset</span>
)}
</Button>
</div>
<StepRenderer
isDefault={bugReportStore.chosenEventSteps.length === 0}
steps={
bugReportStore.chosenEventSteps.length === 0
? bugReportStore.sessionEventSteps
: bugReportStore.chosenEventSteps
}
/>
{bugReportStore.isSubStepModalOpen ? (
<SubModal
members={members}
type={bugReportStore.subModalType}
notes={notes}
xrayProps={xrayProps}
/>
) : null}
</div>
);
}
export default observer(Steps);

View file

@ -1,126 +0,0 @@
import React from 'react';
import { Icon, ItemMenu, Tooltip } from 'UI';
import { observer } from 'mobx-react-lite';
import { Step as IStep } from '../../types';
const STEP_NAMES = { CLICKRAGE: 'Multiple click', CLICK: 'Clicked', LOCATION: 'Visited' };
import { useStore } from 'App/mstore';
import cn from 'classnames';
import { ErrorComp, NetworkComp, NoteComp } from './SubModalItems';
import { durationFromMs } from 'App/date';
const SUBSTEP = {
network: NetworkComp,
note: NoteComp,
error: ErrorComp,
};
function Step({ step, ind, isDefault }: { step: IStep; ind: number; isDefault?: boolean }) {
const { bugReportStore } = useStore();
const [menuOpen, setMenu] = React.useState(false);
const menuItems = [
{
icon: 'quotes',
text: 'Notes',
onClick: () => bugReportStore.toggleSubStepModal(true, 'note', step.key),
},
{
icon: 'info-circle',
text: `Errors`,
onClick: () => bugReportStore.toggleSubStepModal(true, 'error', step.key),
},
{
icon: 'network',
text: 'Bad Network Requests',
onClick: () => bugReportStore.toggleSubStepModal(true, 'network', step.key),
},
];
return (
<div className="flex flex-col w-full">
<div
className={cn(
'py-1 px-2 flex items-start gap-2 w-full rounded',
menuOpen
? 'bg-figmaColors-secondary-outlined-hover-background'
: isDefault
? ''
: 'hover:bg-figmaColors-secondary-outlined-hover-background group'
)}
>
<div className="rounded-3xl px-4 bg-gray-lightest relative z-10">{ind + 1}</div>
<div className="w-full">
<div className="flex items-start w-full gap-2">
<div className="px-1 text-disabled-text">{durationFromMs(step.time)}</div>
{/* @ts-ignore */}
<Icon name={step.icon} size={16} color="gray-darkest" className="relative z-10" />
{/* @ts-ignore */}
<div className="font-semibold">{STEP_NAMES[step.type]}</div>
<div className="text-gray-medium">{step.details}</div>
<div
className={cn(
'group-hover:flex items-center ml-auto gap-4',
menuOpen ? 'flex' : 'hidden'
)}
>
{/* @ts-ignore */}
<Tooltip title="Add Note, Error or bad Network Request" className="!flex items-center">
<ItemMenu
label={
<Icon
name="plus"
size={16}
className="cursor-pointer hover:fill-gray-darkest"
/>
}
items={menuItems}
flat
onToggle={(isOpen) => setMenu(isOpen)}
/>
</Tooltip>
{/* @ts-ignore */}
<Tooltip title="Delete Step" className="whitespace-nowrap">
<div onClick={() => bugReportStore.removeStep(step)}>
<Icon name="trash" size={16} className="cursor-pointer hover:fill-gray-darkest" />
</div>
</Tooltip>
</div>
</div>
{step.substeps?.length ? (
<div className="flex flex-col gap-2 w-full mt-2 relative">
{step.substeps.map((subStep) => {
const Component = SUBSTEP[subStep.type];
return (
<div className="relative" key={subStep.key}>
<div
className="rounded border py-1 px-2 w-full flex flex-col relative z-10"
style={{ background: subStep.type === 'note' ? '#FFFEF5' : 'white' }}
>
{/* @ts-ignore */}
<Component item={subStep} />
</div>
<div
style={{
borderBottom: '1px solid #DDDDDD',
borderLeft: '1px solid #DDDDDD',
borderBottomLeftRadius: 6,
position: 'absolute',
zIndex: 1,
left: -25,
bottom: 10,
height: '120%',
width: 50,
}}
/>
</div>
);
})}
</div>
) : null}
</div>
</div>
</div>
);
}
export default observer(Step);

View file

@ -1,37 +0,0 @@
import React from 'react';
import { Tooltip } from 'UI'
interface Props {
pickRadius: number;
setRadius: (v: number) => void;
stepsNum: number;
}
function StepRadius({ pickRadius, setRadius, stepsNum }: Props) {
return (
<div className="w-full flex items-center gap-4">
<div className="border-b border-dotted border-gray-medium cursor-help">
{/* @ts-ignore */}
<Tooltip title={<span>Closest step to the selected timestamp &plusmn; {pickRadius}.</span>}>
<span>&plusmn; {pickRadius}</span>
</Tooltip>
</div>
<div className="flex items-center gap-1">
<div
className="rounded px-2 bg-light-blue-bg cursor-pointer hover:bg-teal-light"
onClick={() => pickRadius < Math.floor(stepsNum/2) ? setRadius(pickRadius + 1) : null}
>
+1
</div>
<div
className="rounded px-2 bg-light-blue-bg cursor-pointer hover:bg-teal-light"
onClick={() => (pickRadius > 1 ? setRadius(pickRadius - 1) : null)}
>
-1
</div>
</div>
</div>
);
}
export default StepRadius;

View file

@ -1,28 +0,0 @@
import React from 'react'
import Step from './EventStep';
import { Step as IStep } from '../../types';
function StepRenderer(props: { steps: IStep[]; isDefault: boolean }) {
const stepAmount = props.steps.length;
const shouldSkip = stepAmount > 2;
if (props.isDefault && shouldSkip) {
return (
<div className="flex flex-col gap-4 opacity-50">
<Step step={props.steps[0]} ind={1} isDefault />
<div className="ml-4"> + {stepAmount - 2} Steps</div>
<Step step={props.steps[stepAmount - 1]} ind={stepAmount} isDefault />
</div>
);
}
return (
<div className="flex flex-col gap-4">
{props.steps.map((step, ind) => (
<React.Fragment key={step.key}>
<Step step={step} ind={ind} />
</React.Fragment>
))}
</div>
);
}
export default StepRenderer

View file

@ -1,180 +0,0 @@
import React from 'react';
import { Icon, Button } from 'UI';
import { observer } from 'mobx-react-lite';
import { Note } from 'App/services/NotesService';
import { NoteItem, ErrorItem, NetworkReq, SubItem } from './SubModalItems';
import { filterList, debounce } from 'App/utils';
import { useStore } from 'App/mstore';
const Titles = {
note: 'Notes',
network: 'Fetch/XHR Errors',
error: 'Console Errors',
};
const Icons = {
note: 'quotes' as const,
network: 'network' as const,
error: 'info-circle' as const
}
const Filters = {
note: 'note message or author',
network: 'url',
error: 'error name or message',
};
interface Props {
type: 'note' | 'network' | 'error';
items: SubItem[];
}
let debounceUpdate: any = () => {};
const SUB_ITEMS = {
note: NoteItem,
error: ErrorItem,
network: NetworkReq,
};
function ModalContent(props: Props) {
const [searchStr, setSearch] = React.useState('');
const list =
searchStr !== ''
? filterList(props.items, searchStr, ['url', 'name', 'title', 'message'])
: props.items;
React.useEffect(() => {
debounceUpdate = debounce((val: string) => setSearch(val), 250);
}, []);
const SubItem = SUB_ITEMS[props.type];
return (
<div className="flex flex-col p-4 bg-white gap-4 w-full">
<div className="flex items-center gap-2">
<div className="p-2 rounded-full bg-light-blue-bg">
<Icon name={Icons[props.type]} size={18} />
</div>
<div className="text-2xl font-semibold">{`Select ${Titles[props.type]}`}</div>
<div className="ml-auto">
<input
onChange={(e) => debounceUpdate(e.target.value)}
className="bg-white p-2 border border-borderColor-gray-light-shade rounded"
placeholder={`Filter by ${Filters[props.type]}`}
style={{ width: 250 }}
/>
</div>
</div>
<div
className="flex flex-col rounded -mx-4 px-4 py-2 bg-white"
style={{ height: 'calc(100vh - 130px)', overflowY: 'scroll', maxWidth: '70vw', width: 620 }}
>
{list.length > 0 ? (
list.map((item) => (
<React.Fragment key={item.key}>
{/* @ts-ignore */}
<SubItem item={item} />
</React.Fragment>
))
) : (
<div className="text-2xl font-semibold text-center">No items to show.</div>
)}
</div>
<ModalActionsObs />
</div>
);
}
function ModalActions() {
const { bugReportStore } = useStore();
const removeModal = () => {
bugReportStore.toggleSubStepModal(false, bugReportStore.subModalType, undefined);
};
const saveChoice = () => {
bugReportStore.saveSubItems();
removeModal();
};
return (
<div className="flex items-center gap-2">
<Button
disabled={bugReportStore.pickedSubItems[bugReportStore.targetStep].size === 0}
variant="primary"
onClick={saveChoice}
>
Add Selected
</Button>
<Button variant="text-primary" onClick={removeModal}>
Cancel
</Button>
</div>
);
}
const ModalActionsObs = observer(ModalActions);
interface ModalProps {
xrayProps: {
currentLocation: Record<string, any>[];
resourceList: Record<string, any>[];
exceptionsList: Record<string, any>[];
eventsList: Record<string, any>[];
endTime: number;
};
type: 'note' | 'network' | 'error';
notes: Note[];
members: Record<string, any>[];
}
function SubModal(props: ModalProps) {
let items;
if (props.type === 'note') {
items = props.notes.map((note) => ({
type: 'note' as const,
title: props.members.find((m) => m.id === note.userId)?.email || note.userId,
message: note.message,
time: 0,
key: note.noteId as unknown as string,
}));
}
if (props.type === 'error') {
items = props.xrayProps.exceptionsList.map((error) => ({
type: 'error' as const,
time: error.time,
message: error.message,
name: error.name,
key: error.key,
}));
}
if (props.type === 'network') {
items = props.xrayProps.resourceList.map((fetch) => ({
type: 'network' as const,
time: fetch.time,
url: fetch.url,
status: fetch.status,
success: fetch.success,
message: fetch.name,
key: fetch.key,
}));
}
return (
<div
className="bg-white fixed"
style={{
maxWidth: '70vw',
overflow: 'hidden',
width: 620,
height: '100vh',
top: 0,
right: 0,
zIndex: 999,
}}
>
{/* @ts-ignore */}
<ModalContent type={props.type} items={items} />
</div>
);
}
export default SubModal;

View file

@ -1,128 +0,0 @@
import React from 'react';
import { Checkbox } from 'UI';
import { observer } from 'mobx-react-lite';
import { useStore } from 'App/mstore';
import { durationFromMs } from 'App/date'
import cn from 'classnames'
interface Item {
time: number;
message: string;
type: 'note' | 'network' | 'error';
key: string;
}
export interface INoteItem extends Item {
title: string;
}
export interface IError extends Item {
name?: string;
}
export interface INetworkReq extends Item {
url: string;
status: string;
success: boolean;
}
export type SubItem = INoteItem | IError | INetworkReq;
const safeStr = (ogStr: string) => {
if (!ogStr) return ''
return (ogStr.length > 80 ? ogStr.slice(0, 80) + '...' : ogStr)
}
export const NetworkComp = ({ item }: { item: INetworkReq }) => (
<div className="flex items-start flex-col z-10">
<div className="flex items-center gap-2 text-disabled-text">
<div>{durationFromMs(item.time)}</div>
<div>{safeStr(item.url)}</div>
</div>
<div className="flex items-center gap-2">
<div className="rounded bg-active-blue px-2 whitespace-nowrap overflow-hidden text-clip font-mono">{item.status}</div>
<div className={item.success ? '' : 'text-red'}>{safeStr(item.message)}</div>
</div>
</div>
);
export const NetworkReq = observer(({ item }: { item: INetworkReq }) => {
const { bugReportStore } = useStore();
return (
<SubModalItemContainer
isChecked={bugReportStore.isSubItemChecked(item)}
onChange={(isChecked) => bugReportStore.toggleSubItem(isChecked, item)}
>
<NetworkComp item={item} />
</SubModalItemContainer>
);
});
export const NoteComp = ({ item }: { item: INoteItem }) => (
<div className="flex items-start flex-col z-10">
<div>{item.message}</div>
<div className="text-disabled-text text-sm">{item.title}</div>
</div>
);
export const NoteItem = observer(({ item }: { item: INoteItem }) => {
const { bugReportStore } = useStore();
return (
<SubModalItemContainer
isChecked={bugReportStore.isSubItemChecked(item)}
onChange={(isChecked) => bugReportStore.toggleSubItem(isChecked, item)}
isNote
>
<NoteComp item={item} />
</SubModalItemContainer>
);
});
export const ErrorComp = ({ item }: { item: IError }) => (
<div className="flex items-start flex-col z-10">
<div className="text-disabled-text">{durationFromMs(item.time)}</div>
{item.name ? <div className="text-red">{item.name}</div> : null}
<div className="text-secondary">{safeStr(item.message)}</div>
</div>
);
export const ErrorItem = observer(({ item }: { item: IError }) => {
const { bugReportStore } = useStore();
return (
<SubModalItemContainer
isChecked={bugReportStore.isSubItemChecked(item)}
onChange={(isChecked) => bugReportStore.toggleSubItem(isChecked, item)}
>
<ErrorComp item={item} />
</SubModalItemContainer>
);
});
export function SubModalItemContainer({
children,
isChecked,
onChange,
isNote,
}: {
children: React.ReactNode;
isChecked: boolean;
onChange: (arg: any) => void;
isNote?: boolean;
}) {
return (
<div
className={cn("flex items-start p-2 gap-2 border-t last:border-b cursor-pointer", isNote ? 'note-hover-bg' : 'hover:bg-active-blue')}
// style={{ background: isNote ? '#FFFEF5' : undefined }}
onClick={() => onChange(!isChecked)}
>
<Checkbox
name="isIncluded"
type="checkbox"
checked={isChecked}
onChange={(e: any) => onChange(e.target.checked)}
className="pt-1"
/>
{children}
</div>
);
}

View file

@ -1,155 +0,0 @@
import React from 'react';
import { Duration } from 'luxon';
import { observer } from 'mobx-react-lite';
import { Icon, Button } from 'UI';
import { useStore } from 'App/mstore';
import { INDEXES } from 'App/constants/zindex';
import TimelinePointer from 'App/components/Session_/OverviewPanel/components/TimelinePointer';
import EventRow from 'App/components/Session_/OverviewPanel/components/EventRow';
import { selectEventSteps } from '../../utils';
interface IXRay {
xrayProps: {
currentLocation: Record<string, any>[];
resourceList: Record<string, any>[];
exceptionsList: Record<string, any>[];
eventsList: Record<string, any>[];
endTime: number;
};
timePointer: number;
stepPickRadius: number;
clearEventSelection: () => void;
setPointer: (time: number) => void;
}
function XRay({ xrayProps, timePointer, stepPickRadius, clearEventSelection, setPointer }: IXRay) {
const [selectedTime, setTime] = React.useState(0);
const xrayContainer = React.useRef<HTMLDivElement>();
const { bugReportStore } = useStore();
const { resourceList, exceptionsList, eventsList, endTime } = xrayProps;
const resources = {
NETWORK: resourceList,
ERRORS: exceptionsList,
CLICKRAGE: eventsList.filter((item: any) => item.type === 'CLICKRAGE'),
};
const pickEventRadius = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
e.stopPropagation();
const pos = e.clientX - xrayContainer.current?.getBoundingClientRect().left;
const percent = pos / xrayContainer.current?.getBoundingClientRect().width;
const targetTime = percent * endTime;
const selectedSteps = selectEventSteps(
bugReportStore.sessionEventSteps,
targetTime,
stepPickRadius
);
setTime(targetTime);
setPointer(e.clientX - xrayContainer.current?.getBoundingClientRect().left);
bugReportStore.setSteps(selectedSteps);
};
React.useEffect(() => {
if (timePointer > 0 && selectedTime > 0 && bugReportStore.chosenEventSteps) {
const selectedSteps = selectEventSteps(
bugReportStore.sessionEventSteps,
selectedTime,
stepPickRadius
);
bugReportStore.setSteps(selectedSteps);
}
}, [stepPickRadius]);
const shouldShowPointerReset = timePointer > 0;
return (
<>
<div className="flex items-center justify-between my-2">
<div className=" text-gray-dark py-2">
X-RAY
{timePointer > 0 ? (
<span className="text-disabled-text ml-2">
{Duration.fromMillis(selectedTime).toFormat('hh:mm:ss')}
</span>
) : null}
</div>
{!shouldShowPointerReset ? (
<div
className="flex items-center gap-2 rounded bg-active-blue px-2 py-1 whitespace-nowrap overflow-hidden text-clip group"
id="pdf-ignore"
>
<Icon name="info-circle" size={16} />
<div>Click anywhere in the graph below to drilldown and add steps</div>
</div>
) : (
<Button id="pdf-ignore" variant="text-primary" onClick={clearEventSelection}>
Clear Selection
</Button>
)}
</div>
<div
className="relative cursor-pointer group-hover:border-dotted hover:border-dotted group-hover:border-gray-dark hover:border-gray-dark border border-transparent"
onClick={pickEventRadius}
ref={xrayContainer}
>
<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,
left: timePointer - 20,
pointerEvents: 'none',
}}
>
<div
style={{
height: '100%',
width: 0,
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 relative z-20">
<EventRow
title={feature}
// @ts-ignore
list={resources[feature]}
zIndex={INDEXES.BUG_REPORT}
noMargin
renderElement={(pointer: any) => (
<TimelinePointer noClick pointer={pointer} type={feature} />
)}
endTime={endTime}
/>
</div>
))}
</div>
</>
);
}
export default observer(XRay);

View file

@ -1,62 +0,0 @@
import React from 'react';
import Select from 'Shared/Select';
import { Icon } from 'UI';
import ReportTitle from './ReportTitle';
import { useStore } from 'App/mstore';
import { observer } from 'mobx-react-lite';
import { SeverityLevels } from 'App/mstore/bugReportStore';
const selectOptions = [
{
label: (
<div className="flex items-center gap-1 cursor-pointer w-full">
<Icon name="arrow-up-short" color="red" size="24" />
HIGH
</div>
),
value: SeverityLevels.High,
},
{
label: (
<div className="flex items-center gap-1 cursor-pointer w-full">
<Icon name="dash" size="24" color="yellow2" />
MEDIUM
</div>
),
value: SeverityLevels.Medium,
},
{
label: (
<div className="flex items-center gap-1 cursor-pointer w-full">
<Icon name="arrow-down-short" color="teal" size="24" />
LOW
</div>
),
value: SeverityLevels.Low,
},
];
function Title({ userName }: { userName: string }) {
const { bugReportStore } = useStore();
return (
<div className="flex items-center py-2 px-3 justify-between bg-gray-lightest rounded">
<div className="flex flex-col gap-2">
<ReportTitle />
<div className="text-gray-medium">By {userName}</div>
</div>
<div className="flex items-center gap-2">
<div className="font-semibold">Severity</div>
<Select
plain
controlStyle={{ minWidth: 115 }}
defaultValue={SeverityLevels.High}
options={selectOptions}
onChange={({ value }) => bugReportStore.setSeverity(value.value)}
/>
</div>
</div>
);
}
export default observer(Title);

View file

@ -1,3 +0,0 @@
body {
line-height: 0.5!important;
}

View file

@ -1,119 +0,0 @@
import { SeverityLevels } from 'App/mstore/bugReportStore';
import { SubItem } from './components/StepsComponents/SubModalItems';
export interface ReportDefaults {
author: string;
env: EnvData;
meta: {
[key: string]: string;
};
session: {
user: string;
url: string;
id: string;
};
}
export interface BugReportPdf extends ReportDefaults {
title: string;
comment?: string;
severity: SeverityLevels;
steps: Step[];
activity: Activity
}
export interface Activity {
network: NetworkEvent[];
console: Exception[];
clickRage: ClickRage[];
};
interface Event {
time: number;
key: string;
}
interface NetworkEvent extends Event {
decodedBodySize: number | null;
duration: number | null;
encodedBodySize: number | null;
headerSize: number | null;
index?: number;
method: string;
name: string;
payload: string;
response: string;
responseBodySize: number;
score: number;
status: string;
success: boolean;
timewidth: number;
timings: Record<string, any>;
ttfb: number;
type: string;
url: string;
}
interface Exception extends Event {
errorId: string;
function: string;
key: string;
message: string;
messageId: number;
name: string;
projectId: number;
sessionId: number;
source: string;
timestamp: number;
}
interface ClickRage extends Event {
type: 'CLICKRAGE';
label: string
targetContent: string,
target: {
key: string,
path: string,
label: string | null
},
count: number
}
export interface EnvData {
Browser: string;
OS: string;
Country: string;
Device: string;
Resolution: string;
}
export interface Step {
key: string;
type: string;
time: number;
details: string;
icon: string;
substeps?: SubItem[];
}
export interface Note {
author: string;
message: string;
step: 'note';
}
export interface Error {
timestamp: string;
error: string;
step: 'error';
}
export interface Request {
url: string;
status: number;
type: 'GET' | 'POST' | 'PUT' | 'DELETE';
time: number;
name: string;
step: 'request';
}

View file

@ -1,69 +0,0 @@
import { Step } from './types'
const TYPES = { CLICKRAGE: 'CLICKRAGE', CLICK: 'CLICK', LOCATION: 'LOCATION' }
export const RADIUS = 3
export function mapEvents(events: Record<string,any>[]): Step[] {
const steps: Step[] = []
events.forEach(event => {
if (event.type === TYPES.LOCATION) {
const step = {
key: event.key,
type: TYPES.LOCATION,
icon: 'event/location',
details: event.url,
time: event.time,
}
steps.push(step)
}
if (event.type === TYPES.CLICK) {
const step = {
key: event.key,
type: TYPES.CLICK,
icon: 'puzzle-piece',
details: event.label,
time: event.time,
}
steps.push(step)
}
if (event.type === TYPES.CLICKRAGE) {
const step = {
key: event.key,
type: TYPES.CLICKRAGE,
icon: 'event/clickrage',
details: event.label,
time: event.time,
}
steps.push(step)
}
})
return steps
}
export function getClosestEventStep(time: number, arr: Step[]) {
let mid;
let low = 0;
let high = arr.length - 1;
while (high - low > 1) {
mid = Math.floor ((low + high) / 2);
if (arr[mid].time < time) {
low = mid;
} else {
high = mid;
}
}
if (time - arr[low].time <= arr[high].time - time) {
return { targetStep: arr[low], index: low } ;
}
return { targetStep: arr[high], index: high } ;
}
export const selectEventSteps = (steps: Step[], targetTime: number, radius: number) => {
const { targetStep, index } = getClosestEventStep(targetTime, steps)
const stepsBeforeEvent = steps.slice(Math.max(index - radius, 0), index)
const stepsAfterEvent = steps.slice(index + 1, index + 1 + radius)
return [...stepsBeforeEvent, targetStep, ...stepsAfterEvent]
}

View file

@ -7,8 +7,6 @@ import Bookmark from 'Shared/Bookmark';
import SharePopup from '../shared/SharePopup/SharePopup';
import Issues from './Issues/Issues';
import NotePopup from './components/NotePopup';
import { useModal } from 'App/components/Modal';
import BugReportModal from './BugReport/BugReportModal';
import { PlayerContext } from 'App/components/Session/playerContext';
import { observer } from 'mobx-react-lite';
import { connect } from 'react-redux';
@ -16,7 +14,7 @@ import SessionTabs from 'Components/Session/Player/SharedComponents/SessionTabs'
import { IFRAME } from 'App/constants/storageKeys';
import cn from 'classnames';
import { Switch, Button as AntButton, Popover } from 'antd';
import { BugOutlined, ShareAltOutlined } from '@ant-design/icons';
import { ShareAltOutlined } from '@ant-design/icons';
const localhostWarn = (project) => project + '_localhost_warn';
const disableDevtools = 'or_devtools_uxt_toggle';
@ -25,8 +23,8 @@ function SubHeader(props) {
const localhostWarnKey = localhostWarn(props.siteId);
const defaultLocalhostWarn = localStorage.getItem(localhostWarnKey) !== '1';
const [showWarningModal, setWarning] = React.useState(defaultLocalhostWarn);
const { player, store } = React.useContext(PlayerContext);
const { width, height, endTime, location: currentLocation = 'loading...' } = store.get();
const { store } = React.useContext(PlayerContext);
const { location: currentLocation = 'loading...' } = store.get();
const hasIframe = localStorage.getItem(IFRAME) === 'true';
const { uxtestingStore } = useStore();
@ -39,40 +37,11 @@ function SubHeader(props) {
return integrations.some((i) => i.token);
}, [props.integrations]);
const { showModal, hideModal } = useModal();
const location =
currentLocation && currentLocation.length > 70
? `${currentLocation.slice(0, 25)}...${currentLocation.slice(-40)}`
: currentLocation;
const showReportModal = () => {
const { tabStates, currentTab } = store.get();
const resourceList = tabStates[currentTab]?.resourceList || [];
const exceptionsList = tabStates[currentTab]?.exceptionsList || [];
const eventsList = tabStates[currentTab]?.eventList || [];
const graphqlList = tabStates[currentTab]?.graphqlList || [];
const fetchList = tabStates[currentTab]?.fetchList || [];
const mappedResourceList = resourceList
.filter((r) => r.isRed || r.isYellow)
.concat(fetchList.filter((i) => parseInt(i.status) >= 400))
.concat(graphqlList.filter((i) => parseInt(i.status) >= 400));
player.pause();
const xrayProps = {
currentLocation: currentLocation,
resourceList: mappedResourceList,
exceptionsList: exceptionsList,
eventsList: eventsList,
endTime: endTime,
};
showModal(
<BugReportModal width={width} height={height} xrayProps={xrayProps} hideModal={hideModal} />,
{ right: true, width: 620 }
);
};
const showWarning =
location && /(localhost)|(127.0.0.1)|(0.0.0.0)/.test(location) && showWarningModal;
const closeWarning = () => {
@ -128,15 +97,6 @@ function SubHeader(props) {
style={{ width: 'max-content' }}
>
<KeyboardHelp />
<Popover content={'Create Bug Report'}>
<AntButton
size={'small'}
className={'flex items-center justify-center'}
onClick={showReportModal}
>
<BugOutlined />
</AntButton>
</Popover>
<Bookmark sessionId={props.sessionId} />
<NotePopup />
{enabledIntegration && <Issues sessionId={props.sessionId} />}

View file

@ -1,144 +0,0 @@
import { makeAutoObservable } from 'mobx';
import { BugReportPdf, ReportDefaults, Step, Activity } from 'Components/Session_/BugReport/types';
import { SubItem } from 'App/components/Session_/BugReport/components/StepsComponents/SubModalItems';
export enum SeverityLevels {
Low,
Medium,
High,
}
export default class BugReportStore {
reportTitle = 'Untitled Report';
comment = '';
severity = SeverityLevels.High;
isCommentEdit = false;
isTitleEdit = false;
isSubStepModalOpen = false;
bugReport: Partial<BugReportPdf>;
sessionEventSteps: Step[] = [];
chosenEventSteps: Step[] = [];
subModalType: 'note' | 'network' | 'error';
targetStep: string
pickedSubItems: Record<string, Map<string, SubItem>> = {}
constructor() {
makeAutoObservable(this);
}
clearStore() {
this.reportTitle = 'Untitled Report';
this.comment = '';
this.severity = SeverityLevels.High;
this.isCommentEdit = false;
this.isTitleEdit = false;
this.bugReport = undefined;
this.sessionEventSteps = [];
this.chosenEventSteps = [];
this.subModalType = undefined;
this.isSubStepModalOpen = false;
this.targetStep = undefined;
this.pickedSubItems = {};
}
toggleTitleEdit(isEdit: boolean) {
this.isTitleEdit = isEdit;
}
setTitle(title: string) {
if (title.length < 40) {
this.reportTitle = title;
this.bugReport = Object.assign(this.bugReport, { title: this.reportTitle });
}
}
setSeverity(severity: SeverityLevels) {
this.severity = severity;
this.bugReport = Object.assign(this.bugReport, { severity: this.severity });
}
toggleCommentEditing(isEdit: boolean) {
this.isCommentEdit = isEdit;
}
setComment(comment: string) {
this.comment = comment;
this.bugReport = Object.assign(this.bugReport, {
comment: this.comment.length > 0 ? this.comment : undefined,
});
}
updateReportDefaults(defaults: ReportDefaults) {
this.bugReport = Object.assign(this.bugReport || {}, defaults);
}
composeReport(activity: Activity) {
const reportObj = {
title: this.reportTitle,
comment: this.comment,
severity: this.severity,
steps: this.chosenEventSteps,
activity
}
this.bugReport = Object.assign(this.bugReport, reportObj)
return this.bugReport
}
setDefaultSteps(steps: Step[]) {
this.sessionEventSteps = steps;
}
setSteps(steps: Step[]) {
this.chosenEventSteps = steps.map(step => ({ ...step, substeps: undefined }));
this.pickedSubItems = {};
}
removeStep(step: Step) {
this.chosenEventSteps = this.chosenEventSteps.filter(
(chosenStep) => chosenStep.key !== step.key
);
if (this.pickedSubItems[step.key]) this.pickedSubItems[step.key] = new Map()
}
toggleSubStepModal(isOpen: boolean, type: 'note' | 'network' | 'error', stepKey?: string) {
this.isSubStepModalOpen = isOpen;
this.subModalType = type;
this.targetStep = stepKey
if (!this.pickedSubItems[this.targetStep]) this.pickedSubItems[this.targetStep] = new Map()
}
toggleSubItem(isAdded: boolean, item: SubItem) {
if (isAdded) {
this.pickedSubItems[this.targetStep].set(item.key, item)
} else {
this.pickedSubItems[this.targetStep].delete(item.key)
}
}
isSubItemChecked(item: SubItem) {
return this.pickedSubItems[this.targetStep]?.get(item.key) !== undefined
}
saveSubItems() {
const targetIndex = this.chosenEventSteps.findIndex(step => step.key === this.targetStep)
const eventStepsCopy = this.chosenEventSteps
const step = this.chosenEventSteps[targetIndex]
if (this.pickedSubItems[this.targetStep].size > 0) {
step.substeps = Array.from(this.pickedSubItems[this.targetStep], ([name, value]) => ({ ...value }));
}
eventStepsCopy[targetIndex] = step
return this.chosenEventSteps = eventStepsCopy
}
resetSteps() {
this.chosenEventSteps = [];
}
}

View file

@ -12,7 +12,6 @@ import NotificationStore from './notificationStore';
import ErrorStore from './errorStore';
import SessionStore from './sessionStore';
import NotesStore from './notesStore';
import BugReportStore from './bugReportStore';
import RecordingsStore from './recordingsStore';
import AssistMultiviewStore from './assistMultiviewStore';
import WeeklyReportStore from './weeklyReportConfigStore';
@ -35,7 +34,6 @@ export class RootStore {
notificationStore: NotificationStore;
sessionStore: SessionStore;
notesStore: NotesStore;
bugReportStore: BugReportStore;
recordingsStore: RecordingsStore;
assistMultiviewStore: AssistMultiviewStore;
weeklyReportStore: WeeklyReportStore;
@ -58,7 +56,6 @@ export class RootStore {
this.notificationStore = new NotificationStore();
this.sessionStore = new SessionStore();
this.notesStore = new NotesStore();
this.bugReportStore = new BugReportStore();
this.recordingsStore = new RecordingsStore();
this.assistMultiviewStore = new AssistMultiviewStore();
this.weeklyReportStore = new WeeklyReportStore();