change(ui): add steps picker, send real data to bug report components, update select comp
This commit is contained in:
parent
f4350ee083
commit
a68230359c
10 changed files with 181 additions and 65 deletions
|
|
@ -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<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;
|
||||
}
|
||||
}
|
||||
|
||||
const TYPES = { CLICKRAGE: 'CLICKRAGE', CLICK: 'CLICK', LOCATION: 'LOCATION' }
|
||||
|
||||
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: '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) {
|
|||
>
|
||||
<Title userName={account.name} />
|
||||
<MetaInfo envObject={envObject} metadata={metadata} />
|
||||
<Steps />
|
||||
<Steps xrayProps={xrayProps} />
|
||||
<Session user={userDisplayName} sessionId={sessionId} sessionUrl={sessionUrl} />
|
||||
<Comments />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
41
frontend/app/components/Session_/BugReport/utils.ts
Normal file
41
frontend/app/components/Session_/BugReport/utils.ts
Normal file
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ const { ValueContainer } = components;
|
|||
|
||||
type ValueObject = {
|
||||
value: string | number,
|
||||
label: string,
|
||||
label: React.ReactNode,
|
||||
}
|
||||
|
||||
interface Props<Value extends ValueObject> {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue