diff --git a/frontend/app/components/Session_/BugReport/BugReportModal.tsx b/frontend/app/components/Session_/BugReport/BugReportModal.tsx index 1a84b33c1..8c04595c3 100644 --- a/frontend/app/components/Session_/BugReport/BugReportModal.tsx +++ b/frontend/app/components/Session_/BugReport/BugReportModal.tsx @@ -3,12 +3,13 @@ import { connect } from 'react-redux'; import { countries } from 'App/constants'; import { useStore } from 'App/mstore'; import { session as sessionRoute } from 'App/routes'; -import { ReportDefaults, EnvData, Step } from './types' +import { ReportDefaults, EnvData } from './types' import Session from './components/Session' import MetaInfo from './components/MetaInfo' import Title from './components/Title' import Comments from './components/Comments' import Steps from './components/Steps' +import { mapEvents } from './utils' interface Props { hideModal: () => void; @@ -16,49 +17,16 @@ interface Props { account: Record; width: number; height: number; + xrayProps: { + currentLocation: Record[]; + resourceList: Record[]; + exceptionsList: Record[]; + eventsList: Record[]; + endTime: number; + } } -const TYPES = { CLICKRAGE: 'CLICKRAGE', CLICK: 'CLICK', LOCATION: 'LOCATION' } - -function mapEvents(events: Record[]): Step[] { - const steps: Step[] = [] - events.forEach(event => { - if (event.type === TYPES.LOCATION) { - const step = { - key: event.key, - type: TYPES.LOCATION, - icon: 'pointer', - details: event.url, - time: event.time, - } - steps.push(step) - } - if (event.type === TYPES.CLICK) { - const step = { - key: event.key, - type: TYPES.CLICK, - icon: 'finger', - details: event.label, - time: event.time, - } - steps.push(step) - } - if (event.type === TYPES.CLICKRAGE) { - const step = { - key: event.key, - type: TYPES.CLICKRAGE, - icon: 'smile', - details: event.label, - time: event.time, - } - steps.push(step) - } - }) - - return steps -} - -function BugReportModal({ hideModal, session, width, height, account }: Props) { +function BugReportModal({ hideModal, session, width, height, account, xrayProps }: Props) { const { bugReportStore } = useStore() const { userBrowser, @@ -109,7 +77,7 @@ function BugReportModal({ hideModal, session, width, height, account }: Props) { > <MetaInfo envObject={envObject} metadata={metadata} /> - <Steps /> + <Steps xrayProps={xrayProps} /> <Session user={userDisplayName} sessionId={sessionId} sessionUrl={sessionUrl} /> <Comments /> </div> diff --git a/frontend/app/components/Session_/BugReport/components/Session.tsx b/frontend/app/components/Session_/BugReport/components/Session.tsx index 160a2e1f4..97eb060d4 100644 --- a/frontend/app/components/Session_/BugReport/components/Session.tsx +++ b/frontend/app/components/Session_/BugReport/components/Session.tsx @@ -14,7 +14,7 @@ export default function Session({ user, sessionId, sessionUrl }: { user: string, {sessionUrl} </div> </div> - <PlayLink isAssist={false} viewed={false} sessionId={sessionId} /> + <PlayLink newTab isAssist={false} viewed={false} sessionId={sessionId} /> </div> </div> ); diff --git a/frontend/app/components/Session_/BugReport/components/Steps.tsx b/frontend/app/components/Session_/BugReport/components/Steps.tsx index c93fb727d..3066ef150 100644 --- a/frontend/app/components/Session_/BugReport/components/Steps.tsx +++ b/frontend/app/components/Session_/BugReport/components/Steps.tsx @@ -1,20 +1,89 @@ import React from 'react'; import { useStore } from 'App/mstore'; import { observer } from 'mobx-react-lite'; +import cn from 'classnames'; +import { Icon } from 'UI'; +import EventRow from 'App/components/Session_/OverviewPanel/components/EventRow'; +import TimelinePointer from 'App/components/Session_/OverviewPanel/components/TimelinePointer'; import SectionTitle from './SectionTitle'; import { Step as IStep } from '../types'; const STEP_NAMES = { CLICKRAGE: 'Multiple click', CLICK: 'Clicked', LOCATION: 'Visited' }; -function Steps() { +interface Props { + xrayProps: { + currentLocation: Record<string, any>[]; + resourceList: Record<string, any>[]; + exceptionsList: Record<string, any>[]; + eventsList: Record<string, any>[]; + endTime: number; + }; +} + +function Steps({ xrayProps }: Props) { const { bugReportStore } = useStore(); + const [timePointer, setPointer] = React.useState(0); + const xrayContainer = React.useRef<HTMLDivElement>(); + 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(); + setPointer(e.clientX - xrayContainer.current?.getBoundingClientRect().left); + }; return ( <div> <SectionTitle>Steps to reproduce</SectionTitle> - <div className="mb-2 text-gray-medium">STEPS</div> + <div className="mt-2 text-gray-dark">XRAY</div> + <div + className="relative" + onClick={pickEventRadius} + ref={xrayContainer} + style={{ background: timePointer > 0 ? 'rgba(57, 78, 255, 0.07)' : undefined }} + > + {timePointer > 0 ? ( + <div + className="absolute h-full bg-white" + style={{ zIndex: 19, width: 41, left: timePointer - 20, pointerEvents: 'none' }} + > + <div + style={{ + height: '100%', + width: 1, + border: '1px dashed rgba(0,0,0, 0.5)', + left: 20, + position: 'absolute', + }} + /> + </div> + ) : null} + {Object.keys(resources).map(feature => ( + <div + key={feature} + className="border-b last:border-none z-20 -mx-4" + > + <EventRow + title={feature} + // @ts-ignore + list={resources[feature]} + zIndex={20} + renderElement={(pointer: any) => ( + <TimelinePointer noClick pointer={pointer} type={feature} /> + )} + endTime={endTime} + /> + </div> + ))} + </div> + <div className="mt-4 mb-2 text-gray-dark">STEPS</div> <div className="flex flex-col gap-4"> {bugReportStore.sessionEventSteps.map((step, ind) => ( <React.Fragment key={step.key}> @@ -30,8 +99,9 @@ function Step({ step, ind }: { step: IStep; ind: number }) { return ( <div className="py-1 px-2 flex items-center gap-2 w-full rounded hover:bg-figmaColors-secondary-outlined-hover-background"> <div className="rounded-3xl px-4 bg-gray-lightest">{ind + 1}</div> - <div className="flex items-center gap-1"> - <div>{step.icon}</div> + <div className="flex items-center gap-2"> + {/* @ts-ignore */} + <Icon name={step.icon} size={16} color="gray-darkest" /> {/* @ts-ignore */} <div className="font-semibold">{STEP_NAMES[step.type]}</div> <div className="text-gray-medium">{step.details}</div> diff --git a/frontend/app/components/Session_/BugReport/components/Title.tsx b/frontend/app/components/Session_/BugReport/components/Title.tsx index e756ce0ea..405c9e192 100644 --- a/frontend/app/components/Session_/BugReport/components/Title.tsx +++ b/frontend/app/components/Session_/BugReport/components/Title.tsx @@ -1,11 +1,15 @@ -import React from 'react' +import React from 'react'; import Select from 'Shared/Select'; import ReportTitle from './ReportTitle'; import { useStore } from 'App/mstore'; import { observer } from 'mobx-react-lite'; -import { SeverityLevels } from 'App/mstore/bugReportStore' +import { SeverityLevels } from 'App/mstore/bugReportStore'; -const selectOptions = [{ label: 'HIGH', value: SeverityLevels.High }, { label: 'MEDIUM', value: SeverityLevels.Medium }, { label: 'LOW', value: SeverityLevels.Low}] +const selectOptions = [ + { label: <div className="flex items-center gap-2 cursor-pointer w-full"> <div className="p-1 bg-red rounded-full" /> HIGH</div>, value: SeverityLevels.High }, + { label: <div className="flex items-center gap-2 cursor-pointer w-full"> <div className="p-1 bg-yellow2 rounded-full" /> MEDIUM</div>, value: SeverityLevels.Medium }, + { label:<div className="flex items-center gap-2 cursor-pointer w-full"> <div className="p-1 bg-blue rounded-full" /> LOW</div>, value: SeverityLevels.Low }, +]; function Title({ userName }: { userName: string }) { const { bugReportStore } = useStore(); @@ -18,10 +22,16 @@ function Title({ userName }: { userName: string }) { </div> <div className="flex items-center gap-2"> <div className="font-semibold">Severity</div> - <Select plain controlStyle={{ minWidth: 100 }} defaultValue={SeverityLevels.High} options={selectOptions} onChange={({ value }) => bugReportStore.setSeverity(value.value) } /> + <Select + plain + controlStyle={{ minWidth: 115 }} + defaultValue={SeverityLevels.High} + options={selectOptions} + onChange={({ value }) => bugReportStore.setSeverity(value.value)} + /> </div> </div> ); } -export default observer(Title) +export default observer(Title); diff --git a/frontend/app/components/Session_/BugReport/utils.ts b/frontend/app/components/Session_/BugReport/utils.ts new file mode 100644 index 000000000..cc39d793e --- /dev/null +++ b/frontend/app/components/Session_/BugReport/utils.ts @@ -0,0 +1,41 @@ +import { Step } from './types' + +const TYPES = { CLICKRAGE: 'CLICKRAGE', CLICK: 'CLICK', LOCATION: 'LOCATION' } + +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 +} diff --git a/frontend/app/components/Session_/OverviewPanel/components/EventRow/EventRow.tsx b/frontend/app/components/Session_/OverviewPanel/components/EventRow/EventRow.tsx index 8b5ac8571..983552649 100644 --- a/frontend/app/components/Session_/OverviewPanel/components/EventRow/EventRow.tsx +++ b/frontend/app/components/Session_/OverviewPanel/components/EventRow/EventRow.tsx @@ -11,6 +11,8 @@ interface Props { endTime?: number; renderElement?: (item: any) => React.ReactNode; isGraph?: boolean; + zIndex?: number; + } const EventRow = React.memo((props: Props) => { const { title, className, list = [], endTime = 0, isGraph = false, message = '' } = props; @@ -28,10 +30,10 @@ const EventRow = React.memo((props: Props) => { }, [list]); return ( - <div className={cn('w-full flex flex-col py-2', className)} style={{ height: '60px' }}> + <div className={cn('w-full flex flex-col py-2', className)} style={{ height: isGraph ? 60 : 50 }}> <div className="uppercase color-gray-medium ml-4 text-sm flex items-center py-1"> - <div className="mr-2 leading-none">{title}</div> - <RowInfo message={message} /> + <div style={{ zIndex: props.zIndex ? props.zIndex : undefined }} className="mr-2 leading-none">{title}</div> + {message ? <RowInfo zIndex={props.zIndex} message={message} /> : null} </div> <div className="relative w-full"> {isGraph ? ( @@ -39,7 +41,7 @@ const EventRow = React.memo((props: Props) => { ) : ( _list.length > 0 ? _list.map((item: any, index: number) => { return ( - <div key={index} className="absolute" style={{ left: item.left + '%' }}> + <div key={index} className="absolute" style={{ left: item.left + '%', zIndex: props.zIndex ? props.zIndex : undefined }}> {props.renderElement ? props.renderElement(item) : null} </div> ); @@ -54,10 +56,10 @@ const EventRow = React.memo((props: Props) => { export default EventRow; -function RowInfo({ message} : any) { +function RowInfo({ message, zIndex } : any) { return ( - <Popup content={message} delay={0}> + <Popup content={message} delay={0} style={{ zIndex: zIndex ? zIndex : undefined }}> <Icon name="info-circle" color="gray-medium"/> - </Popup> + </Popup> ) } diff --git a/frontend/app/components/Session_/OverviewPanel/components/TimelinePointer/TimelinePointer.tsx b/frontend/app/components/Session_/OverviewPanel/components/TimelinePointer/TimelinePointer.tsx index 3e7c4c6f9..a1949c0ce 100644 --- a/frontend/app/components/Session_/OverviewPanel/components/TimelinePointer/TimelinePointer.tsx +++ b/frontend/app/components/Session_/OverviewPanel/components/TimelinePointer/TimelinePointer.tsx @@ -13,10 +13,12 @@ import GraphQLDetailsModal from 'Shared/GraphQLDetailsModal'; interface Props { pointer: any; type: any; + noClick?: boolean; } const TimelinePointer = React.memo((props: Props) => { - const { showModal, hideModal } = useModal(); + const { showModal } = useModal(); const createEventClickHandler = (pointer: any, type: any) => (e: any) => { + if (props.noClick) return; e.stopPropagation(); Controls.jump(pointer.time); if (!type) { @@ -56,7 +58,7 @@ const TimelinePointer = React.memo((props: Props) => { position="top" > <div onClick={createEventClickHandler(item, NETWORK)} className="cursor-pointer"> - <div className="h-3 w-3 rounded-full bg-red" /> + <div className="h-4 w-4 rounded-full bg-red" /> </div> </Popup> ); diff --git a/frontend/app/components/Session_/Subheader.js b/frontend/app/components/Session_/Subheader.js index aa0365402..395ef88fd 100644 --- a/frontend/app/components/Session_/Subheader.js +++ b/frontend/app/components/Session_/Subheader.js @@ -24,7 +24,14 @@ function SubHeader(props) { const showReportModal = () => { pause(); - showModal(<BugReportModal width={props.width} height={props.height} hideModal={hideModal} />, { right: true }); + const xrayProps = { + currentLocation: props.currentLocation, + resourceList: props.resourceList, + exceptionsList: props.exceptionsList, + eventsList: props.eventsList, + endTime: props.endTime, + } + showModal(<BugReportModal width={props.width} height={props.height} xrayProps={xrayProps} hideModal={hideModal} />, { right: true }); }; return ( @@ -109,6 +116,20 @@ function SubHeader(props) { ); } -const SubH = connectPlayer((state) => ({ width: state.width, height: state.height, currentLocation: state.location }))(SubHeader); +const SubH = connectPlayer( + (state) => ({ + width: state.width, + height: state.height, + currentLocation: state.location, + resourceList: state.resourceList + .filter((r) => r.isRed() || r.isYellow()) + .concat(state.fetchList.filter((i) => parseInt(i.status) >= 400)) + .concat(state.graphqlList.filter((i) => parseInt(i.status) >= 400)), + exceptionsList: state.exceptionsList, + eventsList: state.eventList, + endTime: state.endTime, + }) + + )(SubHeader); export default React.memo(SubH); diff --git a/frontend/app/components/shared/Select/Select.tsx b/frontend/app/components/shared/Select/Select.tsx index b2b7afa8c..3a6928e7c 100644 --- a/frontend/app/components/shared/Select/Select.tsx +++ b/frontend/app/components/shared/Select/Select.tsx @@ -6,7 +6,7 @@ const { ValueContainer } = components; type ValueObject = { value: string | number, - label: string, + label: React.ReactNode, } interface Props<Value extends ValueObject> { diff --git a/frontend/app/components/shared/SessionItem/PlayLink/PlayLink.tsx b/frontend/app/components/shared/SessionItem/PlayLink/PlayLink.tsx index adaeeee71..805240dd8 100644 --- a/frontend/app/components/shared/SessionItem/PlayLink/PlayLink.tsx +++ b/frontend/app/components/shared/SessionItem/PlayLink/PlayLink.tsx @@ -16,6 +16,7 @@ interface Props { sessionId: string; onClick?: () => void; queryParams?: any; + newTab?: boolean; } export default function PlayLink(props: Props) { const { isAssist, viewed, sessionId, onClick = null, queryParams } = props; @@ -35,6 +36,7 @@ export default function PlayLink(props: Props) { to={isAssist ? liveSessionRoute(sessionId, queryParams) : sessionRoute(sessionId)} onMouseEnter={() => toggleHover(true)} onMouseLeave={() => toggleHover(false)} + target={props.newTab ? "_blank" : undefined} rel={props.newTab ? "noopener noreferrer" : undefined} > <Icon name={iconName} size={38} color={isAssist ? 'tealx' : 'teal'} /> </Link>