Steps to reproduce
-
XRAY
-
0 ? 'rgba(57, 78, 255, 0.07)' : undefined }}
- >
- {timePointer > 0 ? (
-
- ) : null}
- {Object.keys(resources).map(feature => (
-
- (
-
- )}
- endTime={endTime}
- />
-
- ))}
-
+
-
STEPS
-
- {bugReportStore.sessionEventSteps.map((step, ind) => (
-
-
-
- ))}
-
-
- );
-}
+
+
+ STEPS
-function Step({ step, ind }: { step: IStep; ind: number }) {
- return (
-
-
{ind + 1}
-
- {/* @ts-ignore */}
-
- {/* @ts-ignore */}
-
{STEP_NAMES[step.type]}
-
{step.details}
+ {timePointer > 0 ?
: null}
+
+
+ {!shouldShowEventReset ? (
+ Add {timePointer > 0 ? '' : 'All'} Steps
+ ) : (
+ Reset
+ )}
+
+
);
}
diff --git a/frontend/app/components/Session_/BugReport/components/StepsComponents/EventStep.tsx b/frontend/app/components/Session_/BugReport/components/StepsComponents/EventStep.tsx
new file mode 100644
index 000000000..d7049073d
--- /dev/null
+++ b/frontend/app/components/Session_/BugReport/components/StepsComponents/EventStep.tsx
@@ -0,0 +1,37 @@
+import React from 'react';
+import { Icon } from 'UI';
+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 { Duration } from 'luxon';
+
+function Step({ step, ind, isDefault }: { step: IStep; ind: number; isDefault?: boolean }) {
+ const { bugReportStore } = useStore();
+ return (
+
+
{ind + 1}
+
+ {/* @ts-ignore */}
+
+
{Duration.fromMillis(step.time).toFormat('hh:mm:ss')}
+ {/* @ts-ignore */}
+
{STEP_NAMES[step.type]}
+
{step.details}
+
+
+
+
bugReportStore.removeStep(step)}>
+
+
+
+
+ );
+}
+
+export default Step;
diff --git a/frontend/app/components/Session_/BugReport/components/StepsComponents/StepRadius.tsx b/frontend/app/components/Session_/BugReport/components/StepsComponents/StepRadius.tsx
new file mode 100644
index 000000000..85940e7c9
--- /dev/null
+++ b/frontend/app/components/Session_/BugReport/components/StepsComponents/StepRadius.tsx
@@ -0,0 +1,36 @@
+import React from 'react';
+import { Tooltip } from 'react-tippy'
+
+interface Props {
+ pickRadius: number;
+ setRadius: (v: number) => void;
+}
+
+function StepRadius({ pickRadius, setRadius }: Props) {
+ return (
+
+
+ {/* @ts-ignore */}
+ Closest step to the selected timestamp ± {pickRadius}.}>
+ ± {pickRadius}
+
+
+
+
setRadius(pickRadius + 1)}
+ >
+ +1
+
+
(pickRadius > 1 ? setRadius(pickRadius - 1) : null)}
+ >
+ -1
+
+
+
+ );
+}
+
+export default StepRadius;
diff --git a/frontend/app/components/Session_/BugReport/components/StepsComponents/StepRenderer.tsx b/frontend/app/components/Session_/BugReport/components/StepsComponents/StepRenderer.tsx
new file mode 100644
index 000000000..569a794d0
--- /dev/null
+++ b/frontend/app/components/Session_/BugReport/components/StepsComponents/StepRenderer.tsx
@@ -0,0 +1,28 @@
+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 (
+
+
+
+ {stepAmount - 2} Steps
+
+
+ );
+ }
+ return (
+
+ {props.steps.map((step, ind) => (
+
+
+
+ ))}
+
+ );
+}
+
+export default StepRenderer
diff --git a/frontend/app/components/Session_/BugReport/components/StepsComponents/XRay.tsx b/frontend/app/components/Session_/BugReport/components/StepsComponents/XRay.tsx
new file mode 100644
index 000000000..81f4ff8d3
--- /dev/null
+++ b/frontend/app/components/Session_/BugReport/components/StepsComponents/XRay.tsx
@@ -0,0 +1,141 @@
+import React from 'react';
+import { Duration } from 'luxon';
+import { observer } from 'mobx-react-lite';
+import { Icon } 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
[];
+ resourceList: Record[];
+ exceptionsList: Record[];
+ eventsList: Record[];
+ 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();
+ 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) => {
+ 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 (
+ <>
+
+
+ XRAY
+ {timePointer > 0 ? (
+
+ {Duration.fromMillis(selectedTime).toFormat('hh:mm:ss')}
+
+ ) : null}
+
+ {!shouldShowPointerReset ? (
+
+
+
+ Click anywhere on X-RAY to drilldown and add
+ steps
+
+
+ ) : (
+
+ Clear Selection
+
+ )}
+
+ 0 ? 'rgba(57, 78, 255, 0.07)' : undefined }}
+ >
+ {timePointer > 0 ? (
+
+ ) : null}
+ {Object.keys(resources).map((feature) => (
+
+ (
+
+ )}
+ endTime={endTime}
+ />
+
+ ))}
+
+ >
+ );
+}
+
+export default observer(XRay);
diff --git a/frontend/app/components/Session_/BugReport/utils.ts b/frontend/app/components/Session_/BugReport/utils.ts
index cc39d793e..a97e275d7 100644
--- a/frontend/app/components/Session_/BugReport/utils.ts
+++ b/frontend/app/components/Session_/BugReport/utils.ts
@@ -1,6 +1,7 @@
import { Step } from './types'
const TYPES = { CLICKRAGE: 'CLICKRAGE', CLICK: 'CLICK', LOCATION: 'LOCATION' }
+export const RADIUS = 3
export function mapEvents(events: Record[]): Step[] {
const steps: Step[] = []
@@ -39,3 +40,30 @@ export function mapEvents(events: Record[]): 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(index - radius, index)
+ const stepsAfterEvent = steps.slice(index + 1, index + 1 + radius)
+
+ return [...stepsBeforeEvent, targetStep, ...stepsAfterEvent]
+}
diff --git a/frontend/app/components/Session_/OverviewPanel/components/EventRow/EventRow.tsx b/frontend/app/components/Session_/OverviewPanel/components/EventRow/EventRow.tsx
index 983552649..c07086fdb 100644
--- a/frontend/app/components/Session_/OverviewPanel/components/EventRow/EventRow.tsx
+++ b/frontend/app/components/Session_/OverviewPanel/components/EventRow/EventRow.tsx
@@ -12,6 +12,7 @@ interface Props {
renderElement?: (item: any) => React.ReactNode;
isGraph?: boolean;
zIndex?: number;
+ noMargin?: boolean;
}
const EventRow = React.memo((props: Props) => {
@@ -31,22 +32,22 @@ const EventRow = React.memo((props: Props) => {
return (
-
+
{title}
{message ?
: null}
-
+
{isGraph ? (
) : (
_list.length > 0 ? _list.map((item: any, index: number) => {
return (
-
+
{props.renderElement ? props.renderElement(item) : null}
);
}) : (
-
None captured.
+
None captured.
)
)}
diff --git a/frontend/app/constants/zindex.ts b/frontend/app/constants/zindex.ts
index 807223caf..f5026ea85 100644
--- a/frontend/app/constants/zindex.ts
+++ b/frontend/app/constants/zindex.ts
@@ -1,7 +1,9 @@
export const INDEXES = {
+ PLAYER_REQUEST_WINDOW: 10,
+ BUG_REPORT_PICKER: 19,
+ BUG_REPORT: 20,
POPUP_GUIDE_BG: 99998,
POPUP_GUIDE_BTN: 99999,
- PLAYER_REQUEST_WINDOW: 10,
}
export const getHighest = () => {
diff --git a/frontend/app/hooks/useInputState.ts b/frontend/app/hooks/useInputState.ts
index 88e1221b4..2af6fc169 100644
--- a/frontend/app/hooks/useInputState.ts
+++ b/frontend/app/hooks/useInputState.ts
@@ -2,11 +2,11 @@ import React, { useState, useCallback } from 'react';
type SupportedElements = HTMLInputElement | HTMLSelectElement;
-export default function(state: string = ""): [string, React.ChangeEventHandler
, (string) => void] {
+export default function(state: string = ""): [string, React.ChangeEventHandler, (value: string) => void] {
const [ value, setValue ] = useState(state);
const onChange = useCallback(
- ({ target: { value } }: React.ChangeEvent) =>
- setValue(value),
+ ({ target: { value } }: React.ChangeEvent) =>
+ setValue(value),
[]);
return [ value, onChange, setValue ];
-}
\ No newline at end of file
+}
diff --git a/frontend/app/mstore/bugReportStore.ts b/frontend/app/mstore/bugReportStore.ts
index 9bbab2aaa..bd5e560c9 100644
--- a/frontend/app/mstore/bugReportStore.ts
+++ b/frontend/app/mstore/bugReportStore.ts
@@ -17,6 +17,7 @@ export default class BugReportStore {
bugReport: Partial
sessionEventSteps: Step[] = []
+ chosenEventSteps: Step[] = []
constructor() {
makeAutoObservable(this)
@@ -52,7 +53,19 @@ export default class BugReportStore {
this.bugReport = Object.assign(this.bugReport || {}, defaults)
}
- setSteps(steps: Step[]) {
+ setDefaultSteps(steps: Step[]) {
this.sessionEventSteps = steps
}
+
+ setSteps(steps: Step[]) {
+ this.chosenEventSteps = steps
+ }
+
+ removeStep(step: Step) {
+ this.chosenEventSteps = this.chosenEventSteps.filter(chosenStep => chosenStep.key !== step.key)
+ }
+
+ resetSteps() {
+ this.chosenEventSteps = []
+ }
}