fix ui: remove bugreport modal code (#2198)
This commit is contained in:
parent
dacbb8aa54
commit
ccaa67ae7a
20 changed files with 3 additions and 1633 deletions
|
|
@ -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;
|
||||
|
|
@ -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);
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
@ -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>;
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
@ -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);
|
||||
|
|
@ -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 ± {pickRadius}.</span>}>
|
||||
<span>± {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;
|
||||
|
|
@ -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
|
||||
|
|
@ -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;
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
@ -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);
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
body {
|
||||
line-height: 0.5!important;
|
||||
}
|
||||
|
|
@ -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';
|
||||
}
|
||||
|
|
@ -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]
|
||||
}
|
||||
|
|
@ -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} />}
|
||||
|
|
|
|||
|
|
@ -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 = [];
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue