From 130e00748b0cd55c0b1deac7d5818380d5fdac48 Mon Sep 17 00:00:00 2001 From: nick-delirium Date: Thu, 5 Dec 2024 16:37:42 +0100 Subject: [PATCH] ui: comparing for funnels, alternate column view, some refactoring to prepare for customizations... --- .../CustomMetricsWidgets/AreaChart.tsx | 4 +- .../Widgets/CustomMetricsWidgets/BarChart.tsx | 4 +- .../CustomMetricLineChart.tsx | 4 +- .../CustomMetricPieChart.tsx | 5 +- .../components/WidgetChart/WidgetChart.tsx | 87 ++++-- .../WidgetDateRange/WidgetDateRange.tsx | 16 +- .../Dashboard/components/WidgetOptions.tsx | 25 +- .../WidgetPreview/WidgetPreview.tsx | 96 +----- .../WidgetWrapper/WidgetWrapper.tsx | 100 +++---- .../Funnels/FunnelWidget/FunnelBar.tsx | 187 ++++++++---- .../Funnels/FunnelWidget/FunnelStepText.tsx | 2 +- .../FunnelWidget/FunnelWidget.module.css | 20 -- .../Funnels/FunnelWidget/FunnelWidget.tsx | 282 ++++++++++-------- .../SelectDateRange/SelectDateRange.tsx | 3 +- frontend/app/mstore/types/funnel.ts | 2 +- frontend/app/mstore/types/widget.ts | 7 +- 16 files changed, 458 insertions(+), 386 deletions(-) diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/AreaChart.tsx b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/AreaChart.tsx index 023579720..67e93516a 100644 --- a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/AreaChart.tsx +++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/AreaChart.tsx @@ -19,6 +19,7 @@ interface Props { yaxis?: Record; label?: string; hideLegend?: boolean; + inGrid?: boolean; } function CustomAreaChart(props: Props) { @@ -29,6 +30,7 @@ function CustomAreaChart(props: Props) { yaxis = { ...Styles.yaxis }, label = 'Number of Sessions', hideLegend = false, + inGrid, } = props; return ( @@ -39,7 +41,7 @@ function CustomAreaChart(props: Props) { onClick={onClick} > {!hideLegend && ( - + )} { @@ -75,6 +76,7 @@ function CustomBarChart(props: Props) { yaxis = { ...Styles.yaxis }, label = 'Number of Sessions', hideLegend = false, + inGrid, } = props; const resultChart = data.chart.map((item, i) => { @@ -112,7 +114,7 @@ function CustomBarChart(props: Props) { {!hideLegend && ( - + )} { @@ -54,7 +56,7 @@ function CustomMetricLineChart(props: Props) { onClick={onClick} > {!hideLegend && ( - + )} void; + inGrid?: boolean; } function CustomMetricPieChart(props: Props) { - const { metric, data, onClick = () => null } = props; + const { metric, data, onClick = () => null, inGrid } = props; const onClickHandler = (event) => { if (event && !event.payload.group) { @@ -62,7 +63,7 @@ function CustomMetricPieChart(props: Props) { > - + (); const isMounted = useIsMounted(); @@ -76,13 +76,15 @@ function WidgetChart(props: Props) { useEffect(() => { if (!data.chart) return; - const series = data.chart[0] ? Object.keys(data.chart[0]).filter( - (key) => key !== 'time' && key !== 'timestamp' - ) : [] + const series = data.chart[0] + ? Object.keys(data.chart[0]).filter( + (key) => key !== 'time' && key !== 'timestamp' + ) + : []; if (series.length) { - setEnabledRows(series) + setEnabledRows(series); } - }, [data.chart]) + }, [data.chart]); const onChartClick = (event: any) => { if (event) { @@ -132,7 +134,7 @@ function WidgetChart(props: Props) { .fetchMetricChartData(metric, payload, isSaved, period, isComparison) .then((res: any) => { if (isMounted()) { - if (isComparison) setCompData(res) + if (isComparison) setCompData(res); else setData(res); } }) @@ -169,12 +171,27 @@ function WidgetChart(props: Props) { const timestamps = dashboardStore.comparisonPeriod.toTimestamps(); // TODO: remove after backend adds support for more view types - const payload = { ...params, ...timestamps, ...metric.toJson(), viewType: 'lineChart' }; - fetchMetricChartData(metric, payload, isSaved, dashboardStore.comparisonPeriod, true); - } + const payload = { + ...params, + ...timestamps, + ...metric.toJson(), + viewType: 'lineChart', + }; + fetchMetricChartData( + metric, + payload, + isSaved, + dashboardStore.comparisonPeriod, + true + ); + }; useEffect(() => { + if (!dashboardStore.comparisonPeriod) { + setCompData(null); + return; + } loadComparisonData(); - }, [dashboardStore.comparisonPeriod]) + }, [dashboardStore.comparisonPeriod]); useEffect(() => { _metric.updateKey('page', 1); loadPage(); @@ -202,6 +219,7 @@ function WidgetChart(props: Props) { ); @@ -224,10 +242,13 @@ function WidgetChart(props: Props) { if (metricType === TIMESERIES) { const chartData = { ...data }; - chartData.namesMap = Array.isArray(chartData.namesMap) ? chartData.namesMap.map(n => enabledRows.includes(n) ? n : null) : chartData.namesMap + chartData.namesMap = Array.isArray(chartData.namesMap) + ? chartData.namesMap.map((n) => (enabledRows.includes(n) ? n : null)) + : chartData.namesMap; if (viewType === 'lineChart') { return ( ); @@ -306,13 +331,14 @@ function WidgetChart(props: Props) { if (viewType === 'progress') { return ( ); @@ -324,16 +350,17 @@ function WidgetChart(props: Props) { return ( - ) + ); } } @@ -428,13 +455,27 @@ function WidgetChart(props: Props) { return
Unknown metric type
; }; + return (
-
+
{renderChart()} - {metric.metricType === TIMESERIES ? ( - + {props.isPreview && metric.metricType === TIMESERIES ? ( + ) : null}
diff --git a/frontend/app/components/Dashboard/components/WidgetDateRange/WidgetDateRange.tsx b/frontend/app/components/Dashboard/components/WidgetDateRange/WidgetDateRange.tsx index ead4854f3..a3cc7023a 100644 --- a/frontend/app/components/Dashboard/components/WidgetDateRange/WidgetDateRange.tsx +++ b/frontend/app/components/Dashboard/components/WidgetDateRange/WidgetDateRange.tsx @@ -5,7 +5,13 @@ import { observer } from 'mobx-react-lite'; import { Space } from 'antd'; import RangeGranularity from "./RangeGranularity"; -function WidgetDateRange({ viewType = undefined, label = 'Time Range', isTimeseries = false }: any) { +function WidgetDateRange({ + viewType = undefined, + label = 'Time Range', + hasGranularSettings = false, + hasGranularity = false, + hasComparison = false, +}: any) { const { dashboardStore } = useStore(); const density = dashboardStore.selectedDensity const onDensityChange = (density: number) => { @@ -25,7 +31,9 @@ function WidgetDateRange({ viewType = undefined, label = 'Time Range', isTimeser }; const onChangeComparison = (period: any) => { + console.log(period) dashboardStore.setComparisonPeriod(period); + if (!period) return; const periodTimestamps = period.toTimestamps(); const compFilter = dashboardStore.cloneCompFilter(); compFilter.merge({ @@ -34,8 +42,6 @@ function WidgetDateRange({ viewType = undefined, label = 'Time Range', isTimeser }); } - const hasGranularity = ['lineChart', 'barChart', 'areaChart'].includes(viewType); - const hasCompare = ['lineChart', 'barChart', 'table', 'progressChart'].includes(viewType); return ( {label && {label}} @@ -45,7 +51,7 @@ function WidgetDateRange({ viewType = undefined, label = 'Time Range', isTimeser isAnt={true} useButtonStyle={true} /> - {isTimeseries ? ( + {hasGranularSettings ? ( <> {hasGranularity ? ( ) : null} - {hasCompare ? + {hasComparison ? {metric.metricType === USER_PATH && ( @@ -50,10 +54,7 @@ function WidgetOptions() { )} {metric.metricType === TIMESERIES ? ( - <> - - ) : null} {(metric.metricType === FUNNEL || metric.metricType === TABLE) && metric.metricOf != FilterKey.USERID && @@ -63,11 +64,12 @@ function WidgetOptions() { onChange={handleChange} variant="borderless" options={[ - { value: 'sessionCount', label: 'Sessions' }, - { value: 'userCount', label: 'Users' }, + { value: 'sessionCount', label: 'All Sessions' }, + { value: 'userCount', label: 'Unique Users' }, ]} /> )} + {hasViewTypes ? : null} {metric.metricType === HEATMAP ? : null}
@@ -121,6 +123,8 @@ const WidgetViewTypeOptions = observer(({ metric }: { metric: any }) => { progressChart: 'Bar', table: 'Table', metric: 'Metric', + chart: 'Funnel Bar', + columnChart: 'Funnel Column', }; const chartIcons = { lineChart: , @@ -130,16 +134,23 @@ const WidgetViewTypeOptions = observer(({ metric }: { metric: any }) => { progressChart: , table: , metric: , + // funnel specific + columnChart: , + chart: , }; + const allowedTypes = { + [TIMESERIES]: ['lineChart', 'barChart', 'areaChart', 'pieChart', 'progressChart', 'table', 'metric',], + [FUNNEL]: ['chart', 'columnChart', ] // + table + metric + } return ( ({ + items: allowedTypes[metric.metricType].map((key) => ({ key, label: (
{chartIcons[key]} -
{name}
+
{chartTypes[key]}
), })), diff --git a/frontend/app/components/Dashboard/components/WidgetPreview/WidgetPreview.tsx b/frontend/app/components/Dashboard/components/WidgetPreview/WidgetPreview.tsx index f559667ef..15018d305 100644 --- a/frontend/app/components/Dashboard/components/WidgetPreview/WidgetPreview.tsx +++ b/frontend/app/components/Dashboard/components/WidgetPreview/WidgetPreview.tsx @@ -3,7 +3,7 @@ import { observer } from 'mobx-react-lite'; import React from 'react'; import WidgetDateRange from "Components/Dashboard/components/WidgetDateRange/WidgetDateRange"; import { useStore } from 'App/mstore'; -import { TIMESERIES } from "App/constants/card"; +import { FUNNEL, TIMESERIES } from "App/constants/card"; import WidgetWrapper from '../WidgetWrapper'; import WidgetOptions from 'Components/Dashboard/components/WidgetOptions'; @@ -16,96 +16,26 @@ interface Props { function WidgetPreview(props: Props) { const { className = '' } = props; - const { metricStore, dashboardStore } = useStore(); + const { metricStore } = useStore(); const metric: any = metricStore.instance; - // compare logic + const hasGranularSettings = [TIMESERIES, FUNNEL].includes(metric.metricType) + const hasGranularity = ['lineChart', 'barChart', 'areaChart'].includes(metric.viewType); + const hasComparison = metric.metricType === FUNNEL || ['lineChart', 'barChart', 'table', 'progressChart'].includes(metric.viewType); return ( <>
-
- -
+
+ +
- {/*{metric.metricType === USER_PATH && (*/} - {/* {*/} - {/* e.preventDefault();*/} - {/* metric.update({ hideExcess: !metric.hideExcess });*/} - {/* }}*/} - {/* >*/} - {/* */} - {/* */} - {/* */} - {/* Hide Minor Paths*/} - {/* */} - {/* */} - {/* */} - {/*)}*/} - - {/*{isTimeSeries && (*/} - {/* <>*/} - {/* Visualization*/} - {/* */} - {/* */} - {/*)}*/} - - {/*{!disableVisualization && isTable && (*/} - {/* <>*/} - {/* Visualization*/} - {/* */} - {/* */} - {/*)}*/} - - {/*{isRetention && (*/} - {/* <>*/} - {/* Visualization*/} - {/* */} - {/**/} - {/*)}*/} - - {/* add to dashboard */} - {/*{metric.exists() && (*/} - {/* */} - {/*)}*/}
diff --git a/frontend/app/components/Dashboard/components/WidgetWrapper/WidgetWrapper.tsx b/frontend/app/components/Dashboard/components/WidgetWrapper/WidgetWrapper.tsx index aa0d848d1..ec88190a9 100644 --- a/frontend/app/components/Dashboard/components/WidgetWrapper/WidgetWrapper.tsx +++ b/frontend/app/components/Dashboard/components/WidgetWrapper/WidgetWrapper.tsx @@ -7,12 +7,12 @@ import { useStore } from 'App/mstore'; import { withRouter, RouteComponentProps } from 'react-router-dom'; import { withSiteId, dashboardMetricDetails } from 'App/routes'; import TemplateOverlay from './TemplateOverlay'; -import AlertButton from './AlertButton'; -import stl from './widgetWrapper.module.css'; import { FilterKey } from 'App/types/filter/filterType'; -import { TIMESERIES } from "App/constants/card"; +import { TIMESERIES } from 'App/constants/card'; -const WidgetChart = lazy(() => import('Components/Dashboard/components/WidgetChart')); +const WidgetChart = lazy( + () => import('Components/Dashboard/components/WidgetChart') +); interface Props { className?: string; @@ -74,14 +74,20 @@ function WidgetWrapper(props: Props & RouteComponentProps) { }); const onDelete = async () => { - dashboardStore.deleteDashboardWidget(dashboard?.dashboardId!, widget.widgetId); + dashboardStore.deleteDashboardWidget( + dashboard?.dashboardId!, + widget.widgetId + ); }; const onChartClick = () => { if (!isSaved || isPredefined) return; props.history.push( - withSiteId(dashboardMetricDetails(dashboard?.dashboardId, widget.metricId), siteId) + withSiteId( + dashboardMetricDetails(dashboard?.dashboardId, widget.metricId), + siteId + ) ); }; @@ -90,7 +96,7 @@ function WidgetWrapper(props: Props & RouteComponentProps) { const addOverlay = isTemplate || (!isPredefined && - isSaved && + isSaved && widget.metricOf !== FilterKey.ERRORS && widget.metricOf !== FilterKey.SESSIONS); @@ -106,73 +112,39 @@ function WidgetWrapper(props: Props & RouteComponentProps) { userSelect: 'none', opacity: isDragging ? 0.5 : 1, borderColor: - (canDrop && isOver) || active ? '#394EFF' : isPreview ? 'transparent' : '#EEEEEE', + (canDrop && isOver) || active + ? '#394EFF' + : isPreview + ? 'transparent' + : '#EEEEEE', }} ref={dragDropRef} onClick={props.onClick ? props.onClick : () => {}} id={`widget-${widget.widgetId}`} > - {!isTemplate && isSaved && isPredefined && ( -
- {'Cannot drill down system provided metrics'} -
+ {addOverlay && ( + )} - - {addOverlay && } -
- {!props.hideName ? ( + {!props.hideName ? ( +
- ) : null} - {isSaved && ( -
- {!isPredefined && isTimeSeries && !isGridView && ( - <> - -
- - )} - - {!isTemplate && !isGridView && ( - - )} -
- )} -
- -
-
+ ) : null} + +
+ +
); } diff --git a/frontend/app/components/Funnels/FunnelWidget/FunnelBar.tsx b/frontend/app/components/Funnels/FunnelWidget/FunnelBar.tsx index e03861290..813591b45 100644 --- a/frontend/app/components/Funnels/FunnelWidget/FunnelBar.tsx +++ b/frontend/app/components/Funnels/FunnelWidget/FunnelBar.tsx @@ -4,77 +4,162 @@ import FunnelStepText from './FunnelStepText'; import { Icon } from 'UI'; import { Space } from 'antd'; import { Styles } from 'Components/Dashboard/Widgets/common'; +import cn from 'classnames'; interface Props { filter: any; + compData?: any; index?: number; focusStage?: (index: number, isFocused: boolean) => void; focusedFilter?: number | null; metricLabel?: string; + isHorizontal?: boolean; } function FunnelBar(props: Props) { - const { filter, index, focusStage, focusedFilter, metricLabel = 'Sessions' } = props; + const { filter, index, focusStage, focusedFilter, compData, isHorizontal } = props; - const isFocused = focusedFilter && index ? focusedFilter === index - 1 : false; + const isFocused = + focusedFilter && index ? focusedFilter === index - 1 : false; return ( -
+
+
+ + {compData ? ( + + ) : null} +
+
+ ); +} + +function FunnelBarData({ + data, + isComp, + isFocused, + focusStage, + index, + isHorizontal, +}: { + data: any; + isComp?: boolean; + isFocused?: boolean; + focusStage?: (index: number, isFocused: boolean) => void; + index?: number; + isHorizontal?: boolean; +}) { + + const vertFillBarStyle = { + width: `${data.completedPercentageTotal}%`, + position: 'absolute', + top: 0, + left: 0, + bottom: 0, + backgroundColor: isComp ? Styles.compareColors[2] : Styles.compareColors[1] + }; + const horizontalFillBarStyle = { + width: '100%', + height: `${data.completedPercentageTotal}%`, + position: 'absolute', + bottom: 0, + left: 0, + right: 0, + backgroundColor: isComp ? Styles.compareColors[2] : Styles.compareColors[1] + } + + const vertEmptyBarStyle = { + width: `${100.1 - data.completedPercentageTotal}%`, + position: 'absolute', + top: 0, + right: 0, + bottom: 0, + background: isFocused + ? 'rgba(204, 0, 0, 0.3)' + : 'repeating-linear-gradient(325deg, lightgray, lightgray 2px, #FFF1F0 2px, #FFF1F0 6px)', + cursor: 'pointer', + } + const horizontalEmptyBarStyle = { + height: `${100.1 - data.completedPercentageTotal}%`, + width: '100%', + position: 'absolute', + top: 0, + right: 0, + left: 0, + background: isFocused + ? 'rgba(204, 0, 0, 0.3)' + : 'repeating-linear-gradient(325deg, lightgray, lightgray 2px, #FFF1F0 2px, #FFF1F0 6px)', + cursor: 'pointer', + } + + const fillBarStyle = isHorizontal ? horizontalFillBarStyle : vertFillBarStyle; + const emptyBarStyle = isHorizontal ? horizontalEmptyBarStyle : vertEmptyBarStyle + return ( +
- {filter.completedPercentageTotal}% + {data.completedPercentageTotal}%
focusStage?.(index! - 1, filter.isActive)} - className={'hover:opacity-75'} + style={emptyBarStyle} + onClick={() => focusStage?.(index! - 1, data.isActive)} + className={'hover:opacity-70'} />
-
+
{/* @ts-ignore */}
- {filter.count} {metricLabel} - ({filter.completedPercentage}%) Completed + {`${data.completedPercentage}% . ${data.count}`}
{index && index > 1 && ( - 0 ? 'red' : 'gray-light'} size={16} /> + 0 ? 'red' : 'gray-light'} + size={16} + /> 0 ? 'color-red' : 'disabled')}>{filter.droppedCount} {metricLabel} - 0 ? 'color-red' : 'disabled')}>({filter.droppedPercentage}%) Dropped + className={ + 'mx-1 ' + (data.droppedCount > 0 ? 'color-red' : 'disabled') + } + > + {data.droppedCount} Skipped + )}
@@ -86,7 +171,7 @@ export function UxTFunnelBar(props: Props) { const { filter } = props; return ( -
+
{filter.title}
- {((filter.completed / (filter.completed + filter.skipped)) * 100).toFixed(1)}% + {( + (filter.completed / (filter.completed + filter.skipped)) * + 100 + ).toFixed(1)} + %
@@ -119,22 +210,22 @@ export function UxTFunnelBar(props: Props) {
- {filter.completed}completed this step + {filter.completed} + completed this step
{durationFormatted(filter.avgCompletionTime)} - - avg. completion time - + avg. completion time
{/* @ts-ignore */}
- {filter.skipped} skipped + {filter.skipped} + skipped
@@ -142,11 +233,3 @@ export function UxTFunnelBar(props: Props) { } export default FunnelBar; - -const calculatePercentage = (completed: number, dropped: number) => { - const total = completed + dropped; - if (dropped === 0) return 100; - if (total === 0) return 0; - - return Math.round((completed / dropped) * 100); -}; diff --git a/frontend/app/components/Funnels/FunnelWidget/FunnelStepText.tsx b/frontend/app/components/Funnels/FunnelWidget/FunnelStepText.tsx index f0c9ef84c..8ea670eab 100644 --- a/frontend/app/components/Funnels/FunnelWidget/FunnelStepText.tsx +++ b/frontend/app/components/Funnels/FunnelWidget/FunnelStepText.tsx @@ -7,7 +7,7 @@ function FunnelStepText(props: Props) { const { filter } = props; const total = filter.value.length; return ( -
+
{filter.label} {filter.operator} {filter.value.map((value: any, index: number) => ( diff --git a/frontend/app/components/Funnels/FunnelWidget/FunnelWidget.module.css b/frontend/app/components/Funnels/FunnelWidget/FunnelWidget.module.css index 0102e4d9f..48b1791ec 100644 --- a/frontend/app/components/Funnels/FunnelWidget/FunnelWidget.module.css +++ b/frontend/app/components/Funnels/FunnelWidget/FunnelWidget.module.css @@ -1,23 +1,3 @@ -.step { - /* display: flex; */ - position: relative; - transition: all 0.5s ease; - &:before { - content: ''; - border-left: 2px solid $gray-lightest; - position: absolute; - top: 16px; - bottom: 9px; - left: 10px; - /* width: 1px; */ - height: 100%; - z-index: 0; - } - &:last-child:before { - display: none; - } -} - .step-disabled { filter: grayscale(1); opacity: 0.8; diff --git a/frontend/app/components/Funnels/FunnelWidget/FunnelWidget.tsx b/frontend/app/components/Funnels/FunnelWidget/FunnelWidget.tsx index ffe3a2efc..a44335318 100644 --- a/frontend/app/components/Funnels/FunnelWidget/FunnelWidget.tsx +++ b/frontend/app/components/Funnels/FunnelWidget/FunnelWidget.tsx @@ -1,6 +1,7 @@ import React, { useEffect } from 'react'; import Widget from 'App/mstore/types/widget'; import Funnelbar, { UxTFunnelBar } from "./FunnelBar"; +import Funnel from 'App/mstore/types/funnel' import cn from 'classnames'; import stl from './FunnelWidget.module.css'; import { observer } from 'mobx-react-lite'; @@ -11,134 +12,186 @@ import { useModal } from 'App/components/Modal'; interface Props { metric?: Widget; isWidget?: boolean; - data: any; + data: { funnel: Funnel }; + compData: { funnel: Funnel }; } + function FunnelWidget(props: Props) { - const [focusedFilter, setFocusedFilter] = React.useState(null); - const { isWidget = false, data, metric } = props; - const funnel = data.funnel || { stages: [] }; - const totalSteps = funnel.stages.length; - const stages = isWidget ? [...funnel.stages.slice(0, 1), funnel.stages[funnel.stages.length - 1]] : funnel.stages; - const hasMoreSteps = funnel.stages.length > 2; - const lastStage = funnel.stages[funnel.stages.length - 1]; - const remainingSteps = totalSteps - 2; - const { hideModal } = useModal(); - const metricLabel = metric?.metricFormat == 'userCount' ? 'Users' : 'Sessions'; + const [focusedFilter, setFocusedFilter] = React.useState(null); + const { isWidget = false, data, metric, compData } = props; + const funnel = data.funnel || { stages: [] }; + const totalSteps = funnel.stages.length; + const stages = isWidget + ? [...funnel.stages.slice(0, 1), funnel.stages[funnel.stages.length - 1]] + : funnel.stages; + const hasMoreSteps = funnel.stages.length > 2; + const lastStage = funnel.stages[funnel.stages.length - 1]; + const remainingSteps = totalSteps - 2; + const { hideModal } = useModal(); + const metricLabel = + metric?.metricFormat == 'userCount' ? 'Users' : 'Sessions'; - useEffect(() => { - return () => { - if (isWidget) return; - hideModal(); + useEffect(() => { + return () => { + if (isWidget) return; + hideModal(); + }; + }, []); + + const focusStage = (index: number) => { + funnel.stages.forEach((s, i) => { + // turning on all filters if one was focused already + if (focusedFilter === index) { + s.updateKey('isActive', true); + setFocusedFilter(null); + } else { + setFocusedFilter(index); + if (i === index) { + s.updateKey('isActive', true); + } else { + s.updateKey('isActive', false); } - }, []); + } + }); + }; - const focusStage = (index: number) => { - funnel.stages.forEach((s, i) => { - // turning on all filters if one was focused already - if (focusedFilter === index) { - s.updateKey('isActive', true) - setFocusedFilter(null) - } else { - setFocusedFilter(index) - if (i === index) { - s.updateKey('isActive', true) - } else { - s.updateKey('isActive', false) - } - } - }) + const shownStages = React.useMemo(() => { + const stages: { data: Funnel['stages'][0], compData?: Funnel['stages'][0] }[] = []; + for (let i = 0; i < funnel.stages.length; i++) { + const stage: any = { data: funnel.stages[i], compData: undefined } + const compStage = compData?.funnel.stages[i]; + if (compStage) { + stage.compData = compStage; + } + stages.push(stage) } - return ( - - - No data available for the selected period. -
- } - show={!stages || stages.length === 0} - > -
- { !isWidget && ( - stages.map((filter: any, index: any) => ( - - )) - )} + return stages; + }, [data, compData]) - { isWidget && ( - <> - + const viewType = metric?.viewType; + const isHorizontal = viewType === 'columnChart'; + return ( + + + No data available for the selected period. +
+ } + show={!stages || stages.length === 0} + > +
+ {!isWidget && + shownStages.map((stage: any, index: any) => ( + + ))} - { hasMoreSteps && ( - <> - - - )} + {isWidget && ( + <> + - {funnel.stages.length > 1 && ( - - )} - - )} -
-
-
- Lost conversion - - - {funnel.lostConversions} - - -
-
-
- Total conversion - - - {funnel.totalConversions} - - -
-
- {funnel.totalDropDueToIssues > 0 &&
{funnel.totalDropDueToIssues} sessions dropped due to issues.
} - - ); + {hasMoreSteps && ( + <> + + + )} + + {funnel.stages.length > 1 && ( + + )} + + )} +
+
+
+ Total conversion + + + {funnel.totalConversions} + + +
+
+ Lost conversion + + + {funnel.lostConversions} + + +
+
+ {funnel.totalDropDueToIssues > 0 && ( +
+ {' '} + + {funnel.totalDropDueToIssues} sessions dropped due to issues. + +
+ )} + + ); } export const EmptyStage = observer(({ total }: any) => { - return ( -
- -
- {`+${total} ${total > 1 ? 'steps' : 'step'}`} -
-
-
- ) -}) + return ( +
+ +
+ {`+${total} ${total > 1 ? 'steps' : 'step'}`} +
+
+
+ ); +}); -export const Stage = observer(({ metricLabel, stage, index, isWidget, uxt, focusStage, focusedFilter }: any) => { +export const Stage = observer(({ + metricLabel, + stage, + index, + uxt, + focusStage, + focusedFilter, + compData, + isHorizontal, +}: any) => { return stage ? (
- {!uxt ? : } - {/*{!isWidget && !uxt && }*/} + {!uxt ? : }
- ) : ( - <> - ) + ) : null }) export const IndexNumber = observer(({ index }: any) => { @@ -149,15 +202,4 @@ export const IndexNumber = observer(({ index }: any) => { ); }) - -const BarActions = observer(({ bar }: any) => { - return ( -
- -
- ) -}) - export default observer(FunnelWidget); diff --git a/frontend/app/components/shared/SelectDateRange/SelectDateRange.tsx b/frontend/app/components/shared/SelectDateRange/SelectDateRange.tsx index 7a6d7a25f..a2b999be4 100644 --- a/frontend/app/components/shared/SelectDateRange/SelectDateRange.tsx +++ b/frontend/app/components/shared/SelectDateRange/SelectDateRange.tsx @@ -52,7 +52,8 @@ function SelectDateRange(props: Props) { ); const onChange = (value: any) => { - if (props.comparison) { + if (props.comparison && props.onChangeComparison) { + if (!value) return props.onChangeComparison(null); const newPeriod = new Period({ start: props.period.start, end: props.period.end, diff --git a/frontend/app/mstore/types/funnel.ts b/frontend/app/mstore/types/funnel.ts index 198c0c166..f99d2fdd3 100644 --- a/frontend/app/mstore/types/funnel.ts +++ b/frontend/app/mstore/types/funnel.ts @@ -21,7 +21,7 @@ export default class Funnel { } this.totalDropDueToIssues = json.totalDropDueToIssues; - if (json.stages.length >= 1) { + if (json.stages?.length >= 1) { const firstStage = json.stages[0] this.stages = json.stages ? json.stages.map((stage: any, index: number) => new FunnelStage().fromJSON(stage, firstStage.count, index > 0 ? json.stages[index - 1].count : stage.count)) : [] const filteredStages = this.stages.filter((stage: any) => stage.isActive) diff --git a/frontend/app/mstore/types/widget.ts b/frontend/app/mstore/types/widget.ts index 78e29acb2..9a24b5399 100644 --- a/frontend/app/mstore/types/widget.ts +++ b/frontend/app/mstore/types/widget.ts @@ -8,7 +8,7 @@ import {FilterKey} from 'Types/filter/filterType'; import Period, {LAST_24_HOURS} from 'Types/app/period'; import Funnel from '../types/funnel'; import {metricService} from 'App/services'; -import { FUNNEL, HEATMAP, INSIGHTS, TABLE, USER_PATH } from "App/constants/card"; +import { FUNNEL, HEATMAP, INSIGHTS, TABLE, TIMESERIES, USER_PATH } from "App/constants/card"; import { ErrorInfo } from '../types/error'; import {getChartFormatter} from 'Types/dashboard/helper'; import FilterItem from './filterItem'; @@ -295,7 +295,7 @@ export default class Widget { setData(data: { timestamp: number, [seriesName: string]: number}[], period: any, isComparison?: boolean) { const _data: any = {}; - if (isComparison) { + if (isComparison && this.metricType === TIMESERIES) { data.forEach((point, i) => { Object.keys(point).forEach((key) => { if (key === 'timestamp') return; @@ -321,10 +321,9 @@ export default class Widget { new InsightIssue(i.category, i.name, i.ratio, i.oldValue, i.value, i.change, i.isNew) ); } else if (this.metricType === FUNNEL) { - _data.funnel = new Funnel().fromJSON(_data); + _data.funnel = new Funnel().fromJSON(data); } else if (this.metricType === TABLE) { const total = data[0]['total']; - const count = data[0]['count']; _data[0]['values'] = data[0]['values'].map((s: any) => new SessionsByRow().fromJson(s, total, this.metricOf)); } else { if (data.hasOwnProperty('chart')) {