feat(ui) - funnels - details

This commit is contained in:
Shekar Siri 2022-06-13 11:35:23 +02:00
parent a461ad0938
commit 936d1f6f6e
27 changed files with 229 additions and 178 deletions

View file

@ -13,7 +13,7 @@ function SessionsPerBrowser(props: Props) {
const getVersions = item => {
return Object.keys(item)
.filter(i => i !== 'browser' && i !== 'count')
.filter(i => i !== 'browser' && i !== 'count' && i !== 'time' && i !== 'timestamp')
.map(i => ({ key: 'v' +i, value: item[i]}))
}
return (

View file

@ -1,7 +1,6 @@
import React from 'react';
import { numberWithCommas } from 'App/utils';
const colors = ['#3EAAAF', '#5FBABF', '#7BCBCF', '#96DCDF', '#ADDCDF'];
const colors = ['#1E889A', '#239DB2', '#28B2C9', '#36C0D7', '#65CFE1'];
const colorsx = ['#256669', '#38999e', '#3eaaaf', '#51b3b7', '#78c4c7', '#9fd5d7', '#c5e6e7'].reverse();
const compareColors = ['#394EFF', '#4D5FFF', '#808DFF', '#B3BBFF', '#E5E8FF'];
const compareColorsx = ["#222F99", "#2E3ECC", "#394EFF", "#6171FF", "#8895FF", "#B0B8FF", "#D7DCFF"].reverse();

View file

@ -123,15 +123,6 @@ function DashboardView(props: RouteComponentProps<Props>) {
</div>
<div className="flex items-center" style={{ flex: 1, justifyContent: 'end' }}>
<div className="flex items-center flex-shrink-0 justify-end" style={{ width: '300px'}}>
{/* <span className="mr-2 color-gray-medium">Time Range</span> */}
{/* <DateRange
rangeValue={period.rangeName}
startDate={period.start}
endDate={period.end}
onDateChange={(period) => dashboardStore.setPeriod(period)}
customRangeRight
direction="left"
/> */}
<SelectDateRange
style={{ width: '300px'}}
fluid

View file

@ -1,22 +1,47 @@
import React, { useEffect, useState } from 'react';
import { useStore } from 'App/mstore';
import { useObserver } from 'mobx-react-lite';
import React, { useEffect } from 'react';
import { NoContent, Loader } from 'UI';
import FunnelIssuesDropdown from '../FunnelIssuesDropdown';
import FunnelIssuesSort from '../FunnelIssuesSort';
import FunnelIssuesList from '../FunnelIssuesList';
import { DateTime } from 'luxon';
import { debounce } from 'App/utils';
import useIsMounted from 'App/hooks/useIsMounted'
function FunnelIssues(props) {
const { funnelStore } = useStore();
const funnel = useObserver(() => funnelStore.instance);
const issues = useObserver(() => funnelStore.issues);
const loading = useObserver(() => funnelStore.isLoadingIssues);
function FunnelIssues() {
const { metricStore, dashboardStore } = useStore();
const [data, setData] = useState<any>({ issues: [] });
const [loading, setLoading] = useState(false);
const isMounted = useIsMounted()
// const funnel = useObserver(() => funnelStore.instance);
// const funnel = useObserver(() => metricStore.instance);
// const issues = useObserver(() => funnelStore.issues);
// const loading = useObserver(() => funnelStore.isLoadingIssues);
const fetchIssues = (filter: any) => {
if (!isMounted()) return;
setLoading(true)
widget.fetchIssues(filter).then((res: any) => {
setData(res)
}).finally(() => {
setLoading(false)
});
}
const filter = useObserver(() => dashboardStore.drillDownFilter);
const widget: any = useObserver(() => metricStore.instance);
const startTime = DateTime.fromMillis(filter.startTimestamp).toFormat('LLL dd, yyyy HH:mm a');
const endTime = DateTime.fromMillis(filter.endTimestamp).toFormat('LLL dd, yyyy HH:mm a');
const debounceRequest: any = React.useCallback(debounce(fetchIssues, 1000), []);
const depsString = JSON.stringify(widget.series);
useEffect(() => {
// funnelStore.fetchIssues(funnel?.funnelId);
}, []);
debounceRequest({ ...filter, series: widget.toJsonDrilldown(), page: metricStore.sessionsPage, limit: metricStore.sessionsPageSize });
}, [filter.startTimestamp, filter.endTimestamp, filter.filters, depsString, metricStore.sessionsPage]);
return (
return useObserver(() => (
<div className="my-8">
<div className="flex justify-between">
<h1 className="font-medium text-2xl">Most significant issues <span className="font-normal">identified in this funnel</span></h1>
@ -29,15 +54,15 @@ function FunnelIssues(props) {
</div>
<Loader loading={loading}>
<NoContent
show={issues.length === 0}
show={!loading && data.issues.length === 0}
title="No issues found."
animatedIcon="empty-state"
>
<FunnelIssuesList />
<FunnelIssuesList issues={data.issues} />
</NoContent>
</Loader>
</div>
);
));
}
export default FunnelIssues;

View file

@ -37,7 +37,7 @@ function FunnelIssuesDropdown(props) {
}, [selectedOptions]);
const handleChange = ({ value }: any) => {
toggleSelectedValue(value);
toggleSelectedValue(value.value);
}
const toggleSelectedValue = (value: string) => {
@ -57,24 +57,24 @@ function FunnelIssuesDropdown(props) {
options={filteredOptions}
onChange={handleChange}
styles={{
control: (provided) => ({
control: (provided: any) => ({
...provided,
border: 'none',
boxShadow: 'none',
backgroundColor: 'transparent',
minHeight: 'unset',
}),
menuList: (provided) => ({
menuList: (provided: any) => ({
...provided,
padding: 0,
minWidth: '190px',
}),
}}
components={{
ValueContainer: () => null,
DropdownIndicator: () => null,
IndicatorSeparator: () => null,
IndicatorsContainer: () => null,
ValueContainer: (): any => null,
DropdownIndicator: (): any => null,
IndicatorSeparator: (): any => null,
IndicatorsContainer: (): any => null,
Control: ({ children, ...props }: any) => (
<components.Control {...props}>
{ children }
@ -84,8 +84,8 @@ function FunnelIssuesDropdown(props) {
</button>
</components.Control>
),
Placeholder: () => null,
SingleValue: () => null,
Placeholder: (): any => null,
SingleValue: (): any => null,
}}
/>
<FunnelIssuesSelectedFilters removeSelectedValue={toggleSelectedValue} />

View file

@ -3,18 +3,22 @@ import { useObserver } from 'mobx-react-lite';
import React from 'react';
import FunnelIssuesListItem from '../FunnelIssuesListItem';
function FunnelIssuesList(props) {
interface Props {
issues: any;
}
function FunnelIssuesList(props: Props) {
const { issues } = props;
const { funnelStore } = useStore();
const issuesSort = useObserver(() => funnelStore.issuesSort);
const issuesFilter = useObserver(() => funnelStore.issuesFilter.map((issue: any) => issue.value));
const issues = useObserver(() => funnelStore.issues);
// const issues = useObserver(() => funnelStore.issues);
let filteredIssues = useObserver(() => issuesFilter.length > 0 ? issues.filter((issue: any) => issuesFilter.includes(issue.type)) : issues);
filteredIssues = useObserver(() => issuesSort.sort ? filteredIssues.slice().sort((a, b) => a[issuesSort.sort] - b[issuesSort.sort]): filteredIssues);
filteredIssues = useObserver(() => issuesSort.sort ? filteredIssues.slice().sort((a: { [x: string]: number; }, b: { [x: string]: number; }) => a[issuesSort.sort] - b[issuesSort.sort]): filteredIssues);
filteredIssues = useObserver(() => issuesSort.order === 'desc' ? filteredIssues.reverse() : filteredIssues);
return useObserver(() => (
<div>
{filteredIssues.map((issue, index) => (
{filteredIssues.map((issue: any, index: React.Key) => (
<div key={index} className="mb-4">
<FunnelIssuesListItem issue={issue} />
</div>

View file

@ -9,7 +9,9 @@ interface Props {
}
function FunnelIssuesListItem(props) {
const { issue, inDetails = false } = props;
const onClick = () => {}
const onClick = () => {
// console.log('onClick', issue);
}
return (
<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 && (

View file

@ -17,8 +17,8 @@ interface Props {
function FunnelIssuesSort(props: Props) {
const { funnelStore } = useStore();
const onSortChange = (opt) => {
const [ sort, order ] = opt.value.split('-');
const onSortChange = (opt: any) => {
const [ sort, order ] = opt.value.value.split('-');
funnelStore.updateKey('issuesSort', { sort, order });
}

View file

@ -64,7 +64,7 @@ function WidgetChart(props: Props) {
const depsString = JSON.stringify(_metric.series);
const fetchMetricChartData = (metric, payload, isWidget) => {
const fetchMetricChartData = (metric: any, payload: any, isWidget: any) => {
if (!isMounted()) return;
setLoading(true)
dashboardStore.fetchMetricChartData(metric, payload, isWidget).then((res: any) => {

View file

@ -1,5 +1,4 @@
import React, { useState } from 'react';
import DropdownPlain from 'Shared/DropdownPlain';
import { metricTypes, metricOf, issueOptions } from 'App/constants/filterOptions';
import { FilterKey } from 'Types/filter/filterType';
import { useStore } from 'App/mstore';
@ -36,8 +35,9 @@ function WidgetForm(props: Props) {
const canAddSeries = metric.series.length < 3;
// const write = ({ target: { value, name } }) => metricStore.merge({ [ name ]: value });
const writeOption = ({ value: { value }, name }) => {
const obj = { [ name ]: value };
const writeOption = ({ value, name }: any) => {
value = value.value
const obj: any = { [ name ]: value };
if (name === 'metricValue') {
obj['metricValue'] = [value];
@ -64,7 +64,7 @@ function WidgetForm(props: Props) {
const onSave = () => {
const wasCreating = !metric.exists()
metricStore.save(metric, dashboardId).then((metric) => {
metricStore.save(metric, dashboardId).then((metric: any) => {
if (wasCreating) {
if (parseInt(dashboardId) > 0) {
history.replace(withSiteId(dashboardMetricDetails(parseInt(dashboardId), metric.metricId), siteId));
@ -98,15 +98,9 @@ function WidgetForm(props: Props) {
<Select
name="metricType"
options={metricTypes}
value={metricTypes.find(i => i.value === metric.metricType) || metricTypes[0]}
value={metricTypes.find((i: any) => i.value === metric.metricType) || metricTypes[0]}
onChange={ writeOption }
/>
{/* <DropdownPlain
name="metricType"
options={metricTypes}
value={ metric.metricType }
onChange={ writeOption }
/> */}
{metric.metricType === 'timeseries' && (
<>
@ -117,12 +111,6 @@ function WidgetForm(props: Props) {
defaultValue={metric.metricOf}
onChange={ writeOption }
/>
{/* <DropdownPlain
name="metricOf"
options={timeseriesOptions}
value={ metric.metricOf }
onChange={ writeOption }
/> */}
</>
)}
@ -135,12 +123,6 @@ function WidgetForm(props: Props) {
defaultValue={metric.metricOf}
onChange={ writeOption }
/>
{/* <DropdownPlain
name="metricOf"
options={tableOptions}
value={ metric.metricOf }
onChange={ writeOption }
/> */}
</>
)}
@ -153,12 +135,6 @@ function WidgetForm(props: Props) {
defaultValue={metric.metricValue[0]}
onChange={ writeOption }
/>
{/* <DropdownPlain
name="metricValue"
options={_issueOptions}
value={ metric.metricValue[0] }
onChange={ writeOption }
/> */}
</>
)}
@ -173,21 +149,13 @@ function WidgetForm(props: Props) {
defaultValue={ metric.metricFormat }
onChange={ writeOption }
/>
{/* <DropdownPlain
name="metricFormat"
options={[
{ value: 'sessionCount', text: 'Session Count' },
]}
value={ metric.metricFormat }
onChange={ writeOption }
/> */}
</>
)}
</div>
</div>
<div className="form-group">
<div className="flex items-center font-medium items-center py-2">
<div className="flex items-center font-medium py-2">
{`${(isTable || isFunnel) ? 'Filter by' : 'Chart Series'}`}
{!isTable && !isFunnel && (
<Button

View file

@ -3,8 +3,8 @@ import cn from 'classnames';
import WidgetWrapper from '../WidgetWrapper';
import { useStore } from 'App/mstore';
import { SegmentSelection } from 'UI';
import DateRange from 'Shared/DateRange';
import { useObserver } from 'mobx-react-lite';
import SelectDateRange from 'Shared/SelectDateRange';
interface Props {
className?: string;
@ -17,7 +17,7 @@ function WidgetPreview(props: Props) {
const isTimeSeries = metric.metricType === 'timeseries';
const isTable = metric.metricType === 'table';
const chagneViewType = (e, { name, value }) => {
const chagneViewType = (e, { name, value }: any) => {
metric.update({ [ name ]: value });
}
@ -63,13 +63,9 @@ function WidgetPreview(props: Props) {
)}
<div className="mx-4" />
<span className="mr-1 color-gray-medium">Time Range</span>
<DateRange
rangeValue={period.rangeName}
startDate={period.startDate}
endDate={period.endDate}
onDateChange={(period) => dashboardStore.setPeriod(period)}
customRangeRight
direction="left"
<SelectDateRange
period={period}
onChange={(period: any) => dashboardStore.setPeriod(period)}
/>
</div>
</div>

View file

@ -1,5 +1,6 @@
import React, { useEffect, useState } from 'react';
import { NoContent, Dropdown, Icon, Loader, Pagination } from 'UI';
import Select from 'Shared/Select';
import cn from 'classnames';
import { useStore } from 'App/mstore';
import SessionItem from 'Shared/SessionItem';
@ -18,20 +19,20 @@ function WidgetSessions(props: Props) {
const isMounted = useIsMounted()
const [loading, setLoading] = useState(false);
const [seriesOptions, setSeriesOptions] = useState([
{ text: 'All', value: 'all' },
{ label: 'All', value: 'all' },
]);
const [activeSeries, setActiveSeries] = useState('all');
const writeOption = (e, { name, value }) => setActiveSeries(value);
const writeOption = (e, { name, value }) => setActiveSeries(value.value);
useEffect(() => {
if (!data) return;
const seriesOptions = data.map(item => ({
text: item.seriesName,
label: item.seriesName,
value: item.seriesId,
}));
setSeriesOptions([
{ text: 'All', value: 'all' },
{ label: 'All', value: 'all' },
...seriesOptions,
]);
}, [data]);
@ -70,7 +71,7 @@ function WidgetSessions(props: Props) {
{ widget.metricType !== 'table' && (
<div className="flex items-center ml-6">
<span className="mr-2 color-gray-medium">Series</span>
<Dropdown
{/* <Dropdown
// className={stl.dropdown}
className="font-medium flex items-center hover:bg-gray-light rounded px-2 py-1"
direction="left"
@ -81,6 +82,11 @@ function WidgetSessions(props: Props) {
id="change-dropdown"
// icon={null}
icon={ <Icon name="chevron-down" color="gray-dark" size="14" className="ml-2" /> }
/> */}
<Select
options={ seriesOptions }
onChange={ writeOption }
plain
/>
</div>
)}

View file

@ -3,14 +3,11 @@ import FunnelStepText from './FunnelStepText';
import { Icon } from 'UI';
interface Props {
completed: number;
dropped: number;
filter: any;
}``
}
function FunnelBar(props: Props) {
const { filter } = props;
const { completed, dropped } = filter;
const completedPercentage = calculatePercentage(completed, dropped);
const completedPercentage = calculatePercentage(filter.sessionsCount, filter.dropDueToIssues);
return (
<div className="w-full mb-4">
@ -39,12 +36,12 @@ function FunnelBar(props: Props) {
<div className="flex justify-between py-2">
<div className="flex items-center">
<Icon name="arrow-right-short" size="20" color="green" />
<span className="mx-1 font-medium">{completed}</span>
<span>completed</span>
<span className="mx-1 font-medium">{filter.sessionsCount}</span>
<span>Completed</span>
</div>
<div className="flex items-center">
<Icon name="caret-down-fill" color="red" size={16} />
<span className="font-medium mx-1 color-red">{dropped}</span>
<span className="font-medium mx-1 color-red">{filter.dropDueToIssues}</span>
<span>Dropped off</span>
</div>
</div>

View file

@ -8,13 +8,13 @@ function FunnelStepText(props: Props) {
const total = filter.value.length;
return (
<div className="mb-2 color-gray-medium">
<span>{filter.label}</span>
<span className="color-gray-darkest">{filter.label}</span>
<span className="mx-1">{filter.operator}</span>
{filter.value.map((value, index) => (
<>
{filter.value.map((value: any, index: number) => (
<span key={index}>
<span key={index} className="font-medium color-gray-darkest">{value}</span>
{index < total - 1 && <span className="mx-1 color-gray-medium">or</span>}
</>
</span>
))}
</div>
);

View file

@ -4,22 +4,24 @@ import Funnelbar from './FunnelBar';
import cn from 'classnames';
import stl from './FunnelWidget.module.css';
import { Icon } from 'UI';
import { useObserver } from 'mobx-react-lite';
interface Props {
metric: Widget;
}
function FunnelWidget(props: Props) {
const { metric } = props;
const funnel = metric.data.funnel || { stages: [] };
return (
return useObserver(() => (
<>
<div className="w-full">
{metric.series[0].filter.filters.filter(f => f.isEvent).map((filter, index) => (
<div className={cn("flex items-start mb-4", stl.step, { [stl['step-disabled']] : !filter.isActive })}>
{funnel.stages.map((filter: any, index: any) => (
<div key={index} className={cn("flex items-start mb-4", stl.step, { [stl['step-disabled']] : !filter.isActive })}>
<div className="z-10 w-6 h-6 border mr-4 text-sm rounded-full bg-gray-lightest flex items-center justify-center leading-3">
{index + 1}
</div>
<Funnelbar key={index} completed={90} dropped={10} filter={filter} />
<Funnelbar key={index} filter={filter} />
<div className="self-end flex items-center justify-center ml-4" style={{ marginBottom: '49px'}}>
<button onClick={() => filter.updateKey('isActive', !filter.isActive)}>
<Icon name="eye-slash-fill" color={filter.isActive ? "gray-light" : "gray-darkest"} size="22" />
@ -28,7 +30,15 @@ function FunnelWidget(props: Props) {
</div>
))}
</div>
<div className="flex items-center">
<div className="flex items-center pb-4">
<div className="flex items-center">
<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>
</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">
@ -36,18 +46,17 @@ function FunnelWidget(props: Props) {
<span className="text-sm">(12%)</span>
</div>
</div>
<div className="mx-3" />
<div className="flex items-center">
<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">20</span>
<span className="text-sm">(12%)</span>
<span className="text-xl mr-2">Affected users</span>
<div className="rounded px-2 py-1 bg-gray-lightest">
<span className="text-xl font-medium">{funnel.affectedUsers}</span>
{/* <span className="text-sm">(12%)</span> */}
</div>
</div>
</div>
</>
);
));
}
export default FunnelWidget;

View file

@ -29,11 +29,11 @@ function FilterItem(props: Props) {
};
const onOperatorChange = (e, { name, value }) => {
props.onUpdate({ ...filter, operator: value })
props.onUpdate({ ...filter, operator: value.value })
}
const onSourceOperatorChange = (e, { name, value }) => {
props.onUpdate({ ...filter, sourceOperator: value })
props.onUpdate({ ...filter, sourceOperator: value.value })
}
const onUpdateSubFilter = (subFilter, subFilterIndex) => {

View file

@ -46,6 +46,7 @@ function SelectDateRange(props: Props) {
)
} }}
period={period}
right={true}
style={{ width: '100%' }}
/>
{

View file

@ -7,8 +7,9 @@ export const CUSTOM_RANGE = 'CUSTOM_RANGE';
const DATE_RANGE_LABELS = {
// LAST_30_MINUTES: '30 Minutes',
TODAY: 'Today',
YESTERDAY: 'Yesterday',
// TODAY: 'Today',
LAST_24_HOURS: 'Last 24 Hours',
// YESTERDAY: 'Yesterday',
LAST_7_DAYS: 'Past 7 Days',
LAST_30_DAYS: 'Past 30 Days',
//THIS_MONTH: 'This Month',
@ -58,6 +59,11 @@ export function getDateRangeFromValue(value) {
moment().subtract(1, 'days').startOf('day'),
moment().subtract(1, 'days').endOf('day'),
);
case DATE_RANGE_VALUES.LAST_24_HOURS:
return moment.range(
moment().subtract(24, 'hours'),
moment(),
);
case DATE_RANGE_VALUES.LAST_7_DAYS:
return moment.range(
moment().subtract(7, 'days').startOf('day'),

View file

@ -6,6 +6,7 @@ import { toast } from 'react-toastify';
import Period, { LAST_24_HOURS, LAST_7_DAYS } from 'Types/app/period';
import { getChartFormatter } from 'Types/dashboard/helper';
import Filter, { IFilter } from "./types/filter";
import Funnel from "./types/funnel";
export interface IDashboardSotre {
dashboards: IDashboard[]
@ -427,7 +428,7 @@ export default class DashboardStore implements IDashboardSotre {
}
setPeriod(period: any) {
this.period = Period({ start: period.startDate, end: period.endDate, rangeName: period.rangeValue })
this.period = new Period({ start: period.startDate, end: period.endDate, rangeName: period.rangeName })
}
fetchMetricChartData(metric: IWidget, data: any, isWidget: boolean = false): Promise<any> {
@ -440,6 +441,11 @@ export default class DashboardStore implements IDashboardSotre {
const _data = { ...data, chart: getChartFormatter(this.period)(data.chart) }
metric.setData(_data)
resolve(_data);
} else if (metric.metricType === 'funnel') {
const _data = { ...data }
_data.funnel = new Funnel().fromJSON(data)
metric.setData(_data)
resolve(_data);
} else {
const _data = {
...data,

View file

@ -113,7 +113,7 @@ export default class FunnelStore {
fetchIssues(funnelId?: string): Promise<any> {
this.isLoadingIssues = true
return new Promise((resolve, reject) => {
funnelService.fetchIssues(funnelId, this.period)
funnelService.fetchIssues(funnelId, this.period.toTimestamps())
.then(response => {
this.issues = response.map(i => new FunnelIssue().fromJSON(i))
resolve(this.issues)

View file

@ -90,10 +90,10 @@ export default class MetricStore implements IMetricStore {
// State Actions
init(metric?: IWidget|null) {
const _metric = new Widget().fromJson(sampleJsonErrors)
this.instance.update(metric || _metric)
// const _metric = new Widget().fromJson(sampleJsonErrors)
// this.instance.update(metric || _metric)
// this.instance.update(metric || new Widget())
this.instance.update(metric || new Widget())
}
updateKey(key: string, value: any) {
@ -141,7 +141,7 @@ export default class MetricStore implements IMetricStore {
const wasCreating = !metric.exists()
this.isSaving = true
return metricService.saveMetric(metric, dashboardId)
.then((metric) => {
.then((metric: any) => {
const _metric = new Widget().fromJson(metric)
if (wasCreating) {
toast.success('Metric created successfully')
@ -162,7 +162,7 @@ export default class MetricStore implements IMetricStore {
fetchList() {
this.isLoading = true
return metricService.getMetrics()
.then(metrics => {
.then((metrics: any[]) => {
this.metrics = metrics.map(m => new Widget().fromJson(m))
}).finally(() => {
this.isLoading = false
@ -172,7 +172,7 @@ export default class MetricStore implements IMetricStore {
fetch(id: string) {
this.isLoading = true
return metricService.getMetric(id)
.then(metric => {
.then((metric: any) => {
return this.instance = new Widget().fromJson(metric)
}).finally(() => {
this.isLoading = false

View file

@ -62,7 +62,7 @@ export default class Filter implements IFilter {
this.filters[index] = new FilterItem(filter)
}
updateKey(key: string, value) {
updateKey(key: string, value: any) {
this[key] = value
}

View file

@ -1,13 +1,10 @@
import Filter, { IFilter } from "./filter"
// import Filter, { IFilter } from "./filter"
import FunnelStage from './funnelStage'
export interface IFunnel {
funnelId: string
name: string
filter: IFilter
sessionsCount: number
conversionRate: number
totalConversations: number
lostConversations: number
affectedUsers: number;
conversionImpact: number
lostConversions: number
isPublic: boolean
fromJSON: (json: any) => void
toJSON: () => any
@ -15,38 +12,37 @@ export interface IFunnel {
}
export default class Funnel implements IFunnel {
funnelId: string = ''
name: string = ''
filter: IFilter = new Filter()
sessionsCount: number = 0
conversionRate: number = 0
totalConversations: number = 0
lostConversations: number = 0
affectedUsers: number = 0
conversionImpact: number = 0
lostConversions: number = 0
isPublic: boolean = false
stages: FunnelStage[] = []
constructor() {
}
fromJSON(json: any) {
this.funnelId = json.funnelId
this.name = json.name
this.filter = new Filter().fromJson(json.filter)
this.sessionsCount = json.sessionsCount
this.conversionRate = json.conversionRate
const firstStage = json.stages[0]
const lastStage = json.stages[json.stages.length - 1]
this.lostConversions = json.totalDropDueToIssues
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.affectedUsers = firstStage.usersCount ? firstStage.usersCount - lastStage.usersCount : 0;
return this
}
toJSON(): any {
return {
funnelId: this.funnelId,
name: this.name,
filter: this.filter.toJson(),
sessionsCount: this.sessionsCount,
conversionRate: this.conversionRate,
}
}
// toJSON(): any {
// return {
// // funnelId: this.funnelId,
// // name: this.name,
// // filter: this.filter.toJson(),
// // sessionsCount: this.sessionsCount,
// // conversionRate: this.conversionRate,
// }
// }
exists(): boolean {
return this.funnelId !== ''
}
// exists(): boolean {
// return this.funnelId !== ''
// }
}

View file

@ -1,10 +1,22 @@
import { makeAutoObservable, observable, action } from "mobx"
import { filterLabelMap } from 'Types/filter/newFilter';
export default class FunnelStage {
dropDueToIssues: number = 0;
dropPct: number = 0;
operator: string = "";
sessionsCount: number = 0;
usersCount: number = 0;
type: string = '';
value: string[] = [];
label: string = '';
isActive: boolean = false;
constructor() {
makeAutoObservable(this, {
isActive: observable,
updateKey: action,
})
}
fromJSON(json: any) {
this.dropDueToIssues = json.dropDueToIssues;
@ -13,6 +25,12 @@ export default class FunnelStage {
this.sessionsCount = json.sessionsCount;
this.usersCount = json.usersCount;
this.value = json.value;
this.type = json.type;
this.label = filterLabelMap[json.type] || json.type;
return this;
}
updateKey(key: any, value: any) {
this[key] = value
}
}

View file

@ -4,6 +4,7 @@ import { DateTime } from 'luxon';
import { IFilter } from "./filter";
import { metricService } from "App/services";
import Session, { ISession } from "App/mstore/types/session";
import Funnelissue from 'App/mstore/types/funnelIssue';
export interface IWidget {
metricId: any
@ -173,6 +174,10 @@ export default class Widget implements IWidget {
viewType: this.viewType,
name: this.name,
series: this.series.map((series: any) => series.toJson()),
config: {
...this.config,
col: this.metricType === 'funnel' ? 4 : this.config.col
},
}
}
@ -198,14 +203,24 @@ export default class Widget implements IWidget {
fetchSessions(metricId: any, filter: any): Promise<any> {
return new Promise((resolve, reject) => {
metricService.fetchSessions(metricId, filter).then(response => {
resolve(response.map(cat => {
metricService.fetchSessions(metricId, filter).then((response: any[]) => {
resolve(response.map((cat: { sessions: any[]; }) => {
return {
...cat,
sessions: cat.sessions.map(s => new Session().fromJson(s))
sessions: cat.sessions.map((s: any) => new Session().fromJson(s))
}
}))
})
})
}
fetchIssues(filter: any): Promise<any> {
return new Promise((resolve, reject) => {
metricService.fetchIssues(filter).then((response: any) => {
resolve({
issues: response.issues.insignificant ? response.issues.insignificant.map((issue: any) => new Funnelissue().fromJSON(issue)) : [],
})
})
})
}
}

View file

@ -13,6 +13,7 @@ export interface IMetricService {
getTemplates(): Promise<any>;
getMetricChartData(metric: IWidget, data: any, isWidget: boolean): Promise<any>;
fetchSessions(metricId: string, filter: any): Promise<any>
fetchIssues(filter: string): Promise<any>;
}
export default class MetricService implements IMetricService {
@ -32,8 +33,8 @@ export default class MetricService implements IMetricService {
*/
getMetrics(): Promise<any> {
return this.client.get('/metrics')
.then(response => response.json())
.then(response => response.data || []);
.then((response: { json: () => any; }) => response.json())
.then((response: { data: any; }) => response.data || []);
}
/**
@ -43,8 +44,8 @@ export default class MetricService implements IMetricService {
*/
getMetric(metricId: string): Promise<any> {
return this.client.get('/metrics/' + metricId)
.then(response => response.json())
.then(response => response.data || {});
.then((response: { json: () => any; }) => response.json())
.then((response: { data: any; }) => response.data || {});
}
/**
@ -58,8 +59,8 @@ export default class MetricService implements IMetricService {
const method = isCreating ? 'post' : 'put';
const url = isCreating ? '/metrics' : '/metrics/' + data[Widget.ID_KEY];
return this.client[method](url, data)
.then(response => response.json())
.then(response => response.data || {});
.then((response: { json: () => any; }) => response.json())
.then((response: { data: any; }) => response.data || {});
}
/**
@ -69,8 +70,8 @@ export default class MetricService implements IMetricService {
*/
deleteMetric(metricId: string): Promise<any> {
return this.client.delete('/metrics/' + metricId)
.then(response => response.json())
.then(response => response.data);
.then((response: { json: () => any; }) => response.json())
.then((response: { data: any; }) => response.data);
}
@ -80,15 +81,15 @@ export default class MetricService implements IMetricService {
*/
getTemplates(): Promise<any> {
return this.client.get('/metrics/templates')
.then(response => response.json())
.then(response => response.data || []);
.then((response: { json: () => any; }) => response.json())
.then((response: { data: any; }) => response.data || []);
}
getMetricChartData(metric: IWidget, data: any, isWidget: boolean = false): Promise<any> {
const path = isWidget ? `/metrics/${metric.metricId}/chart` : `/metrics/try`;
return this.client.post(path, data)
.then(response => response.json())
.then(response => response.data || {});
.then((response: { json: () => any; }) => response.json())
.then((response: { data: any; }) => response.data || {});
}
/**
@ -98,7 +99,13 @@ export default class MetricService implements IMetricService {
*/
fetchSessions(metricId: string, filter: any): Promise<any> {
return this.client.post(metricId ? `/metrics/${metricId}/sessions` : '/metrics/try/sessions', filter)
.then(response => response.json())
.then(response => response.data || []);
.then((response: { json: () => any; }) => response.json())
.then((response: { data: any; }) => response.data || []);
}
fetchIssues(filter: string): Promise<any> {
return this.client.post(`/metrics/try/issues`, filter)
.then((response: { json: () => any; }) => response.json())
.then((response: { data: any; }) => response.data || {});
}
}

View file

@ -55,6 +55,11 @@ export const filtersMap = {
[FilterKey.ISSUE]: { key: FilterKey.ISSUE, type: FilterType.ISSUE, category: FilterCategory.JAVASCRIPT, label: 'Issue', operator: 'is', operatorOptions: filterOptions.getOperatorsByKeys(['is', 'isAny', 'isNot']), icon: 'filters/click', options: filterOptions.issueOptions },
}
export const filterLabelMap = Object.keys(filtersMap).reduce((acc, key) => {
acc[key] = filtersMap[key].label
return acc
}, {})
export const liveFiltersMap = {
[FilterKey.USERID]: { key: FilterKey.USERID, type: FilterType.STRING, category: FilterCategory.USER, label: 'User Id', operator: 'contains', operatorOptions: containsFilters, icon: 'filters/userid', isLive: true },
}