From 936d1f6f6ef4e4073570fed000b586c99792d521 Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Mon, 13 Jun 2022 11:35:23 +0200 Subject: [PATCH] feat(ui) - funnels - details --- .../SessionsPerBrowser/SessionsPerBrowser.tsx | 2 +- .../Dashboard/Widgets/common/Styles.js | 3 +- .../DashboardView/DashboardView.tsx | 9 --- .../Funnels/FunnelIssues/FunnelIssues.tsx | 49 +++++++++++---- .../FunnelIssuesDropdown.tsx | 18 +++--- .../FunnelIssuesList/FunnelIssuesList.tsx | 12 ++-- .../FunnelIssuesListItem.tsx | 4 +- .../FunnelIssuesSort/FunnelIssuesSort.tsx | 4 +- .../components/WidgetChart/WidgetChart.tsx | 2 +- .../components/WidgetForm/WidgetForm.tsx | 44 ++------------ .../WidgetPreview/WidgetPreview.tsx | 14 ++--- .../WidgetSessions/WidgetSessions.tsx | 16 +++-- .../Funnels/FunnelWidget/FunnelBar.tsx | 13 ++-- .../Funnels/FunnelWidget/FunnelStepText.tsx | 8 +-- .../Funnels/FunnelWidget/FunnelWidget.tsx | 31 ++++++---- .../shared/Filters/FilterItem/FilterItem.tsx | 4 +- .../SelectDateRange/SelectDateRange.tsx | 1 + frontend/app/dateRange.js | 10 +++- frontend/app/mstore/dashboardStore.ts | 8 ++- frontend/app/mstore/funnelStore.ts | 2 +- frontend/app/mstore/metricStore.ts | 12 ++-- frontend/app/mstore/types/filter.ts | 2 +- frontend/app/mstore/types/funnel.ts | 60 +++++++++---------- frontend/app/mstore/types/funnelStage.ts | 18 ++++++ frontend/app/mstore/types/widget.ts | 21 ++++++- frontend/app/services/MetricService.ts | 35 ++++++----- frontend/app/types/filter/newFilter.js | 5 ++ 27 files changed, 229 insertions(+), 178 deletions(-) diff --git a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SessionsPerBrowser/SessionsPerBrowser.tsx b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SessionsPerBrowser/SessionsPerBrowser.tsx index ad8663390..6b155364d 100644 --- a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SessionsPerBrowser/SessionsPerBrowser.tsx +++ b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SessionsPerBrowser/SessionsPerBrowser.tsx @@ -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 ( diff --git a/frontend/app/components/Dashboard/Widgets/common/Styles.js b/frontend/app/components/Dashboard/Widgets/common/Styles.js index 6da043a28..1804b6dbc 100644 --- a/frontend/app/components/Dashboard/Widgets/common/Styles.js +++ b/frontend/app/components/Dashboard/Widgets/common/Styles.js @@ -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(); diff --git a/frontend/app/components/Dashboard/components/DashboardView/DashboardView.tsx b/frontend/app/components/Dashboard/components/DashboardView/DashboardView.tsx index eed6bbaf9..fd6bd2ee3 100644 --- a/frontend/app/components/Dashboard/components/DashboardView/DashboardView.tsx +++ b/frontend/app/components/Dashboard/components/DashboardView/DashboardView.tsx @@ -123,15 +123,6 @@ function DashboardView(props: RouteComponentProps) {
- {/* Time Range */} - {/* dashboardStore.setPeriod(period)} - customRangeRight - direction="left" - /> */} funnelStore.instance); - const issues = useObserver(() => funnelStore.issues); - const loading = useObserver(() => funnelStore.isLoadingIssues); +function FunnelIssues() { + const { metricStore, dashboardStore } = useStore(); + const [data, setData] = useState({ 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(() => (

Most significant issues identified in this funnel

@@ -29,15 +54,15 @@ function FunnelIssues(props) {
- +
- ); + )); } export default FunnelIssues; \ No newline at end of file diff --git a/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesDropdown/FunnelIssuesDropdown.tsx b/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesDropdown/FunnelIssuesDropdown.tsx index 7bf620cd1..547e896ed 100644 --- a/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesDropdown/FunnelIssuesDropdown.tsx +++ b/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesDropdown/FunnelIssuesDropdown.tsx @@ -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) => ( { children } @@ -84,8 +84,8 @@ function FunnelIssuesDropdown(props) { ), - Placeholder: () => null, - SingleValue: () => null, + Placeholder: (): any => null, + SingleValue: (): any => null, }} /> diff --git a/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesList/FunnelIssuesList.tsx b/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesList/FunnelIssuesList.tsx index 0ef7dc436..b46a3cbae 100644 --- a/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesList/FunnelIssuesList.tsx +++ b/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesList/FunnelIssuesList.tsx @@ -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(() => (
- {filteredIssues.map((issue, index) => ( + {filteredIssues.map((issue: any, index: React.Key) => (
diff --git a/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesListItem/FunnelIssuesListItem.tsx b/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesListItem/FunnelIssuesListItem.tsx index 4adfbeb31..1335c1353 100644 --- a/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesListItem/FunnelIssuesListItem.tsx +++ b/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesListItem/FunnelIssuesListItem.tsx @@ -9,7 +9,9 @@ interface Props { } function FunnelIssuesListItem(props) { const { issue, inDetails = false } = props; - const onClick = () => {} + const onClick = () => { + // console.log('onClick', issue); + } return (
null}> {/* {inDetails && ( diff --git a/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesSort/FunnelIssuesSort.tsx b/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesSort/FunnelIssuesSort.tsx index cdd9a0c3c..7bb55cf50 100644 --- a/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesSort/FunnelIssuesSort.tsx +++ b/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesSort/FunnelIssuesSort.tsx @@ -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 }); } diff --git a/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx b/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx index 5b33ac26c..62ab68a1d 100644 --- a/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx +++ b/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx @@ -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) => { diff --git a/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx b/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx index 2e7f998e9..0ba99c29d 100644 --- a/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx +++ b/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx @@ -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) {
)} diff --git a/frontend/app/components/Funnels/FunnelWidget/FunnelBar.tsx b/frontend/app/components/Funnels/FunnelWidget/FunnelBar.tsx index 55878e278..f0c045ff7 100644 --- a/frontend/app/components/Funnels/FunnelWidget/FunnelBar.tsx +++ b/frontend/app/components/Funnels/FunnelWidget/FunnelBar.tsx @@ -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 (
@@ -39,12 +36,12 @@ function FunnelBar(props: Props) {
- {completed} - completed + {filter.sessionsCount} + Completed
- {dropped} + {filter.dropDueToIssues} Dropped off
diff --git a/frontend/app/components/Funnels/FunnelWidget/FunnelStepText.tsx b/frontend/app/components/Funnels/FunnelWidget/FunnelStepText.tsx index c805fdef5..f0c9ef84c 100644 --- a/frontend/app/components/Funnels/FunnelWidget/FunnelStepText.tsx +++ b/frontend/app/components/Funnels/FunnelWidget/FunnelStepText.tsx @@ -8,13 +8,13 @@ function FunnelStepText(props: Props) { const total = filter.value.length; return (
- {filter.label} + {filter.label} {filter.operator} - {filter.value.map((value, index) => ( - <> + {filter.value.map((value: any, index: number) => ( + {value} {index < total - 1 && or} - + ))}
); diff --git a/frontend/app/components/Funnels/FunnelWidget/FunnelWidget.tsx b/frontend/app/components/Funnels/FunnelWidget/FunnelWidget.tsx index 5f7c1cd91..0146a9c1f 100644 --- a/frontend/app/components/Funnels/FunnelWidget/FunnelWidget.tsx +++ b/frontend/app/components/Funnels/FunnelWidget/FunnelWidget.tsx @@ -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(() => ( <>
- {metric.series[0].filter.filters.filter(f => f.isEvent).map((filter, index) => ( -
+ {funnel.stages.map((filter: any, index: any) => ( +
{index + 1}
- +
))}
-
+
+
+ Lost conversions +
+ {funnel.lostConversions} + (12%) +
+
+
Total conversions
@@ -36,18 +46,17 @@ function FunnelWidget(props: Props) { (12%)
-
- Lost conversions -
- 20 - (12%) + Affected users +
+ {funnel.affectedUsers} + {/* (12%) */}
- ); + )); } export default FunnelWidget; \ No newline at end of file diff --git a/frontend/app/components/shared/Filters/FilterItem/FilterItem.tsx b/frontend/app/components/shared/Filters/FilterItem/FilterItem.tsx index 36330db44..0910b8e4c 100644 --- a/frontend/app/components/shared/Filters/FilterItem/FilterItem.tsx +++ b/frontend/app/components/shared/Filters/FilterItem/FilterItem.tsx @@ -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) => { diff --git a/frontend/app/components/shared/SelectDateRange/SelectDateRange.tsx b/frontend/app/components/shared/SelectDateRange/SelectDateRange.tsx index fad42acd3..8f43c2095 100644 --- a/frontend/app/components/shared/SelectDateRange/SelectDateRange.tsx +++ b/frontend/app/components/shared/SelectDateRange/SelectDateRange.tsx @@ -46,6 +46,7 @@ function SelectDateRange(props: Props) { ) } }} period={period} + right={true} style={{ width: '100%' }} /> { diff --git a/frontend/app/dateRange.js b/frontend/app/dateRange.js index 20e5c5544..ab28ffd3a 100644 --- a/frontend/app/dateRange.js +++ b/frontend/app/dateRange.js @@ -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'), diff --git a/frontend/app/mstore/dashboardStore.ts b/frontend/app/mstore/dashboardStore.ts index 18a49a996..a89be0b99 100644 --- a/frontend/app/mstore/dashboardStore.ts +++ b/frontend/app/mstore/dashboardStore.ts @@ -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 { @@ -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, diff --git a/frontend/app/mstore/funnelStore.ts b/frontend/app/mstore/funnelStore.ts index 77d4a772e..d727661ea 100644 --- a/frontend/app/mstore/funnelStore.ts +++ b/frontend/app/mstore/funnelStore.ts @@ -113,7 +113,7 @@ export default class FunnelStore { fetchIssues(funnelId?: string): Promise { 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) diff --git a/frontend/app/mstore/metricStore.ts b/frontend/app/mstore/metricStore.ts index 2f847fe94..f2920acaf 100644 --- a/frontend/app/mstore/metricStore.ts +++ b/frontend/app/mstore/metricStore.ts @@ -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 diff --git a/frontend/app/mstore/types/filter.ts b/frontend/app/mstore/types/filter.ts index c557c5a1f..11434e660 100644 --- a/frontend/app/mstore/types/filter.ts +++ b/frontend/app/mstore/types/filter.ts @@ -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 } diff --git a/frontend/app/mstore/types/funnel.ts b/frontend/app/mstore/types/funnel.ts index ffe7037ef..43bc5a8b3 100644 --- a/frontend/app/mstore/types/funnel.ts +++ b/frontend/app/mstore/types/funnel.ts @@ -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 !== '' + // } } \ No newline at end of file diff --git a/frontend/app/mstore/types/funnelStage.ts b/frontend/app/mstore/types/funnelStage.ts index 8714324f6..d4b975518 100644 --- a/frontend/app/mstore/types/funnelStage.ts +++ b/frontend/app/mstore/types/funnelStage.ts @@ -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 + } } \ No newline at end of file diff --git a/frontend/app/mstore/types/widget.ts b/frontend/app/mstore/types/widget.ts index 8933602ac..3fbe21893 100644 --- a/frontend/app/mstore/types/widget.ts +++ b/frontend/app/mstore/types/widget.ts @@ -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 { 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 { + 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)) : [], + }) + }) + }) + } } \ No newline at end of file diff --git a/frontend/app/services/MetricService.ts b/frontend/app/services/MetricService.ts index a64cf7318..f30dbd32d 100644 --- a/frontend/app/services/MetricService.ts +++ b/frontend/app/services/MetricService.ts @@ -13,6 +13,7 @@ export interface IMetricService { getTemplates(): Promise; getMetricChartData(metric: IWidget, data: any, isWidget: boolean): Promise; fetchSessions(metricId: string, filter: any): Promise + fetchIssues(filter: string): Promise; } export default class MetricService implements IMetricService { @@ -32,8 +33,8 @@ export default class MetricService implements IMetricService { */ getMetrics(): Promise { 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 { 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 { 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 { 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 { 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 { 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 { + return this.client.post(`/metrics/try/issues`, filter) + .then((response: { json: () => any; }) => response.json()) + .then((response: { data: any; }) => response.data || {}); } } \ No newline at end of file diff --git a/frontend/app/types/filter/newFilter.js b/frontend/app/types/filter/newFilter.js index 9df4984b5..61c0e42bd 100644 --- a/frontend/app/types/filter/newFilter.js +++ b/frontend/app/types/filter/newFilter.js @@ -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 }, }