change(ui): add substeps to report steps

This commit is contained in:
sylenien 2022-10-31 13:24:08 +01:00 committed by Delirium
parent d162bb6ae7
commit 40af78bc92
7 changed files with 406 additions and 86 deletions

View file

@ -11,6 +11,7 @@ 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;
@ -25,9 +26,11 @@ interface Props {
eventsList: Record<string, any>[];
endTime: number;
};
fetchMembers: () => void
members: any;
}
function BugReportModal({ hideModal, session, width, height, account, xrayProps }: Props) {
function BugReportModal({ hideModal, session, width, height, account, xrayProps, fetchMembers, members }: Props) {
const reportRef = React.createRef<HTMLDivElement>();
const [isRendering, setRendering] = React.useState(false);
@ -45,10 +48,9 @@ function BugReportModal({ hideModal, session, width, height, account, xrayProps
metadata,
sessionId,
events,
notes,
} = session;
console.log(session.toJS());
const envObject: EnvData = {
Device: `${userDevice}${userDeviceType !== userDevice ? ` ${userDeviceType}` : ''}`,
Resolution: `${width}x${height}`,
@ -76,9 +78,8 @@ function BugReportModal({ hideModal, session, width, height, account, xrayProps
},
};
console.log(bugReportStore)
React.useEffect(() => {
fetchMembers()
bugReportStore.updateReportDefaults(defaults);
bugReportStore.setDefaultSteps(mapEvents(events));
return () => bugReportStore.clearStore();
@ -168,10 +169,10 @@ function BugReportModal({ hideModal, session, width, height, account, xrayProps
className="bg-white overflow-y-scroll"
style={{ maxWidth: '70vw', width: 620, height: '100vh' }}
>
<div className="flex flex-col p-4 gap-4 bg-white h-auto relative" ref={reportRef}>
<div className="flex flex-col p-4 gap-4 bg-white relative" ref={reportRef}>
<Title userName={account.name} />
<MetaInfo envObject={envObject} metadata={metadata} />
<Steps xrayProps={xrayProps} />
<Steps xrayProps={xrayProps} notes={notes} members={members} />
<Comments />
<Session user={userDisplayName} sessionId={sessionId} sessionUrl={sessionUrl} />
<div id="pdf-ignore" className="flex items-center gap-2 mt-4">
@ -201,6 +202,8 @@ const WithUIState = connect((state) => ({
session: state.getIn(['sessions', 'current']),
// @ts-ignore
account: state.getIn(['user', 'account']),
}))(BugReportModal);
// @ts-ignore
members: state.getIn(['members', 'list']),
}), { fetchMembers })(BugReportModal);
export default WithUIState;

View file

@ -5,8 +5,9 @@ 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 StepRadius from './StepsComponents/StepRadius';
import SubModal from './StepsComponents/SubModal';
import { Note } from 'App/services/NotesService';
interface Props {
xrayProps: {
@ -16,9 +17,11 @@ interface Props {
eventsList: Record<string, any>[];
endTime: number;
};
notes: Note[];
members: Record<string, any>[];
}
function Steps({ xrayProps }: Props) {
function Steps({ xrayProps, notes, members }: Props) {
const { bugReportStore } = useStore();
const [stepPickRadius, setRadius] = React.useState(RADIUS);
const [timePointer, setPointer] = React.useState(0);
@ -57,8 +60,11 @@ function Steps({ xrayProps }: Props) {
<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} /> : null}</div>
<div id="pdf-ignore">
{timePointer > 0 ? (
<StepRadius pickRadius={stepPickRadius} setRadius={setRadius} />
) : null}
</div>
</div>
<div className="text-blue cursor-pointer" id="pdf-ignore" onClick={handleStepsSelection}>
{!shouldShowEventReset ? (
@ -77,7 +83,12 @@ function Steps({ xrayProps }: Props) {
}
/>
{bugReportStore.isSubStepModalOpen ? (
<SubModal type={bugReportStore.subModalType} toggleModal={bugReportStore.toggleSubStepModal} xrayProps={xrayProps}/>
<SubModal
members={members}
type={bugReportStore.subModalType}
notes={notes}
xrayProps={xrayProps}
/>
) : null}
</div>
);

View file

@ -1,60 +1,119 @@
import React from 'react';
import { Icon, ItemMenu } 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 { Duration } from 'luxon';
import { ErrorComp, NetworkComp, NoteComp } from './SubModalItems';
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: 'Add Note', onClick: () => bugReportStore.toggleSubStepModal(true, 'note') },
{ icon: 'info-circle', text: `Add Error`, onClick: () => bugReportStore.toggleSubStepModal(true, 'error') },
{ icon: 'network', text: 'Add Fetch/XHR', onClick: () => bugReportStore.toggleSubStepModal(true, 'network') },
{
icon: 'quotes',
text: 'Add/Remove Note',
onClick: () => bugReportStore.toggleSubStepModal(true, 'note', step.key),
},
{
icon: 'info-circle',
text: `Add/Remove Error`,
onClick: () => bugReportStore.toggleSubStepModal(true, 'error', step.key),
},
{
icon: 'network',
text: 'Add/Remove Network Request',
onClick: () => bugReportStore.toggleSubStepModal(true, 'network', step.key),
},
];
return (
<div
className={cn(
'py-1 px-2 flex items-center 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">{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="flex flex-col w-full">
<div
className={cn('group-hover:flex items-center ml-auto gap-4', menuOpen ? 'flex' : 'hidden')}
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'
)}
>
<ItemMenu
label={
<Icon name="plus" size={16} className="cursor-pointer hover:fill-gray-darkest" />
}
items={menuItems}
flat
onToggle={(isOpen) => setMenu(isOpen)}
/>
<div onClick={() => bugReportStore.removeStep(step)}>
<Icon name="trash" size={16} className="cursor-pointer hover:fill-gray-darkest" />
<div className="rounded-3xl px-4 bg-gray-lightest relative z-10">{ind + 1}</div>
<div className="w-full">
<div className="flex items-center w-full gap-2">
{/* @ts-ignore */}
<Icon name={step.icon} size={16} color="gray-darkest" className="relative z-10"/>
<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
className={cn(
'group-hover:flex items-center ml-auto gap-4',
menuOpen ? 'flex' : 'hidden'
)}
>
<ItemMenu
label={
<Icon name="plus" size={16} className="cursor-pointer hover:fill-gray-darkest" />
}
items={menuItems}
flat
onToggle={(isOpen) => setMenu(isOpen)}
/>
<div onClick={() => bugReportStore.removeStep(step)}>
<Icon name="trash" size={16} className="cursor-pointer hover:fill-gray-darkest" />
</div>
</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">
<div
key={subStep.key}
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 Step;
export default observer(Step);

View file

@ -1,20 +1,48 @@
import React from 'react';
import { Icon, Button } from 'UI';
import cn from 'classnames';
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: 'Note',
network: 'Fetch/XHR',
error: 'Console Error',
};
const Filters = {
note: 'note message or author',
network: 'url',
error: 'error name or message',
};
interface Props {
type: 'note' | 'network' | 'error';
toggleModal: (isOpen: boolean) => void;
items: SubItem[];
}
let debounceUpdate: any = () => {};
const SUB_ITEMS = {
note: NoteItem,
error: ErrorItem,
network: NetworkReq,
};
function ModalContent(props: Props) {
const [selected, setSelected] = React.useState([]);
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">
@ -22,25 +50,63 @@ function ModalContent(props: Props) {
<Icon name="quotes" 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 border rounded w-full"
style={{ background: props.type === 'note' ? '#FFFEF5' : 'white' }}
className="flex flex-col rounded -mx-4 px-4 py-2 bg-white"
style={{ height: '90vh', overflowY: 'scroll', maxWidth: '70vw', width: 620 }}
>
<div className="p-2 border-b last:border-b-none w-full">item1</div>
<div className="p-2 border-b last:border-b-none w-full">item2</div>
{list.map((item) => (
<React.Fragment key={item.key}>
{/* @ts-ignore */}
<SubItem item={item} />
</React.Fragment>
))}
</div>
<div className="flex items-center gap-2">
<Button disabled={selected.length === 0} variant="primary">
Add Selected
</Button>
<Button variant="text-primary" onClick={() => props.toggleModal(false)}>Cancel</Button>
</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.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>[];
@ -50,16 +116,56 @@ interface ModalProps {
endTime: number;
};
type: 'note' | 'network' | 'error';
toggleModal: (isOpen: boolean) => void;
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 overflow-y-scroll absolute"
style={{ maxWidth: '70vw', width: 620, height: '100vh', top: 0, right: 0, zIndex: 999 }}
className="bg-white absolute"
style={{
maxWidth: '70vw',
overflow: 'hidden',
width: 620,
height: '100vh',
top: 0,
right: 0,
zIndex: 999,
}}
>
<ModalContent type={props.type} toggleModal={props.toggleModal} />
<ModalContent type={props.type} items={items} />
</div>
);
}

View file

@ -0,0 +1,123 @@
import React from 'react';
import { Checkbox } from 'UI';
import { observer } from 'mobx-react-lite';
import { useStore } from 'App/mstore';
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) => (ogStr.length > 60 ? ogStr.slice(0, 60) + '...' : 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>{item.time}</div>
<div>{safeStr(item.url)}</div>
</div>
<div className="flex items-center gap-2">
<div className="px-1 bg-light-blue-bg rounded-xl 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 className="font-semibold">{item.title}</div>
<div className="text-secondary">{item.message}</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">{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="flex items-start p-2 gap-2 shadow-border-gray hover:shadow-border-main hover:bg-active-blue cursor-pointer"
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>
);
}

View file

@ -1,4 +1,5 @@
import { SeverityLevels } from 'App/mstore/bugReportStore';
import { SubItem, INoteItem, IError, INetworkReq } from './components/StepsComponents/SubModalItems';
export interface BugReportPdf extends ReportDefaults {
title: string;
@ -6,9 +7,8 @@ export interface BugReportPdf extends ReportDefaults {
severity: SeverityLevels;
steps: Step[];
activity: {
network: NetworkError[];
console: ConsoleError[];
clickRage: ClickRage[];
network: INetworkReq[];
console: IError[];
};
}
@ -33,19 +33,6 @@ export interface EnvData {
Resolution: string;
}
export interface NetworkError {
time: number;
}
export interface ConsoleError {
time: number;
}
export interface ClickRage {
time: number;
}
export type SubStep = Note | Error | Request;
export interface Step {
key: string;
@ -53,7 +40,7 @@ export interface Step {
time: number;
details: string;
icon: string;
substeps?: SubStep[]
substeps?: SubItem[]
}
export interface Note {

View file

@ -1,5 +1,6 @@
import { makeAutoObservable } from 'mobx';
import { BugReportPdf, ReportDefaults, Step } from 'Components/Session_/BugReport/types';
import { SubItem } from 'App/components/Session_/BugReport/components/StepsComponents/SubModalItems';
export enum SeverityLevels {
Low,
@ -20,6 +21,8 @@ export default class BugReportStore {
sessionEventSteps: Step[] = [];
chosenEventSteps: Step[] = [];
subModalType: 'note' | 'network' | 'error';
targetStep: string
pickedSubItems: Map<string, SubItem> = new Map()
constructor() {
makeAutoObservable(this);
@ -38,6 +41,8 @@ export default class BugReportStore {
this.chosenEventSteps = [];
this.subModalType = undefined;
this.isSubStepModalOpen = false;
this.targetStep = undefined;
this.pickedSubItems = new Map();
}
toggleTitleEdit(isEdit: boolean) {
@ -77,7 +82,8 @@ export default class BugReportStore {
}
setSteps(steps: Step[]) {
this.chosenEventSteps = steps;
this.chosenEventSteps = steps.map(step => ({ ...step, substeps: undefined }));
this.pickedSubItems = undefined;
}
removeStep(step: Step) {
@ -86,9 +92,34 @@ export default class BugReportStore {
);
}
toggleSubStepModal(isOpen: boolean, type: 'note' | 'network' | 'error') {
toggleSubStepModal(isOpen: boolean, type: 'note' | 'network' | 'error', stepKey?: string) {
this.isSubStepModalOpen = isOpen;
this.subModalType = type;
this.targetStep = stepKey
}
toggleSubItem(isAdded: boolean, item: SubItem) {
if (isAdded) {
this.pickedSubItems.set(item.key, item)
} else {
this.pickedSubItems.delete(item.key)
}
}
isSubItemChecked(item: SubItem) {
return this.pickedSubItems.has(item.key)
}
saveSubItems() {
const targetIndex = this.chosenEventSteps.findIndex(step => step.key === this.targetStep)
const eventStepsCopy = this.chosenEventSteps
const step = this.chosenEventSteps[targetIndex]
if (this.pickedSubItems.size > 0) {
step.substeps = Array.from(this.pickedSubItems, ([name, value]) => ({ ...value }));
}
eventStepsCopy[targetIndex] = step
return this.chosenEventSteps = eventStepsCopy
}
resetSteps() {