feat(ui) - funnels - details
This commit is contained in:
parent
a461ad0938
commit
936d1f6f6e
27 changed files with 229 additions and 178 deletions
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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} />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 && (
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ function SelectDateRange(props: Props) {
|
|||
)
|
||||
} }}
|
||||
period={period}
|
||||
right={true}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 !== ''
|
||||
// }
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)) : [],
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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 || {});
|
||||
}
|
||||
}
|
||||
|
|
@ -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 },
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue