feat(ui) - issues and errors widgets

This commit is contained in:
Shekar Siri 2022-06-14 12:47:43 +02:00
parent 06855c41f4
commit 7874dcbe0b
15 changed files with 107 additions and 52 deletions

View file

@ -0,0 +1,15 @@
import React from 'react';
interface Props {
}
function CustomMetricTableErrors(props) {
return (
<div>
</div>
);
}
export default CustomMetricTableErrors;

View file

@ -0,0 +1 @@
export { default } from './CustomMetricTableErrors';

View file

@ -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>
);
}

View file

@ -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]}

View file

@ -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);
}

View file

@ -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" />

View file

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

View file

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

View file

@ -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' },

View file

@ -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)
})
})

View file

@ -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;
}

View file

@ -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;
}

View file

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

View file

@ -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 || {});
}

View file

@ -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'
}