feat(ui) - funnels - issues list
This commit is contained in:
parent
8584cf74cb
commit
6a1e72e1d5
9 changed files with 190 additions and 11 deletions
|
|
@ -4,6 +4,7 @@ import React, { useEffect } from 'react';
|
|||
import { NoContent, Loader } from 'UI';
|
||||
import FunnelIssuesDropdown from '../FunnelIssuesDropdown';
|
||||
import FunnelIssuesSort from '../FunnelIssuesSort';
|
||||
import FunnelIssuesList from './components/FunnelIssuesList';
|
||||
|
||||
function FunnelIssues(props) {
|
||||
const { funnelStore } = useStore();
|
||||
|
|
@ -12,7 +13,7 @@ function FunnelIssues(props) {
|
|||
const loading = useObserver(() => funnelStore.isLoadingIssues);
|
||||
|
||||
useEffect(() => {
|
||||
funnelStore.fetchIssues(funnel?.funnelId);
|
||||
// funnelStore.fetchIssues(funnel?.funnelId);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
|
|
@ -32,7 +33,7 @@ function FunnelIssues(props) {
|
|||
title="No issues found."
|
||||
animatedIcon="empty-state"
|
||||
>
|
||||
Issues
|
||||
<FunnelIssuesList />
|
||||
</NoContent>
|
||||
</Loader>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,53 @@
|
|||
import React from 'react';
|
||||
import { Popup } from 'UI';
|
||||
|
||||
const MIN_WIDTH = '20px';
|
||||
interface Props {
|
||||
issue: any
|
||||
}
|
||||
function FunnelIssueGraph(props: Props) {
|
||||
const { issue } = props;
|
||||
return (
|
||||
<div className="flex rounded-sm" style={{ width: '600px' }}>
|
||||
<Popup
|
||||
trigger={
|
||||
<div style={{ width: issue.unaffectedSessionsPer + '%', minWidth: MIN_WIDTH }} className="relative">
|
||||
<div className="w-full relative rounded-tl-sm rounded-bl-sm" style={{ height: '18px', backgroundColor: 'rgba(217, 219, 238, 0.7)' }} />
|
||||
<div className="absolute ml-2 font-bold top-0 bottom-0 text-sm">{issue.unaffectedSessions}</div>
|
||||
</div>
|
||||
}
|
||||
content={ `Unaffected sessions` }
|
||||
size="tiny"
|
||||
inverted
|
||||
position="top center"
|
||||
/>
|
||||
<Popup
|
||||
trigger={
|
||||
<div style={{ width: issue.affectedSessionsPer + '%', minWidth: MIN_WIDTH}} className="border-l relative">
|
||||
<div className="w-full relative" style={{ height: '18px', backgroundColor: 'rgba(238, 238, 238, 0.7)' }} />
|
||||
<div className="absolute ml-2 font-bold top-0 bottom-0 text-sm">{issue.affectedSessions}</div>
|
||||
{/* <div className="absolute left-0 ml-1 text-xs">{issue.affectedSessionsPer}</div> */}
|
||||
</div>
|
||||
}
|
||||
content={ `Affected sessions` }
|
||||
size="tiny"
|
||||
inverted
|
||||
position="top center"
|
||||
/>
|
||||
<Popup
|
||||
trigger={
|
||||
<div style={{ width: issue.lostConversionsPer + '%', minWidth: MIN_WIDTH}} className="border-l relative">
|
||||
<div className="w-full relative rounded-tr-sm rounded-br-sm" style={{ height: '18px', backgroundColor: 'rgba(204, 0, 0, 0.26)' }} />
|
||||
<div className="absolute ml-2 font-bold top-0 bottom-0 text-sm color-red">{issue.lostConversions}</div>
|
||||
</div>
|
||||
}
|
||||
content={ `Conversion lost` }
|
||||
size="tiny"
|
||||
inverted
|
||||
position="top center"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default FunnelIssueGraph;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './FunnelIssueGraph';
|
||||
|
|
@ -5,12 +5,14 @@ import FunnelIssuesListItem from '../FunnelIssuesListItem';
|
|||
|
||||
function FunnelIssuesList(props) {
|
||||
const { funnelStore } = useStore();
|
||||
const issuesFilter = useObserver(() => funnelStore.issuesFilter.map((issue: any) => issue.value));
|
||||
const issues = useObserver(() => funnelStore.issues);
|
||||
const filteredIssues = useObserver(() => issuesFilter.length > 0 ? issues.filter((issue: any) => issuesFilter.includes(issue.type)) : issues);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{issues.map((issue, index) => (
|
||||
<div key={index}>
|
||||
{filteredIssues.map((issue, index) => (
|
||||
<div key={index} className="mb-4">
|
||||
<FunnelIssuesListItem issue={issue} />
|
||||
</div>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,85 @@
|
|||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import { Icon, TextEllipsis } from 'UI';
|
||||
import FunnelIssueGraph from '../FunnelIssueGraph';
|
||||
|
||||
interface Props {
|
||||
issue: any;
|
||||
inDetails?: boolean;
|
||||
}
|
||||
function FunnelIssuesListItem(props) {
|
||||
const { issue, inDetails = false } = props;
|
||||
const onClick = () => {}
|
||||
return (
|
||||
<div>
|
||||
list item
|
||||
</div>
|
||||
<div className={cn('flex flex-col bg-white w-full rounded border relative hover:bg-active-blue', { 'cursor-pointer bg-hover' : !inDetails })} onClick={!inDetails ? onClick : () => null}>
|
||||
{/* {inDetails && (
|
||||
<BackLink onClick={onBack} className="absolute" style={{ left: '-50px', top: '8px' }} />
|
||||
)} */}
|
||||
<div className="flex items-center px-6 py-4 relative">
|
||||
<div className="mr-3">
|
||||
<div
|
||||
className="flex items-center justify-center flex-shrink-0 mr-3 relative"
|
||||
>
|
||||
<Icon name={issue.icon.icon} style={{ fill: issue.icon.color }} size="24" className="z-10 inset-0" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{inDetails && (
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<div className="text-lg font-medium mb-2 capitalize">{issue.title}</div>
|
||||
<div className="text-xl whitespace-nowrap">
|
||||
<TextEllipsis text={issue.contextString} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!inDetails && (
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<div className="text-xl mb-2 capitalize">{issue.title}</div>
|
||||
<div className="text-sm color-gray-medium whitespace-nowrap leading-none">
|
||||
<TextEllipsis text={issue.contextString} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="text-center text-sm ml-10 flex-shrink-0">
|
||||
<div className="text-xl mb-2">{issue.affectedUsers}</div>
|
||||
<div className="color-gray-medium leading-none">Affected Users</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center text-sm ml-10 flex-shrink-0">
|
||||
<div className="text-xl mb-2 color-red">{issue.conversionImpact}<span className="text-sm ml-1">%</span></div>
|
||||
<div className="color-gray-medium leading-none">Conversion Impact</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center text-sm ml-10 flex-shrink-0">
|
||||
<div className="text-xl mb-2">{issue.lostConversions}</div>
|
||||
<div className="color-gray-medium leading-none">Lost Conversions</div>
|
||||
</div>
|
||||
</div>
|
||||
{inDetails && (
|
||||
<div className="flex items-center px-6 py-4 justify-between border-t">
|
||||
<FunnelIssueGraph issue={issue} />
|
||||
<div className="flex items-center">
|
||||
<Info label="Unaffected sessions" color="rgba(217, 219, 238, 0.7)" />
|
||||
<Info label="Affected sessions" color="rgba(238, 238, 238, 0.7)" />
|
||||
<Info label="Conversion Lost" color="rgba(204, 0, 0, 0.26)" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default FunnelIssuesListItem;
|
||||
export default FunnelIssuesListItem;
|
||||
|
||||
const Info = ({ label = '', color = 'red'}) => {
|
||||
return (
|
||||
<div className="flex items-center ml-4">
|
||||
<div className="flex text-sm items-center color-gray-medium">
|
||||
<div className={ cn("w-2 h-2 rounded-full mr-2") } style={{ backgroundColor: color }} />
|
||||
<div>{ label }</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -41,6 +41,7 @@ export default function({ styles= {}, alignRight = false, plain = false, options
|
|||
position: 'absolute',
|
||||
minWidth: 'fit-content',
|
||||
overflow: 'hidden',
|
||||
zIndex: 100,
|
||||
...(alignRight && { right: 0 })
|
||||
}),
|
||||
menuList: (provided, state) => ({
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ export default class FunnelStore {
|
|||
saveFunnel: action,
|
||||
deleteFunnel: action
|
||||
})
|
||||
this.issues = sampleIssues.map(i => new FunnelIssue().fromJSON(i));
|
||||
}
|
||||
|
||||
updateKey(key: string, value: any) {
|
||||
|
|
@ -117,4 +118,7 @@ export default class FunnelStore {
|
|||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const sampleIssues = [{"type":"missing_resource","title":"Missing Image","affectedSessions":1,"unaffectedSessions":59,"lostConversions":0,"affectedUsers":1,"conversionImpact":11,"contextString":"https://openreplay.com/images/assist-story-4.svg","issueId":"9157d556854f17cc25df3510bf7a980fd4d"},{"type":"click_rage","title":"Click Rage","affectedSessions":1,"unaffectedSessions":59,"lostConversions":0,"affectedUsers":1,"conversionImpact":11,"contextString":"Back More Info 0:40 78:02 Pause Back Skip to Issue 1x Skip Inactivity Network Fetch 6 Redux 2 Consol","issueId":"91502c2c13f69c09a68503c61b0fd4461ca"},{"type":"missing_resource","title":"Missing Image","affectedSessions":1,"unaffectedSessions":59,"lostConversions":0,"affectedUsers":1,"conversionImpact":11,"contextString":"https://openreplay.com/images/icn-slack.svg","issueId":"915b1b3a25c5f1127ec762235be7f896c3a"},{"type":"dead_click","title":"Dead Click","affectedSessions":1,"unaffectedSessions":59,"lostConversions":0,"affectedUsers":1,"conversionImpact":11,"contextString":"py-1 px-2 bg-white border border-gray-light rounded w-16","issueId":"9159e30220bb6a6a31afcaa1979a0c7d69c"},{"type":"dead_click","title":"Dead Click","affectedSessions":4,"unaffectedSessions":56,"lostConversions":2,"affectedUsers":2,"conversionImpact":61,"contextString":"OpenReplay App New Project SESSIONS ASSIST ERRORS DASHBOARDS Billing Details Announcements There are","issueId":"91515f9118ed803291f87133e2cb49a16ea"},{"type":"dead_click","title":"Dead Click","affectedSessions":2,"unaffectedSessions":58,"lostConversions":0,"affectedUsers":1,"conversionImpact":20,"contextString":"Type to search","issueId":"915832d68d21f03f83af1bfc758a1dda50b"},{"type":"missing_resource","title":"Missing Image","affectedSessions":1,"unaffectedSessions":59,"lostConversions":0,"affectedUsers":1,"conversionImpact":11,"contextString":"https://openreplay.com/images/icn-linkedin.svg","issueId":"91506bb929c2cb3679f8b01c228d8a0b5c8"},{"type":"dead_click","title":"Dead Click","affectedSessions":3,"unaffectedSessions":57,"lostConversions":0,"affectedUsers":2,"conversionImpact":31,"contextString":"Search sessions using any captured event (click, input, page, error...)","issueId":"9157be39a537e81243a2ff44ad74867941f"},{"type":"cpu","title":"High CPU","affectedSessions":1,"unaffectedSessions":59,"lostConversions":0,"affectedUsers":1,"conversionImpact":11,"contextString":"","issueId":"915a68d6bb4448b5822836dbc797bafadf9"},{"type":"dead_click","title":"Dead Click","affectedSessions":1,"unaffectedSessions":59,"lostConversions":0,"affectedUsers":1,"conversionImpact":11,"contextString":"Back More Info Report Issue This session's issues 10:35 83:19 Play Back Skip to Issue 4x Skip Inacti","issueId":"915b43e81f8da042f70ea47bd9ad14a3bb8"},{"type":"missing_resource","title":"Missing Image","affectedSessions":1,"unaffectedSessions":59,"lostConversions":0,"affectedUsers":1,"conversionImpact":11,"contextString":"https://openreplay.com/images/icn-git.svg","issueId":"915f7f277daa7a695d5bf9e233c43af7f02"},{"type":"click_rage","title":"Click Rage","affectedSessions":1,"unaffectedSessions":59,"lostConversions":0,"affectedUsers":1,"conversionImpact":11,"contextString":"Back More Info 0:35 78:02 Pause Back Skip to Issue 1x Skip Inactivity Network Fetch 6 Redux 2 Consol","issueId":"915788d9976c9f80c6f599e3e5816f2c7be"},{"type":"missing_resource","title":"Missing Image","affectedSessions":1,"unaffectedSessions":59,"lostConversions":0,"affectedUsers":1,"conversionImpact":11,"contextString":"https://openreplay.com/images/icn-twitter.svg","issueId":"915ab993784a0432c39b4a4e9248dfe6acd"},{"type":"missing_resource","title":"Missing Image","affectedSessions":1,"unaffectedSessions":59,"lostConversions":0,"affectedUsers":1,"conversionImpact":11,"contextString":"https://openreplay.com/images/logo-open-replay.svg","issueId":"915ac8719c95392adb8b79d2d5eae1063b9"}]
|
||||
|
|
@ -192,7 +192,7 @@ export default class MetricStore implements IMetricStore {
|
|||
}
|
||||
|
||||
const sampleJson = {
|
||||
// metricId: 1,
|
||||
metricId: 1,
|
||||
name: "Funnel Sample",
|
||||
metricType: 'funnel',
|
||||
series: [
|
||||
|
|
|
|||
|
|
@ -8,6 +8,10 @@ export default class FunnelIssue {
|
|||
contextString: string = ''
|
||||
conversionImpact: number = 0
|
||||
lostConversions: number = 0
|
||||
lostConversionsPer: number = 0
|
||||
affectedSessionsPer: number = 0
|
||||
unaffectedSessionsPer: number = 0
|
||||
icon: any = {}
|
||||
|
||||
constructor() {
|
||||
}
|
||||
|
|
@ -16,12 +20,54 @@ export default class FunnelIssue {
|
|||
this.issueId = json.issueId
|
||||
this.title = json.title
|
||||
this.type = json.type
|
||||
this.icon = getIconDetails(json.type)
|
||||
this.affectedSessions = json.affectedSessions
|
||||
this.affectedUsers = json.affectedUsers
|
||||
this.unaffectedSessions = json.unaffectedSessions
|
||||
this.contextString = json.contextString
|
||||
this.conversionImpact = json.conversionImpact
|
||||
this.lostConversions = json.lostConversions
|
||||
|
||||
const total = json.lostConversions + json.affectedSessions + json.unaffectedSessions;
|
||||
this.lostConversionsPer = json.lostConversions * 100 / total;
|
||||
this.affectedSessionsPer = json.affectedSessions * 100 / total;
|
||||
this.unaffectedSessionsPer = json.unaffectedSessions * 100 / total;
|
||||
return this
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const getIconDetails = (type) => {
|
||||
switch(type) {
|
||||
case 'click_rage':
|
||||
return { icon: 'funnel/emoji-angry-fill', color: '#CC0000' };
|
||||
case 'dead_click':
|
||||
return { icon: 'funnel/emoji-dizzy-fill', color: '#9C001F' };
|
||||
case 'excessive_scrolling':
|
||||
return { icon: 'funnel/mouse', color: '#D3545F' };
|
||||
case 'bad_request':
|
||||
return { icon: 'funnel/patch-exclamation-fill', color: '#D70072' };
|
||||
case 'missing_resource':
|
||||
return { icon: 'funnel/image-fill', color: '#B89C50' };
|
||||
case 'memory':
|
||||
return { icon: 'funnel/cpu-fill', color: '#8A5A83' };
|
||||
case 'cpu':
|
||||
return { icon: 'funnel/hdd-fill', color: '#8A5A83' };
|
||||
case 'slow_resource':
|
||||
return { icon: 'funnel/hourglass-top', color: '#8B006D' };
|
||||
case 'slow_page_load':
|
||||
return { icon: 'funnel/hourglass-top', color: '#8B006D' };
|
||||
case 'custom_event_error':
|
||||
case 'custom':
|
||||
return { icon: 'funnel/exclamation-circle-fill', color: '#BF6C00' };
|
||||
case 'crash':
|
||||
return { icon: 'funnel/file-x', color: '#BF2D00' };
|
||||
case 'js_exception':
|
||||
return { icon: 'funnel/exclamation-circle', color: '#BF2D00' };
|
||||
}
|
||||
|
||||
return {
|
||||
icon: 'info',
|
||||
color: 'red'
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue