change(ui): sort and pick events around selected time, add components around picking, allow selecting pick range etc

This commit is contained in:
sylenien 2022-10-24 16:52:04 +02:00 committed by Delirium
parent a68230359c
commit e3b924a046
14 changed files with 368 additions and 105 deletions

View file

@ -69,7 +69,7 @@ function BugReportModal({ hideModal, session, width, height, account, xrayProps
}
bugReportStore.updateReportDefaults(defaults)
bugReportStore.setSteps(mapEvents(events))
bugReportStore.setDefaultSteps(mapEvents(events))
return (
<div
className="flex flex-col p-4 gap-4 bg-white overflow-y-scroll"

View file

@ -13,7 +13,7 @@ function Comments() {
};
React.useEffect(() => {
if (inputRef.current && bugReportStore.isTitleEdit) {
if (inputRef.current && bugReportStore.isCommentEdit) {
inputRef.current?.focus();
}
}, [bugReportStore.isCommentEdit]);
@ -26,13 +26,13 @@ function Comments() {
return (
<div className="w-full">
<div className="flex items-center gap-2">
<SectionTitle>Comments</SectionTitle>{' '}
<SectionTitle>Comments</SectionTitle>
<div className="text-disabled-text mb-2">(Optional)</div>
</div>
{bugReportStore.isCommentEdit ? (
<textarea
ref={inputRef}
name="name"
name="reportComments"
placeholder="Comment..."
rows={3}
autoFocus

View file

@ -1,5 +1,5 @@
import React from 'react'
import SectionTitle from './SectionTitle'
import React from 'react';
import SectionTitle from './SectionTitle';
interface EnvObj {
Device: string;
@ -10,7 +10,13 @@ interface EnvObj {
Rev?: string;
}
export default function MetaInfo({ envObject, metadata }: { envObject: EnvObj, metadata: Record<string, any> }) {
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">
@ -26,15 +32,17 @@ export default function MetaInfo({ envObject, metadata }: { envObject: EnvObj, m
))}
</div>
<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>
{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

@ -23,7 +23,7 @@ function ReportTitle() {
{bugReportStore.isTitleEdit ? (
<input
ref={inputRef}
name="name"
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)}

View file

@ -1,14 +1,11 @@
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 { RADIUS } from '../utils';
import SectionTitle from './SectionTitle';
import { Step as IStep } from '../types';
const STEP_NAMES = { CLICKRAGE: 'Multiple click', CLICK: 'Clicked', LOCATION: 'Visited' };
import XRay from './StepsComponents/XRay';
import StepRenderer from './StepsComponents/StepRenderer';
import StepRadius from './StepsComponents/StepRadius'
interface Props {
xrayProps: {
@ -22,90 +19,62 @@ interface Props {
function Steps({ xrayProps }: Props) {
const { bugReportStore } = useStore();
const [stepPickRadius, setRadius] = React.useState(RADIUS);
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 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 pickEventRadius = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
e.stopPropagation();
setPointer(e.clientX - xrayContainer.current?.getBoundingClientRect().left);
const clearEventSelection = () => {
setPointer(0);
bugReportStore.resetSteps();
};
return (
<div>
<SectionTitle>Steps to reproduce</SectionTitle>
<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>
<XRay
xrayProps={xrayProps}
timePointer={timePointer}
clearEventSelection={clearEventSelection}
setPointer={setPointer}
stepPickRadius={stepPickRadius}
/>
<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}>
<Step step={step} ind={ind} />
</React.Fragment>
))}
</div>
</div>
);
}
<div className="flex items-center justify-between">
<div className="mt-4 mb-2 text-gray-dark flex items-center gap-4">
STEPS
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-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>
{timePointer > 0 ? <StepRadius pickRadius={stepPickRadius} setRadius={setRadius} /> : null}
</div>
<div className="text-blue cursor-pointer" onClick={handleStepsSelection}>
{!shouldShowEventReset ? (
<span>Add {timePointer > 0 ? '' : 'All'} Steps</span>
) : (
<span>Reset</span>
)}
</div>
</div>
<StepRenderer
isDefault={bugReportStore.chosenEventSteps.length === 0}
steps={
bugReportStore.chosenEventSteps.length === 0
? bugReportStore.sessionEventSteps
: bugReportStore.chosenEventSteps
}
/>
</div>
);
}

View file

@ -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 (
<div
className={cn(
'py-1 px-2 flex items-center gap-2 w-full rounded',
isDefault ? '' : 'hover:bg-figmaColors-secondary-outlined-hover-background group'
)}
>
<div className="rounded-3xl px-4 bg-gray-lightest">{ind + 1}</div>
<div className="flex items-center gap-2">
{/* @ts-ignore */}
<Icon name={step.icon} size={16} color="gray-darkest" />
<div className="px-2 text-disabled-text rounded bg-light-blue-bg">{Duration.fromMillis(step.time).toFormat('hh:mm:ss')}</div>
{/* @ts-ignore */}
<div className="font-semibold">{STEP_NAMES[step.type]}</div>
<div className="text-gray-medium">{step.details}</div>
</div>
<div className="hidden group-hover:flex items-center ml-auto gap-4">
<Icon name="plus" size={16} className="cursor-pointer hover:fill-gray-darkest" />
<div onClick={() => bugReportStore.removeStep(step)}>
<Icon name="trash" size={16} className="cursor-pointer hover:fill-gray-darkest" />
</div>
</div>
</div>
);
}
export default Step;

View file

@ -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 (
<div className="w-full flex items-center gap-4">
<div className="border-b border-dotted border-gray-medium cursor-help">
{/* @ts-ignore */}
<Tooltip html={<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={() => setRadius(pickRadius + 1)}
>
+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

@ -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 (
<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

@ -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<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">
XRAY
{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 px-2 py-1 rounded bg-light-blue-bg">
<Icon name="info-circle" size={16} />
<div>
Click anywhere on <span className="font-semibold">X-RAY</span> to drilldown and add
steps
</div>
</div>
) : (
<div className="text-blue py-1 cursor-pointer" onClick={clearEventSelection}>
Clear Selection
</div>
)}
</div>
<div
className="relative cursor-pointer"
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: INDEXES.BUG_REPORT_PICKER,
width: 41,
left: timePointer - 20,
pointerEvents: 'none',
}}
>
<div
style={{
height: '100%',
width: 0,
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-2 last:border-none 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,6 +1,7 @@
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[] = []
@ -39,3 +40,30 @@ export function mapEvents(events: Record<string,any>[]): 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]
}

View file

@ -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 (
<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={cn("uppercase color-gray-medium text-sm flex items-center py-1", props.noMargin ? '' : 'ml-4' )}>
<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">
<div className="relative w-full" style={{ zIndex: props.zIndex ? props.zIndex : undefined }}>
{isGraph ? (
<PerformanceGraph list={list} />
) : (
_list.length > 0 ? _list.map((item: any, index: number) => {
return (
<div key={index} className="absolute" style={{ left: item.left + '%', zIndex: props.zIndex ? props.zIndex : undefined }}>
<div key={index} className="absolute" style={{ left: `clamp(0%, calc(${item.left}% - 7px), calc(100% - 14px))`, zIndex: props.zIndex ? props.zIndex : undefined }}>
{props.renderElement ? props.renderElement(item) : null}
</div>
);
}) : (
<div className="ml-4 color-gray-medium text-sm pt-2">None captured.</div>
<div className={cn("color-gray-medium text-sm pt-2", props.noMargin ? '' : 'ml-4')}>None captured.</div>
)
)}
</div>

View file

@ -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 = () => {

View file

@ -2,11 +2,11 @@ import React, { useState, useCallback } from 'react';
type SupportedElements = HTMLInputElement | HTMLSelectElement;
export default function(state: string = ""): [string, React.ChangeEventHandler<SupportedElements>, (string) => void] {
export default function(state: string = ""): [string, React.ChangeEventHandler<SupportedElements>, (value: string) => void] {
const [ value, setValue ] = useState<string>(state);
const onChange = useCallback(
({ target: { value } }: React.ChangeEvent<SupportedElements>) =>
setValue(value),
({ target: { value } }: React.ChangeEvent<SupportedElements>) =>
setValue(value),
[]);
return [ value, onChange, setValue ];
}
}

View file

@ -17,6 +17,7 @@ export default class BugReportStore {
bugReport: Partial<BugReportPdf>
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 = []
}
}