feat(ui) - issues and errors widgets
This commit is contained in:
parent
06855c41f4
commit
7874dcbe0b
15 changed files with 107 additions and 52 deletions
|
|
@ -0,0 +1,15 @@
|
|||
import React from 'react';
|
||||
|
||||
|
||||
interface Props {
|
||||
|
||||
}
|
||||
function CustomMetricTableErrors(props) {
|
||||
return (
|
||||
<div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default CustomMetricTableErrors;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './CustomMetricTableErrors';
|
||||
|
|
@ -1,14 +1,20 @@
|
|||
import React from 'react';
|
||||
import SessionItem from 'Shared/SessionItem';
|
||||
|
||||
interface Props {
|
||||
data: any
|
||||
metric?: any
|
||||
isTemplate?: boolean;
|
||||
}
|
||||
|
||||
function CustomMetricTableSessions(props: Props) {
|
||||
const { data = { sessions: [] }, metric = {}, isTemplate } = props;
|
||||
console.log('data', data)
|
||||
return (
|
||||
<div>
|
||||
|
||||
{data.sessions && data.sessions.map((session: any, index: any) => (
|
||||
<SessionItem session={session} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import useIsMounted from 'App/hooks/useIsMounted'
|
|||
import FunnelWidget from 'App/components/Funnels/FunnelWidget';
|
||||
import ErrorsWidget from '../Errors/ErrorsWidget';
|
||||
import SessionWidget from '../Sessions/SessionWidget';
|
||||
import CustomMetricTableSessions from '../../Widgets/CustomMetricsWidgets/CustomMetricTableSessions';
|
||||
interface Props {
|
||||
metric: any;
|
||||
isWidget?: boolean;
|
||||
|
|
@ -86,7 +87,7 @@ function WidgetChart(props: Props) {
|
|||
}, [period, depsString]);
|
||||
|
||||
const renderChart = () => {
|
||||
const { metricType, viewType } = metric;
|
||||
const { metricType, viewType, metricOf } = metric;
|
||||
|
||||
if (metricType === 'sessions') {
|
||||
return <SessionWidget metric={metric} />
|
||||
|
|
@ -129,6 +130,14 @@ function WidgetChart(props: Props) {
|
|||
}
|
||||
|
||||
if (metricType === 'table') {
|
||||
if (metricOf === 'SESSIONS') {
|
||||
return <CustomMetricTableSessions
|
||||
metric={metric}
|
||||
data={data}
|
||||
// onClick={onChartClick}
|
||||
isTemplate={isTemplate}
|
||||
/>
|
||||
}
|
||||
if (viewType === 'table') {
|
||||
return <CustomMetricTable
|
||||
metric={metric} data={data[0]}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ function FunnelBar(props: Props) {
|
|||
overflow: 'hidden',
|
||||
}}>
|
||||
<div className="flex items-center" style={{
|
||||
width: `${completedPercentage}%`,
|
||||
width: `${filter.completedPercentage}%`,
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
|
|
@ -30,7 +30,7 @@ function FunnelBar(props: Props) {
|
|||
// height: '10px',
|
||||
backgroundColor: '#00b5ad',
|
||||
}}>
|
||||
<div className="color-white absolute right-0 flex items-center font-medium mr-2 leading-3">{completedPercentage}%</div>
|
||||
<div className="color-white absolute right-0 flex items-center font-medium mr-2 leading-3">{filter.completedPercentage}%</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between py-2">
|
||||
|
|
@ -41,8 +41,8 @@ function FunnelBar(props: Props) {
|
|||
</div>
|
||||
<div className="flex items-center">
|
||||
<Icon name="caret-down-fill" color="red" size={16} />
|
||||
<span className="font-medium mx-1 color-red">{filter.dropDueToIssues}</span>
|
||||
<span>Dropped off</span>
|
||||
<span className="font-medium mx-1 color-red">{filter.droppedCount}</span>
|
||||
<span>Dropped</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -53,8 +53,9 @@ export default FunnelBar;
|
|||
|
||||
const calculatePercentage = (completed: number, dropped: number) => {
|
||||
const total = completed + dropped;
|
||||
if (total === 0) {
|
||||
return 0;
|
||||
}
|
||||
return Math.round((completed / total) * 100);
|
||||
if (dropped === 0) return 100;
|
||||
if (total === 0) return 0;
|
||||
|
||||
return Math.round((completed / dropped) * 100);
|
||||
|
||||
}
|
||||
|
|
@ -35,15 +35,15 @@ function FunnelWidget(props: Props) {
|
|||
<span className="text-xl mr-2">Lost conversions</span>
|
||||
<div className="rounded px-2 py-1 bg-red-lightest color-red">
|
||||
<span className="text-xl mr-2 font-medium">{funnel.lostConversions}</span>
|
||||
<span className="text-sm">(12%)</span>
|
||||
<span className="text-sm">({funnel.lostConversionsPercentage}%)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx-3" />
|
||||
<div className="flex items-center">
|
||||
<span className="text-xl mr-2">Total conversions</span>
|
||||
<div className="rounded px-2 py-1 bg-tealx-lightest color-tealx">
|
||||
<span className="text-xl mr-2 font-medium">20</span>
|
||||
<span className="text-sm">(12%)</span>
|
||||
<span className="text-xl mr-2 font-medium">{funnel.totalConversions}</span>
|
||||
<span className="text-sm">({funnel.totalConversionsPercentage}%)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx-3" />
|
||||
|
|
|
|||
|
|
@ -54,10 +54,10 @@ function SelectDateRange(props: Props) {
|
|||
<OutsideClickDetectingDiv
|
||||
onClickOutside={() => setIsCustom(false)}
|
||||
>
|
||||
<div className="absolute top-0 mx-auto mt-10 z-40" style={{
|
||||
<div className="absolute top-0 mt-10 z-40 right-0" style={{
|
||||
width: '770px',
|
||||
margin: 'auto 50vh 0',
|
||||
transform: 'translateX(-50%)'
|
||||
// margin: 'auto 50vh 0',
|
||||
// transform: 'translateX(-50%)'
|
||||
}}>
|
||||
<DateRangePopup
|
||||
onApply={ onApplyDateRange }
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ export default ({
|
|||
}) => (
|
||||
<div
|
||||
{ ...props }
|
||||
className={ cn(className, styles.label, 'border') }
|
||||
className={ cn('border rounded bg-gray-lightest px-2 w-fit', className) }
|
||||
>
|
||||
{ children }
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -80,6 +80,7 @@ export const metricOf = [
|
|||
{ text: 'Session Count', label: 'Session Count', value: 'sessionCount', type: 'timeseries' },
|
||||
{ text: 'Users', label: 'Users', value: FilterKey.USERID, type: 'table' },
|
||||
{ text: 'Sessions', label: 'Sessions', value: FilterKey.SESSIONS, type: 'table' },
|
||||
{ text: 'JS Errors', label: 'JS Errors', value: FilterKey.ERRORS, type: 'table' },
|
||||
{ text: 'Issues', label: 'Issues', value: FilterKey.ISSUE, type: 'table' },
|
||||
{ text: 'Browsers', label: 'Browsers', value: FilterKey.USER_BROWSER, type: 'table' },
|
||||
{ text: 'Devices', label: 'Devices', value: FilterKey.USER_DEVICE, type: 'table' },
|
||||
|
|
|
|||
|
|
@ -3,10 +3,11 @@ import Dashboard, { IDashboard } from "./types/dashboard"
|
|||
import Widget, { IWidget } from "./types/widget";
|
||||
import { dashboardService, metricService } from "App/services";
|
||||
import { toast } from 'react-toastify';
|
||||
import Period, { LAST_24_HOURS, LAST_7_DAYS } from 'Types/app/period';
|
||||
import Period, { LAST_24_HOURS, LAST_30_DAYS } from 'Types/app/period';
|
||||
import { getChartFormatter } from 'Types/dashboard/helper';
|
||||
import Filter, { IFilter } from "./types/filter";
|
||||
import Funnel from "./types/funnel";
|
||||
import Session from "./types/session";
|
||||
|
||||
export interface IDashboardSotre {
|
||||
dashboards: IDashboard[]
|
||||
|
|
@ -79,7 +80,7 @@ export default class DashboardStore implements IDashboardSotre {
|
|||
currentWidget: Widget = new Widget()
|
||||
widgetCategories: any[] = []
|
||||
widgets: Widget[] = []
|
||||
period: Period = Period({ rangeName: LAST_24_HOURS })
|
||||
period: Period = Period({ rangeName: LAST_30_DAYS })
|
||||
drillDownFilter: Filter = new Filter()
|
||||
startTimestamp: number = 0
|
||||
endTimestamp: number = 0
|
||||
|
|
@ -434,9 +435,8 @@ export default class DashboardStore implements IDashboardSotre {
|
|||
fetchMetricChartData(metric: IWidget, data: any, isWidget: boolean = false): Promise<any> {
|
||||
const period = this.period.toTimestamps()
|
||||
return new Promise((resolve, reject) => {
|
||||
// this.isLoading = true
|
||||
return metricService.getMetricChartData(metric, { ...period, ...data, key: metric.predefinedKey }, isWidget)
|
||||
.then(data => {
|
||||
.then((data: any) => {
|
||||
if (metric.metricType === 'predefined' && metric.viewType === 'overview') {
|
||||
const _data = { ...data, chart: getChartFormatter(this.period)(data.chart) }
|
||||
metric.setData(_data)
|
||||
|
|
@ -450,35 +450,41 @@ export default class DashboardStore implements IDashboardSotre {
|
|||
const _data = {
|
||||
...data,
|
||||
}
|
||||
if (data.hasOwnProperty('chart')) {
|
||||
_data['chart'] = getChartFormatter(this.period)(data.chart)
|
||||
_data['namesMap'] = data.chart
|
||||
.map(i => Object.keys(i))
|
||||
.flat()
|
||||
.filter(i => i !== 'time' && i !== 'timestamp')
|
||||
.reduce((unique: any, item: any) => {
|
||||
if (!unique.includes(item)) {
|
||||
unique.push(item);
|
||||
}
|
||||
return unique;
|
||||
}, [])
|
||||
|
||||
// TODO refactor to widget class
|
||||
if (metric.metricOf === 'SESSIONS') {
|
||||
_data['sessions'] = data.sessions.map((s: any) => new Session().fromJson(s))
|
||||
} else {
|
||||
_data['chart'] = getChartFormatter(this.period)(Array.isArray(data) ? data : []);
|
||||
_data['namesMap'] = Array.isArray(data) ? data.map(i => Object.keys(i))
|
||||
.flat()
|
||||
.filter(i => i !== 'time' && i !== 'timestamp')
|
||||
.reduce((unique: any, item: any) => {
|
||||
if (!unique.includes(item)) {
|
||||
unique.push(item);
|
||||
}
|
||||
return unique;
|
||||
}, []) : []
|
||||
if (data.hasOwnProperty('chart')) {
|
||||
_data['chart'] = getChartFormatter(this.period)(data.chart)
|
||||
_data['namesMap'] = data.chart
|
||||
.map(i => Object.keys(i))
|
||||
.flat()
|
||||
.filter(i => i !== 'time' && i !== 'timestamp')
|
||||
.reduce((unique: any, item: any) => {
|
||||
if (!unique.includes(item)) {
|
||||
unique.push(item);
|
||||
}
|
||||
return unique;
|
||||
}, [])
|
||||
} else {
|
||||
_data['chart'] = getChartFormatter(this.period)(Array.isArray(data) ? data : []);
|
||||
_data['namesMap'] = Array.isArray(data) ? data.map(i => Object.keys(i))
|
||||
.flat()
|
||||
.filter(i => i !== 'time' && i !== 'timestamp')
|
||||
.reduce((unique: any, item: any) => {
|
||||
if (!unique.includes(item)) {
|
||||
unique.push(item);
|
||||
}
|
||||
return unique;
|
||||
}, []) : []
|
||||
}
|
||||
}
|
||||
|
||||
metric.setData(_data)
|
||||
resolve(_data);
|
||||
}
|
||||
}).catch((err) => {
|
||||
}).catch((err: any) => {
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -2,8 +2,11 @@ import FunnelStage from './funnelStage'
|
|||
|
||||
export interface IFunnel {
|
||||
affectedUsers: number;
|
||||
totalConversions: number;
|
||||
totalConversionsPercentage: number;
|
||||
conversionImpact: number
|
||||
lostConversions: number
|
||||
lostConversionsPercentage: number
|
||||
isPublic: boolean
|
||||
fromJSON: (json: any) => void
|
||||
toJSON: () => any
|
||||
|
|
@ -12,8 +15,11 @@ export interface IFunnel {
|
|||
|
||||
export default class Funnel implements IFunnel {
|
||||
affectedUsers: number = 0
|
||||
totalConversions: number = 0
|
||||
conversionImpact: number = 0
|
||||
lostConversions: number = 0
|
||||
lostConversionsPercentage: number = 0
|
||||
totalConversionsPercentage: number = 0
|
||||
isPublic: boolean = false
|
||||
stages: FunnelStage[] = []
|
||||
|
||||
|
|
@ -24,9 +30,12 @@ export default class Funnel implements IFunnel {
|
|||
if (json.stages.length >= 1) {
|
||||
const firstStage = json.stages[0]
|
||||
const lastStage = json.stages[json.stages.length - 1]
|
||||
this.lostConversions = json.totalDropDueToIssues
|
||||
this.lostConversions = firstStage.sessionsCount - lastStage.sessionsCount
|
||||
this.lostConversionsPercentage = this.lostConversions / firstStage.sessionsCount * 100
|
||||
this.totalConversions = lastStage.sessionsCount
|
||||
this.totalConversionsPercentage = this.totalConversions / firstStage.sessionsCount * 100
|
||||
this.conversionImpact = this.lostConversions ? Math.round((this.lostConversions / firstStage.sessionsCount) * 100) : 0;
|
||||
this.stages = json.stages ? json.stages.map((stage: any) => new FunnelStage().fromJSON(stage)) : []
|
||||
this.stages = json.stages ? json.stages.map((stage: any, index: number) => new FunnelStage().fromJSON(stage, firstStage.sessionsCount, index > 0 ? json.stages[index - 1].sessionsCount : stage.sessionsCount)) : []
|
||||
this.affectedUsers = firstStage.usersCount ? firstStage.usersCount - lastStage.usersCount : 0;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,9 @@ export default class FunnelStage {
|
|||
type: string = '';
|
||||
value: string[] = [];
|
||||
label: string = '';
|
||||
isActive: boolean = false;
|
||||
isActive: boolean = true;
|
||||
completedPercentage: number = 0;
|
||||
droppedCount: number = 0;
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this, {
|
||||
|
|
@ -18,7 +20,7 @@ export default class FunnelStage {
|
|||
})
|
||||
}
|
||||
|
||||
fromJSON(json: any) {
|
||||
fromJSON(json: any, total: number = 0, previousSessionCount: number = 0) {
|
||||
this.dropDueToIssues = json.dropDueToIssues;
|
||||
this.dropPct = json.dropPct;
|
||||
this.operator = json.operator;
|
||||
|
|
@ -27,6 +29,8 @@ export default class FunnelStage {
|
|||
this.value = json.value;
|
||||
this.type = json.type;
|
||||
this.label = filterLabelMap[json.type] || json.type;
|
||||
this.completedPercentage = total ? Math.round((this.sessionsCount / total) * 100) : 0;
|
||||
this.droppedCount = previousSessionCount - this.sessionsCount;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -230,9 +230,11 @@ export default class Widget implements IWidget {
|
|||
fetchIssue(funnelId: any, issueId: any, params: any): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
metricService.fetchIssue(funnelId, issueId, params).then((response: any) => {
|
||||
response = response[0]
|
||||
console.log('response', response)
|
||||
resolve({
|
||||
issue: new Funnelissue().fromJSON(response.issue),
|
||||
sessions: response.sessions.map((s: any) => new Session().fromJson(s)),
|
||||
sessions: response.sessions.sessions.map((s: any) => new Session().fromJson(s)),
|
||||
})
|
||||
}).catch((error: any) => {
|
||||
reject(error)
|
||||
|
|
|
|||
|
|
@ -109,8 +109,8 @@ export default class MetricService implements IMetricService {
|
|||
.then((response: { data: any; }) => response.data || {});
|
||||
}
|
||||
|
||||
fetchIssue(funnelId: string, issueId: string, params: any): Promise<any> {
|
||||
return this.client.post(`/funnels/${funnelId}/issues/${issueId}/sessions`, params)
|
||||
fetchIssue(metricId: string, issueId: string, params: any): Promise<any> {
|
||||
return this.client.post(`/custom_metrics/${metricId}/issues/${issueId}/sessions`, params)
|
||||
.then((response: { json: () => any; }) => response.json())
|
||||
.then((response: { data: any; }) => response.data || {});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -92,5 +92,6 @@ export enum FilterKey {
|
|||
GRAPHQL_REQUEST_BODY = "GRAPHQL_REQUEST_BODY",
|
||||
GRAPHQL_RESPONSE_BODY = "GRAPHQL_RESPONSE_BODY",
|
||||
|
||||
SESSIONS = 'SESSIONS'
|
||||
SESSIONS = 'SESSIONS',
|
||||
ERRORS = 'ERRORS'
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue