change(ui): path analysis clear filter (#2207)

This commit is contained in:
Shekar Siri 2024-05-27 15:03:30 +02:00 committed by GitHub
parent 45655a9608
commit 1f9019a103
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 930 additions and 827 deletions

View file

@ -21,6 +21,11 @@ function CardIssues() {
const isMounted = useIsMounted();
const pageSize = 5;
const { showModal } = useModal();
const filter = useObserver(() => dashboardStore.drillDownFilter);
const hasFilters = filter.filters.length > 0 || (filter.startTimestamp !== dashboardStore.drillDownPeriod.start || filter.endTimestamp !== dashboardStore.drillDownPeriod.end);
const drillDownPeriod = useObserver(() => dashboardStore.drillDownPeriod);
const depsString = JSON.stringify(widget.series);
function getFilters(filter: any) {
const mapSeries = (item: any) => {
@ -61,16 +66,13 @@ function CardIssues() {
}
};
const debounceRequest: any = React.useCallback(debounce(fetchIssues, 1000), []);
const handleClick = (issue?: any) => {
// const filters = getFilters(widget.filter);
showModal(<SessionsModal issue={issue} />, { right: true, width: 900 });
};
const filter = useObserver(() => dashboardStore.drillDownFilter);
const drillDownPeriod = useObserver(() => dashboardStore.drillDownPeriod);
const debounceRequest: any = React.useCallback(debounce(fetchIssues, 1000), []);
const depsString = JSON.stringify(widget.series);
useEffect(() => {
const newPayload = {
...widget,
@ -81,6 +83,11 @@ function CardIssues() {
debounceRequest(newPayload);
}, [drillDownPeriod, filter.filters, depsString, metricStore.sessionsPage, filter.page]);
const clearFilters = () => {
metricStore.updateKey('page', 1);
dashboardStore.resetDrillDownFilter();
};
return useObserver(() => (
<div className='my-8 bg-white rounded p-4 border'>
<div className='flex justify-between'>
@ -94,7 +101,8 @@ function CardIssues() {
</div>
)}
</div>
<div>
<div className='flex items-center gap-4'>
{hasFilters && <Button variant='text-primary' onClick={clearFilters}>Clear Filters</Button>}
<Button variant='text-primary' onClick={() => handleClick()}>All Sessions</Button>
</div>
</div>

View file

@ -1,33 +1,32 @@
import React, { useState, useRef, useEffect } from 'react';
import React, {useState, useRef, useEffect} from 'react';
import CustomMetriLineChart from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetriLineChart';
import CustomMetricPercentage from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricPercentage';
import CustomMetricTable from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricTable';
import CustomMetricPieChart from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricPieChart';
import { Styles } from 'App/components/Dashboard/Widgets/common';
import { observer } from 'mobx-react-lite';
import { Loader } from 'UI';
import { useStore } from 'App/mstore';
import {Styles} from 'App/components/Dashboard/Widgets/common';
import {observer} from 'mobx-react-lite';
import {Loader} from 'UI';
import {useStore} from 'App/mstore';
import WidgetPredefinedChart from '../WidgetPredefinedChart';
import CustomMetricOverviewChart from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricOverviewChart';
import { getStartAndEndTimestampsByDensity } from 'Types/dashboard/helper';
import { debounce } from 'App/utils';
import {getStartAndEndTimestampsByDensity} from 'Types/dashboard/helper';
import {debounce} from 'App/utils';
import useIsMounted from 'App/hooks/useIsMounted';
import { FilterKey } from 'Types/filter/filterType';
import {FilterKey} from 'Types/filter/filterType';
import {
TIMESERIES,
TABLE,
CLICKMAP,
FUNNEL,
ERRORS,
PERFORMANCE,
RESOURCE_MONITORING,
WEB_VITALS,
INSIGHTS,
USER_PATH,
RETENTION
TIMESERIES,
TABLE,
CLICKMAP,
FUNNEL,
ERRORS,
PERFORMANCE,
RESOURCE_MONITORING,
WEB_VITALS,
INSIGHTS,
USER_PATH,
RETENTION
} from 'App/constants/card';
import FunnelWidget from 'App/components/Funnels/FunnelWidget';
import SessionWidget from '../Sessions/SessionWidget';
import CustomMetricTableSessions from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricTableSessions';
import CustomMetricTableErrors from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricTableErrors';
import ClickMapCard from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/ClickMapCard';
@ -36,220 +35,221 @@ import SankeyChart from 'Shared/Insights/SankeyChart';
import CohortCard from '../../Widgets/CustomMetricsWidgets/CohortCard';
interface Props {
metric: any;
isWidget?: boolean;
isTemplate?: boolean;
isPreview?: boolean;
metric: any;
isWidget?: boolean;
isTemplate?: boolean;
isPreview?: boolean;
}
function WidgetChart(props: Props) {
const { isWidget = false, metric, isTemplate } = props;
const { dashboardStore, metricStore, sessionStore } = useStore();
const _metric: any = metricStore.instance;
const period = dashboardStore.period;
const drillDownPeriod = dashboardStore.drillDownPeriod;
const drillDownFilter = dashboardStore.drillDownFilter;
const colors = Styles.customMetricColors;
const [loading, setLoading] = useState(true);
const isOverviewWidget = metric.metricType === WEB_VITALS;
const params = { density: isOverviewWidget ? 7 : 70 };
const metricParams = { ...params };
const prevMetricRef = useRef<any>();
const isMounted = useIsMounted();
const [data, setData] = useState<any>(metric.data);
const {isWidget = false, metric, isTemplate} = props;
const {dashboardStore, metricStore, sessionStore} = useStore();
const _metric: any = metricStore.instance;
const period = dashboardStore.period;
const drillDownPeriod = dashboardStore.drillDownPeriod;
const drillDownFilter = dashboardStore.drillDownFilter;
const colors = Styles.customMetricColors;
const [loading, setLoading] = useState(true);
const isOverviewWidget = metric.metricType === WEB_VITALS;
const params = {density: isOverviewWidget ? 7 : 70};
const metricParams = {...params};
const prevMetricRef = useRef<any>();
const isMounted = useIsMounted();
const [data, setData] = useState<any>(metric.data);
const isTableWidget = metric.metricType === 'table' && metric.viewType === 'table';
const isPieChart = metric.metricType === 'table' && metric.viewType === 'pieChart';
const isTableWidget = metric.metricType === 'table' && metric.viewType === 'table';
const isPieChart = metric.metricType === 'table' && metric.viewType === 'pieChart';
useEffect(() => {
return () => {
dashboardStore.resetDrillDownFilter();
useEffect(() => {
return () => {
dashboardStore.resetDrillDownFilter();
};
}, []);
const onChartClick = (event: any) => {
if (event) {
if (isTableWidget || isPieChart) { // get the filter of clicked row
const periodTimestamps = drillDownPeriod.toTimestamps();
drillDownFilter.merge({
filters: event,
startTimestamp: periodTimestamps.startTimestamp,
endTimestamp: periodTimestamps.endTimestamp
});
} else { // get the filter of clicked chart point
const payload = event.activePayload[0].payload;
const timestamp = payload.timestamp;
const periodTimestamps = getStartAndEndTimestampsByDensity(timestamp, drillDownPeriod.start, drillDownPeriod.end, params.density);
drillDownFilter.merge({
startTimestamp: periodTimestamps.startTimestamp,
endTimestamp: periodTimestamps.endTimestamp
});
}
}
};
}, []);
const onChartClick = (event: any) => {
if (event) {
if (isTableWidget || isPieChart) { // get the filter of clicked row
const periodTimestamps = drillDownPeriod.toTimestamps();
drillDownFilter.merge({
filters: event,
startTimestamp: periodTimestamps.startTimestamp,
endTimestamp: periodTimestamps.endTimestamp
});
} else { // get the filter of clicked chart point
const payload = event.activePayload[0].payload;
const timestamp = payload.timestamp;
const periodTimestamps = getStartAndEndTimestampsByDensity(timestamp, drillDownPeriod.start, drillDownPeriod.end, params.density);
drillDownFilter.merge({
startTimestamp: periodTimestamps.startTimestamp,
endTimestamp: periodTimestamps.endTimestamp
});
}
}
};
const depsString = JSON.stringify({
..._metric.series, ..._metric.excludes, ..._metric.startPoint,
hideExcess: _metric.hideExcess
});
const fetchMetricChartData = (metric: any, payload: any, isWidget: any, period: any) => {
if (!isMounted()) return;
setLoading(true);
dashboardStore.fetchMetricChartData(metric, payload, isWidget, period).then((res: any) => {
if (isMounted()) setData(res);
}).finally(() => {
setLoading(false);
const depsString = JSON.stringify({
..._metric.series, ..._metric.excludes, ..._metric.startPoint,
hideExcess: _metric.hideExcess
});
};
const fetchMetricChartData = (metric: any, payload: any, isWidget: any, period: any) => {
if (!isMounted()) return;
setLoading(true);
dashboardStore.fetchMetricChartData(metric, payload, isWidget, period).then((res: any) => {
if (isMounted()) setData(res);
}).finally(() => {
setLoading(false);
});
};
const debounceRequest: any = React.useCallback(debounce(fetchMetricChartData, 500), []);
const loadPage = () => {
if (prevMetricRef.current && prevMetricRef.current.name !== metric.name) {
prevMetricRef.current = metric;
return;
}
prevMetricRef.current = metric;
const timestmaps = drillDownPeriod.toTimestamps();
const payload = isWidget ? { ...params } : { ...metricParams, ...timestmaps, ...metric.toJson() };
debounceRequest(metric, payload, isWidget, !isWidget ? drillDownPeriod : period);
};
useEffect(() => {
_metric.updateKey('page', 1);
loadPage();
}, [drillDownPeriod, period, depsString, metric.metricType, metric.metricOf, metric.viewType, metric.metricValue, metric.startType]);
useEffect(loadPage, [_metric.page]);
const debounceRequest: any = React.useCallback(debounce(fetchMetricChartData, 500), []);
const loadPage = () => {
if (prevMetricRef.current && prevMetricRef.current.name !== metric.name) {
prevMetricRef.current = metric;
return;
}
prevMetricRef.current = metric;
const timestmaps = drillDownPeriod.toTimestamps();
const payload = isWidget ? {...params} : {...metricParams, ...timestmaps, ...metric.toJson()};
debounceRequest(metric, payload, isWidget, !isWidget ? drillDownPeriod : period);
};
useEffect(() => {
_metric.updateKey('page', 1);
loadPage();
}, [drillDownPeriod, period, depsString, metric.metricType, metric.metricOf, metric.viewType, metric.metricValue, metric.startType]);
useEffect(loadPage, [_metric.page]);
const renderChart = () => {
const { metricType, viewType, metricOf } = metric;
const metricWithData = { ...metric, data };
const renderChart = () => {
const {metricType, viewType, metricOf} = metric;
const metricWithData = {...metric, data};
if (metricType === FUNNEL) {
return <FunnelWidget metric={metric} data={data} isWidget={isWidget || isTemplate} />;
}
if (metricType === FUNNEL) {
return <FunnelWidget metric={metric} data={data} isWidget={isWidget || isTemplate}/>;
}
if (metricType === 'predefined' || metricType === ERRORS || metricType === PERFORMANCE || metricType === RESOURCE_MONITORING || metricType === WEB_VITALS) {
const defaultMetric = metric.data.chart && metric.data.chart.length === 0 ? metricWithData : metric;
if (isOverviewWidget) {
return <CustomMetricOverviewChart data={data} />;
}
return <WidgetPredefinedChart isTemplate={isTemplate} metric={defaultMetric} data={data}
predefinedKey={metric.metricOf} />;
}
if (metricType === 'predefined' || metricType === ERRORS || metricType === PERFORMANCE || metricType === RESOURCE_MONITORING || metricType === WEB_VITALS) {
const defaultMetric = metric.data.chart && metric.data.chart.length === 0 ? metricWithData : metric;
if (isOverviewWidget) {
return <CustomMetricOverviewChart data={data}/>;
}
return <WidgetPredefinedChart isTemplate={isTemplate} metric={defaultMetric} data={data}
predefinedKey={metric.metricOf}/>;
}
if (metricType === TIMESERIES) {
if (viewType === 'lineChart') {
return (
<CustomMetriLineChart
data={data}
colors={colors}
params={params}
onClick={onChartClick}
/>
);
} else if (viewType === 'progress') {
return (
<CustomMetricPercentage
data={data[0]}
colors={colors}
params={params}
/>
);
}
}
if (metricType === TIMESERIES) {
if (viewType === 'lineChart') {
return (
<CustomMetriLineChart
data={data}
colors={colors}
params={params}
onClick={onChartClick}
/>
);
} else if (viewType === 'progress') {
return (
<CustomMetricPercentage
data={data[0]}
colors={colors}
params={params}
/>
);
}
}
if (metricType === TABLE) {
if (metricOf === FilterKey.SESSIONS) {
return (
<CustomMetricTableSessions
metric={metric}
data={data}
isTemplate={isTemplate}
isEdit={!isWidget && !isTemplate}
/>
);
}
if (metricOf === FilterKey.ERRORS) {
return (
<CustomMetricTableErrors
metric={metric}
data={data}
// isTemplate={isTemplate}
isEdit={!isWidget && !isTemplate}
/>
);
}
if (viewType === TABLE) {
return (
<CustomMetricTable
metric={metric} data={data[0]}
onClick={onChartClick}
isTemplate={isTemplate}
/>
);
} else if (viewType === 'pieChart') {
return (
<CustomMetricPieChart
metric={metric}
data={data[0]}
colors={colors}
// params={params}
onClick={onChartClick}
/>
);
}
}
if (metricType === CLICKMAP) {
if (!props.isPreview) {
return (
<div style={{ height: '229px', overflow: 'hidden', marginBottom: '10px' }}>
<img src={metric.thumbnail} alt='clickmap thumbnail' />
</div>
);
}
return (
<ClickMapCard />
);
}
if (metricType === TABLE) {
if (metricOf === FilterKey.SESSIONS) {
return (
<CustomMetricTableSessions
metric={metric}
data={data}
isTemplate={isTemplate}
isEdit={!isWidget && !isTemplate}
/>
);
}
if (metricOf === FilterKey.ERRORS) {
return (
<CustomMetricTableErrors
metric={metric}
data={data}
// isTemplate={isTemplate}
isEdit={!isWidget && !isTemplate}
/>
);
}
if (viewType === TABLE) {
return (
<CustomMetricTable
metric={metric} data={data[0]}
onClick={onChartClick}
isTemplate={isTemplate}
/>
);
} else if (viewType === 'pieChart') {
return (
<CustomMetricPieChart
metric={metric}
data={data[0]}
colors={colors}
// params={params}
onClick={onChartClick}
/>
);
}
}
if (metricType === CLICKMAP) {
if (!props.isPreview) {
return (
<div style={{height: '229px', overflow: 'hidden', marginBottom: '10px'}}>
<img src={metric.thumbnail} alt='clickmap thumbnail'/>
</div>
);
}
return (
<ClickMapCard/>
);
}
if (metricType === INSIGHTS) {
return <InsightsCard data={data} />;
}
if (metricType === INSIGHTS) {
return <InsightsCard data={data}/>;
}
if (metricType === USER_PATH && data && data.links) {
return <SankeyChart
height={props.isPreview ? 500 : 240}
data={data}
onChartClick={(filters: any) => {
dashboardStore.drillDownFilter.merge({ filters, page: 1 });
}} />;
}
if (metricType === USER_PATH && data && data.links) {
// return <PathAnalysis data={data}/>;
return <SankeyChart
height={props.isPreview ? 500 : 240}
data={data}
onChartClick={(filters: any) => {
dashboardStore.drillDownFilter.merge({filters, page: 1});
}}/>;
}
if (metricType === RETENTION) {
if (viewType === 'trend') {
return (
<CustomMetriLineChart
data={data}
colors={colors}
params={params}
onClick={onChartClick}
/>
);
} else if (viewType === 'cohort') {
return (
<CohortCard data={data[0]} />
);
}
}
if (metricType === RETENTION) {
if (viewType === 'trend') {
return (
<CustomMetriLineChart
data={data}
colors={colors}
params={params}
onClick={onChartClick}
/>
);
} else if (viewType === 'cohort') {
return (
<CohortCard data={data[0]}/>
);
}
}
return <div>Unknown metric type</div>;
};
return (
<Loader loading={loading} style={{ height: `${isOverviewWidget ? 100 : 240}px` }}>
<div style={{ minHeight: isOverviewWidget ? 100 : 240 }}>{renderChart()}</div>
</Loader>
);
return <div>Unknown metric type</div>;
};
return (
<Loader loading={loading} style={{height: `${isOverviewWidget ? 100 : 240}px`}}>
<div style={{minHeight: isOverviewWidget ? 100 : 240}}>{renderChart()}</div>
</Loader>
);
}
export default observer(WidgetChart);

View file

@ -1,7 +1,26 @@
import React from 'react';
import { Layer, Rectangle } from 'recharts';
import { Layer } from 'recharts';
function CustomLink(props: any) {
interface CustomLinkProps {
hoveredLinks: string[];
activeLinks: string[];
payload: any;
sourceX: number;
targetX: number;
sourceY: number;
targetY: number;
sourceControlX: number;
targetControlX: number;
linkWidth: number;
index: number;
activeLink: any;
onClick?: (payload: any) => void;
onMouseEnter?: () => void;
onMouseLeave?: () => void;
strokeOpacity?: number;
}
const CustomLink: React.FC<CustomLinkProps> = (props) => {
const [fill, setFill] = React.useState('url(#linkGradient)');
const {
hoveredLinks,
@ -15,9 +34,7 @@ function CustomLink(props: any) {
targetControlX,
linkWidth,
index,
activeLink
} =
props;
} = props;
const isActive = activeLinks.length > 0 && activeLinks.includes(payload.id);
const isHover = hoveredLinks.length > 0 && hoveredLinks.includes(payload.id);
@ -28,36 +45,36 @@ function CustomLink(props: any) {
};
return (
<Layer
key={`CustomLink${index}`}
onClick={onClick}
onMouseEnter={props.onMouseEnter}
onMouseLeave={props.onMouseLeave}
>
<path
d={`
M${sourceX},${sourceY + linkWidth / 2}
C${sourceControlX},${sourceY + linkWidth / 2}
${targetControlX},${targetY + linkWidth / 2}
${targetX},${targetY + linkWidth / 2}
L${targetX},${targetY - linkWidth / 2}
C${targetControlX},${targetY - linkWidth / 2}
${sourceControlX},${sourceY - linkWidth / 2}
${sourceX},${sourceY - linkWidth / 2}
Z
`}
fill={isActive ? 'rgba(57, 78, 255, 1)' : (isHover ? 'rgba(57, 78, 255, 0.5)' : fill)}
strokeWidth='1'
strokeOpacity={props.strokeOpacity}
onMouseEnter={() => {
setFill('rgba(57, 78, 255, 0.5)');
}}
onMouseLeave={() => {
setFill('url(#linkGradient)');
}}
/>
</Layer>
<Layer
key={`CustomLink${index}`}
onClick={onClick}
onMouseEnter={props.onMouseEnter}
onMouseLeave={props.onMouseLeave}
>
<path
d={`
M${sourceX},${sourceY + linkWidth / 2}
C${sourceControlX},${sourceY + linkWidth / 2}
${targetControlX},${targetY + linkWidth / 2}
${targetX},${targetY + linkWidth / 2}
L${targetX},${targetY - linkWidth / 2}
C${targetControlX},${targetY - linkWidth / 2}
${sourceControlX},${sourceY - linkWidth / 2}
${sourceX},${sourceY - linkWidth / 2}
Z
`}
fill={isActive ? 'rgba(57, 78, 255, 1)' : (isHover ? 'rgba(57, 78, 255, 0.5)' : fill)}
strokeWidth='1'
strokeOpacity={props.strokeOpacity}
onMouseEnter={() => {
setFill('rgba(57, 78, 255, 0.5)');
}}
onMouseLeave={() => {
setFill('url(#linkGradient)');
}}
/>
</Layer>
);
}
};
export default CustomLink;

View file

@ -1,35 +1,35 @@
import React from 'react';
import { Layer, Rectangle } from 'recharts';
import NodeButton from './NodeButton';
import NodeDropdown from './NodeDropdown';
function CustomNode(props: any) {
const { x, y, width, height, index, payload, containerWidth, activeNodes } = props;
const isOut = x + width + 6 > containerWidth;
return (
<Layer key={`CustomNode${index}`} style={{ cursor: 'pointer' }}>
<Rectangle x={x} y={y} width={width} height={height} fill='#394EFF' fillOpacity='1' />
{/*<foreignObject*/}
{/* x={isOut ? x - 6 : x + width + 5}*/}
{/* y={0}*/}
{/* height={48}*/}
{/* style={{ width: '150px', padding: '2px' }}*/}
{/*>*/}
{/* <NodeDropdown payload={payload} />*/}
{/*</foreignObject>*/}
<foreignObject
x={isOut ? x - 6 : x + width + 5}
y={y + 5}
height='25'
style={{ width: '150px', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}
>
<NodeButton payload={payload} />
</foreignObject>
</Layer>
);
interface CustomNodeProps {
x: number;
y: number;
width: number;
height: number;
index: number;
payload: any;
containerWidth: number;
activeNodes: any[];
}
export default CustomNode;
const CustomNode: React.FC<CustomNodeProps> = (props) => {
const { x, y, width, height, index, payload, containerWidth } = props;
const isOut = x + width + 6 > containerWidth;
return (
<Layer key={`CustomNode${index}`} style={{ cursor: 'pointer' }}>
<Rectangle x={x} y={y} width={width} height={height} fill='#394EFF' fillOpacity='1' />
<foreignObject
x={isOut ? x - 6 : x + width + 5}
y={y + 5}
height='25'
style={{ width: '150px', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}
>
<NodeButton payload={payload} />
</foreignObject>
</Layer>
);
};
export default CustomNode;

View file

@ -1,63 +1,67 @@
import React from 'react';
import { Icon } from 'UI';
import { Popover } from 'antd';
import {Icon} from 'UI';
import {Popover} from 'antd';
interface Props {
payload: any;
payload: any;
}
function NodeButton(props: Props) {
const { payload } = props;
const {payload} = props;
return (
<div className='relative'>
<Popover
content={<div className='bg-white rounded mt-1 text-xs'>
<div className='border-b py-1 px-2 flex items-center'>
<div className='w-6 shrink-0'>
<Icon name='link-45deg' size={18} />
</div>
<div className='ml-1'>{payload.name}</div>
</div>
<div className='border-b py-1 px-2 flex items-center'>
<div className='w-6 shrink-0'>
<Icon name='arrow-right-short' size={18} color='green' />
</div>
<div className='ml-1 font-medium'>Continuing {Math.round(payload.value)}%</div>
</div>
{payload.avgTimeFromPrevious && (
<div className='border-b py-1 px-2 flex items-center'>
<div className='w-6 shrink-0'>
<Icon name='clock-history' size={16} />
</div>
return (
<div className='relative'>
<Popover
content={<div className='bg-white rounded mt-1 text-xs'>
<div className='border-b py-1 px-2 flex items-center'>
<div className='w-6 shrink-0'>
<Icon name='link-45deg' size={18}/>
</div>
<div className='ml-1'>{payload.name}</div>
</div>
<div className='border-b py-1 px-2 flex items-center'>
<div className='w-6 shrink-0'>
<Icon name='arrow-right-short' size={18} color='green'/>
</div>
<div className='ml-1 font-medium'>Continuing {Math.round(payload.value)}%</div>
</div>
{payload.avgTimeFromPrevious && (
<div className='border-b py-1 px-2 flex items-center'>
<div className='w-6 shrink-0'>
<Icon name='clock-history' size={16}/>
</div>
<div className='ml-1 font-medium'>
Average time from previous step <span>{payload.avgTimeFromPrevious}</span>
</div>
</div>
)}
</div>}>
<div
className='flex items-center copy-popover select-none rounded shadow'
style={{
backgroundColor: 'white',
padding: '3px 6px',
// width: 'fit-content',
fontSize: '12px'
}}
>
<div style={{
maxWidth: '120px',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
marginRight: '5px'
}}>{payload.name}</div>
<span style={{ fontWeight: 'bold' }}>{Math.round(payload.value) + '%'}</span>
<div className='ml-1 font-medium'>
Average time from previous step <span>{payload.avgTimeFromPrevious}</span>
</div>
</div>
)}
</div>}>
<div
className='flex items-center copy-popover select-none rounded shadow'
style={{
backgroundColor: 'white',
padding: '3px 6px',
// width: 'fit-content',
maxWidth: '120px',
fontSize: '12px',
width: 'fit-content',
}}
>
<div style={{
maxWidth: '120px',
width: 'fit-content',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
marginRight: '5px'
}}>{payload.name}</div>
<span style={{fontWeight: 'bold'}}>{Math.round(payload.value) + '%'}</span>
</div>
</Popover>
</div>
</Popover>
</div>
);
);
}
export default NodeButton;

View file

@ -1,140 +1,140 @@
import React, { useState } from 'react';
import { Sankey, ResponsiveContainer } from 'recharts';
import React, {useState} from 'react';
import {Sankey, ResponsiveContainer} from 'recharts';
import CustomLink from './CustomLink';
import CustomNode from './CustomNode';
import { NoContent } from 'UI';
import {NoContent} from 'UI';
interface Node {
idd: number;
name: string;
eventType: string;
avgTimeFromPrevious: number | null;
idd: string;
name: string;
eventType: string;
avgTimeFromPrevious: number | null;
}
interface Link {
id: string;
eventType: string;
value: number;
source: number;
target: number;
id: string;
eventType: string;
value: number;
source: string;
target: string;
}
interface Data {
nodes: Node[];
links: Link[];
nodes: Node[];
links: Link[];
}
interface Props {
data: Data;
nodeWidth?: number;
height?: number;
onChartClick?: (filters: any[]) => void;
data: Data;
nodeWidth?: number;
height?: number;
onChartClick?: (filters: any[]) => void;
}
const SankeyChart: React.FC<Props> = ({
data,
height = 240,
onChartClick
}: Props) => {
const [highlightedLinks, setHighlightedLinks] = useState<string[]>([]);
const [hoveredLinks, setHoveredLinks] = useState<string[]>([]);
const SankeyChart: React.FC<Props> = ({data, height = 240, onChartClick}: Props) => {
const [highlightedLinks, setHighlightedLinks] = useState<string[]>([]);
const [hoveredLinks, setHoveredLinks] = useState<string[]>([]);
function findPreviousLinks(targetNodeIndex: number): Link[] {
const previousLinks: Link[] = [];
const visitedNodes: Set<number> = new Set();
function findPreviousLinks(targetNodeIndex: number): Link[] {
const previousLinks: Link[] = [];
const visitedNodes: Set<number> = new Set();
const findPreviousLinksRecursive = (nodeIndex: number) => {
visitedNodes.add(nodeIndex);
const findPreviousLinksRecursive = (nodeIndex: number) => {
visitedNodes.add(nodeIndex);
for (const link of data.links) {
if (link.target === nodeIndex && !visitedNodes.has(link.source)) {
previousLinks.push(link);
findPreviousLinksRecursive(link.source);
}
}
for (const link of data.links) {
if (link.target === nodeIndex && !visitedNodes.has(link.source)) {
previousLinks.push(link);
findPreviousLinksRecursive(link.source);
}
}
};
findPreviousLinksRecursive(targetNodeIndex);
return previousLinks;
}
const handleLinkMouseEnter = (linkData: any) => {
const {payload} = linkData;
const link: any = data.links.find(link => link.id === payload.id);
const previousLinks: any = findPreviousLinks(link.source).reverse();
previousLinks.push({id: payload.id});
setHoveredLinks(previousLinks.map((link: any) => link.id));
};
findPreviousLinksRecursive(targetNodeIndex);
const clickHandler = () => {
setHighlightedLinks(hoveredLinks);
return previousLinks;
}
const firstLink = data.links.find(link => link.id === hoveredLinks[0]) || null;
const lastLink = data.links.find(link => link.id === hoveredLinks[hoveredLinks.length - 1]) || null;
const handleLinkMouseEnter = (linkData: any) => {
const { payload } = linkData;
const link: any = data.links.find(link => link.id === payload.id);
const previousLinks: any = findPreviousLinks(link.source).reverse();
previousLinks.push({ id: payload.id });
setHoveredLinks(previousLinks.map((link: any) => link.id));
};
const firstNode = data.nodes[firstLink?.source];
const lastNode = data.nodes[lastLink?.target];
const clickHandler = () => {
setHighlightedLinks(hoveredLinks);
const filters = [];
const firstLink = data.links.find(link => link.id === hoveredLinks[0]) || null;
const lastLink = data.links.find(link => link.id === hoveredLinks[hoveredLinks.length - 1]) || null;
if (firstNode) {
filters.push({
operator: 'is',
type: firstNode.eventType,
value: [firstNode.name],
isEvent: true
});
}
const firstNode = data.nodes[firstLink?.source];
const lastNode = data.nodes[lastLink.target];
if (lastNode) {
filters.push({
operator: 'is',
type: lastNode.eventType,
value: [lastNode.name],
isEvent: true
});
}
const filters = [];
onChartClick?.(filters);
};
if (firstNode) {
filters.push({
operator: 'is',
type: firstNode.eventType,
value: [firstNode.name],
isEvent: true
});
}
// const processedData = processData(data);
if (lastNode) {
filters.push({
operator: 'is',
type: lastNode.eventType,
value: [lastNode.name],
isEvent: true
});
}
onChartClick?.(filters);
};
return (
<NoContent
style={{ paddingTop: '80px' }}
show={!data.nodes.length || !data.links.length}
title={'No data for the selected time period.'}
>
<ResponsiveContainer height={height} width='100%'>
<Sankey
data={data}
iterations={128}
node={<CustomNode />}
sort={true}
onClick={clickHandler}
link={({ source, target, id, ...linkProps }, index) => (
<CustomLink
{...linkProps}
hoveredLinks={hoveredLinks}
activeLinks={highlightedLinks}
strokeOpacity={highlightedLinks.includes(id) ? 0.8 : 0.2}
onMouseEnter={() => handleLinkMouseEnter(linkProps)}
onMouseLeave={() => setHoveredLinks([])}
/>
)}
margin={{ right: 200, bottom: 50 }}
return (
<NoContent
style={{paddingTop: '80px'}}
show={!data.nodes.length || !data.links.length}
title={'No data for the selected time period.'}
>
<defs>
<linearGradient id={'linkGradient'}>
<stop offset='0%' stopColor='rgba(57, 78, 255, 0.2)' />
<stop offset='100%' stopColor='rgba(57, 78, 255, 0.2)' />
</linearGradient>
</defs>
</Sankey>
</ResponsiveContainer>
</NoContent>
);
<ResponsiveContainer height={height} width='100%'>
<Sankey
data={data}
node={<CustomNode/>}
nodePadding={20}
sort={true}
nodeWidth={4}
iterations={128}
// linkCurvature={0.9}
onClick={clickHandler}
link={({source, target, id, ...linkProps}, index) => (
<CustomLink
{...linkProps}
hoveredLinks={hoveredLinks}
activeLinks={highlightedLinks}
strokeOpacity={highlightedLinks.includes(id) ? 0.8 : 0.2}
onMouseEnter={() => handleLinkMouseEnter(linkProps)}
onMouseLeave={() => setHoveredLinks([])}
/>
)}
margin={{right: 130, bottom: 50}}
>
<defs>
<linearGradient id={'linkGradient'}>
<stop offset='0%' stopColor='rgba(57, 78, 255, 0.2)'/>
<stop offset='100%' stopColor='rgba(57, 78, 255, 0.2)'/>
</linearGradient>
</defs>
</Sankey>
</ResponsiveContainer>
</NoContent>
);
};
export default SankeyChart;

View file

@ -1,410 +1,484 @@
import { makeAutoObservable, runInAction } from 'mobx';
import {makeAutoObservable, runInAction} from 'mobx';
import FilterSeries from './filterSeries';
import { DateTime } from 'luxon';
import {DateTime} from 'luxon';
import Session from 'App/mstore/types/session';
import Funnelissue from 'App/mstore/types/funnelIssue';
import { issueOptions, issueCategories, issueCategoriesMap, pathAnalysisEvents } from 'App/constants/filterOptions';
import { FilterKey } from 'Types/filter/filterType';
import Period, { LAST_24_HOURS } from 'Types/app/period';
import {issueOptions, issueCategories, issueCategoriesMap, pathAnalysisEvents} from 'App/constants/filterOptions';
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, INSIGHTS, TABLE, USER_PATH, WEB_VITALS } from 'App/constants/card';
import {metricService} from 'App/services';
import {FUNNEL, INSIGHTS, TABLE, USER_PATH, WEB_VITALS} from 'App/constants/card';
import Error from '../types/error';
import { getChartFormatter } from 'Types/dashboard/helper';
import {getChartFormatter} from 'Types/dashboard/helper';
import FilterItem from './filterItem';
import { filtersMap } from 'Types/filter/newFilter';
import {filtersMap} from 'Types/filter/newFilter';
import Issue from '../types/issue';
import { durationFormatted } from 'App/date';
import {durationFormatted} from 'App/date';
export class InsightIssue {
icon: string;
iconColor: string;
change: number;
isNew = false;
category: string;
label: string;
value: number;
oldValue: number;
isIncreased?: boolean;
icon: string;
iconColor: string;
change: number;
isNew = false;
category: string;
label: string;
value: number;
oldValue: number;
isIncreased?: boolean;
constructor(
category: string,
public name: string,
public ratio: number,
oldValue = 0,
value = 0,
change = 0,
isNew = false
) {
this.category = category;
this.value = Math.round(value);
this.oldValue = Math.round(oldValue);
// @ts-ignore
this.label = issueCategoriesMap[category];
this.icon = `ic-${category}`;
constructor(
category: string,
public name: string,
public ratio: number,
oldValue = 0,
value = 0,
change = 0,
isNew = false
) {
this.category = category;
this.value = Math.round(value);
this.oldValue = Math.round(oldValue);
// @ts-ignore
this.label = issueCategoriesMap[category];
this.icon = `ic-${category}`;
this.change = parseInt(change.toFixed(2));
this.isIncreased = this.change > 0;
this.iconColor = 'gray-dark';
this.isNew = isNew;
}
this.change = parseInt(change.toFixed(2));
this.isIncreased = this.change > 0;
this.iconColor = 'gray-dark';
this.isNew = isNew;
}
}
function cleanFilter(filter: any) {
delete filter['operatorOptions'];
delete filter['placeholder'];
delete filter['category'];
delete filter['label'];
delete filter['icon'];
delete filter['key'];
delete filter['operatorOptions'];
delete filter['placeholder'];
delete filter['category'];
delete filter['label'];
delete filter['icon'];
delete filter['key'];
}
export default class Widget {
public static get ID_KEY(): string {
return 'metricId';
}
metricId: any = undefined;
widgetId: any = undefined;
category?: string = undefined;
name: string = 'Untitled Card';
metricType: string = 'timeseries';
metricOf: string = 'sessionCount';
metricValue: any = '';
viewType: string = 'lineChart';
metricFormat: string = 'sessionCount';
series: FilterSeries[] = [];
sessions: [] = [];
isPublic: boolean = true;
owner: string = '';
lastModified: DateTime | null = new Date().getTime();
dashboards: any[] = [];
dashboardIds: any[] = [];
config: any = {};
page: number = 1;
limit: number = 5;
thumbnail?: string;
params: any = { density: 70 };
startType: string = 'start';
startPoint: FilterItem = new FilterItem(filtersMap[FilterKey.LOCATION]);
excludes: FilterItem[] = [];
hideExcess?: boolean = false;
period: Record<string, any> = Period({ rangeName: LAST_24_HOURS }); // temp value in detail view
hasChanged: boolean = false;
position: number = 0;
data: any = {
sessions: [],
issues: [],
total: 0,
chart: [],
namesMap: {},
avg: 0,
percentiles: []
};
isLoading: boolean = false;
isValid: boolean = false;
dashboardId: any = undefined;
predefinedKey: string = '';
constructor() {
makeAutoObservable(this);
const filterSeries = new FilterSeries();
this.series.push(filterSeries);
}
updateKey(key: string, value: any) {
this[key] = value;
}
removeSeries(index: number) {
this.series.splice(index, 1);
}
setSeries(series: FilterSeries[]) {
this.series = series;
}
addSeries() {
const series = new FilterSeries();
series.name = 'Series ' + (this.series.length + 1);
this.series.push(series);
}
createSeries(filters: Record<string, any>) {
const series = new FilterSeries().fromData({ filter: { filters } , name: 'AI Query', seriesId: 1 })
this.setSeries([series])
}
fromJson(json: any, period?: any) {
json.config = json.config || {};
runInAction(() => {
this.metricId = json.metricId;
this.widgetId = json.widgetId;
this.metricValue = this.metricValueFromArray(json.metricValue, json.metricType);
this.metricOf = json.metricOf;
this.metricType = json.metricType;
this.metricFormat = json.metricFormat;
this.viewType = json.viewType;
this.name = json.name;
this.series =
json.series && json.series.length > 0
? json.series.map((series: any) => new FilterSeries().fromJson(series))
: [new FilterSeries()];
this.dashboards = json.dashboards || [];
this.owner = json.ownerEmail;
this.lastModified =
json.editedAt || json.createdAt
? DateTime.fromMillis(json.editedAt || json.createdAt)
: null;
this.config = json.config;
this.position = json.config.position;
this.predefinedKey = json.predefinedKey;
this.category = json.category;
this.thumbnail = json.thumbnail;
this.isPublic = json.isPublic;
if (this.metricType === FUNNEL) {
this.series[0].filter.eventsOrder = 'then';
this.series[0].filter.eventsOrderSupport = ['then'];
}
if (this.metricType === USER_PATH) {
this.hideExcess = json.hideExcess;
this.startType = json.startType;
if (json.startPoint) {
if (Array.isArray(json.startPoint) && json.startPoint.length > 0) {
this.startPoint = new FilterItem().fromJson(json.startPoint[0]);
}
if (json.startPoint == typeof Object) {
this.startPoint = json.startPoint;
}
}
// TODO change this to excludes after the api change
if (json.exclude) {
this.series[0].filter.excludes = json.exclude.map((i: any) => new FilterItem().fromJson(i));
}
}
if (period) {
this.period = period;
}
});
return this;
}
toWidget(): any {
return {
config: {
position: this.position,
col: this.config.col,
row: this.config.row
}
};
}
toJson() {
const data: any = {
metricId: this.metricId,
widgetId: this.widgetId,
metricOf: this.metricOf,
metricValue: this.metricValueToArray(this.metricValue),
metricType: this.metricType,
metricFormat: this.metricFormat,
viewType: this.viewType,
name: this.name,
series: this.series.map((series: any) => series.toJson()),
thumbnail: this.thumbnail,
page: this.page,
limit: this.limit,
config: {
...this.config,
col:
this.metricType === FUNNEL ||
this.metricOf === FilterKey.ERRORS ||
this.metricOf === FilterKey.SESSIONS ||
this.metricOf === FilterKey.SLOWEST_RESOURCES ||
this.metricOf === FilterKey.MISSING_RESOURCES ||
this.metricOf === FilterKey.PAGES_RESPONSE_TIME_DISTRIBUTION ||
this.metricType === USER_PATH
? 4
: this.metricType === WEB_VITALS
? 1
: 2
}
};
if (this.metricType === USER_PATH) {
data.hideExcess = this.hideExcess;
data.startType = this.startType;
data.startPoint = [this.startPoint.toJson()];
data.excludes = this.series[0].filter.excludes.map((i: any) => i.toJson());
data.metricOf = 'sessionCount';
public static get ID_KEY(): string {
return 'metricId';
}
return data;
}
updateStartPoint(startPoint: any) {
runInAction(() => {
this.startPoint = new FilterItem(startPoint);
});
}
metricId: any = undefined;
widgetId: any = undefined;
category?: string = undefined;
name: string = 'Untitled Card';
metricType: string = 'timeseries';
metricOf: string = 'sessionCount';
metricValue: any = '';
viewType: string = 'lineChart';
metricFormat: string = 'sessionCount';
series: FilterSeries[] = [];
sessions: [] = [];
isPublic: boolean = true;
owner: string = '';
lastModified: DateTime | null = new Date().getTime();
dashboards: any[] = [];
dashboardIds: any[] = [];
config: any = {};
page: number = 1;
limit: number = 5;
thumbnail?: string;
params: any = {density: 70};
startType: string = 'start';
startPoint: FilterItem = new FilterItem(filtersMap[FilterKey.LOCATION]);
excludes: FilterItem[] = [];
hideExcess?: boolean = false;
validate() {
this.isValid = this.name.length > 0;
}
period: Record<string, any> = Period({rangeName: LAST_24_HOURS}); // temp value in detail view
hasChanged: boolean = false;
update(data: any) {
runInAction(() => {
Object.assign(this, data);
});
}
position: number = 0;
data: any = {
sessions: [],
issues: [],
total: 0,
chart: [],
namesMap: {},
avg: 0,
percentiles: []
};
isLoading: boolean = false;
isValid: boolean = false;
dashboardId: any = undefined;
predefinedKey: string = '';
exists() {
return this.metricId !== undefined;
}
constructor() {
makeAutoObservable(this);
setData(data: any, period: any) {
const _data: any = { ...data };
if (this.metricType === USER_PATH) {
_data['nodes'] = data.nodes.map((s: any) => ({
...s,
avgTimeFromPrevious: s.avgTimeFromPrevious ? durationFormatted(s.avgTimeFromPrevious) : null,
idd: Math.random().toString(36).substring(7),
}));
_data['links'] = data.links.map((s: any) => ({
...s,
id: Math.random().toString(36).substring(7),
}));
Object.assign(this.data, _data);
return _data;
const filterSeries = new FilterSeries();
this.series.push(filterSeries);
}
if (this.metricOf === FilterKey.ERRORS) {
_data['errors'] = data.errors.map((s: any) => new Error().fromJSON(s));
} else if (this.metricType === INSIGHTS) {
_data['issues'] = data
.filter((i: any) => i.change > 0 || i.change < 0)
.map(
(i: any) =>
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);
} else {
if (data.hasOwnProperty('chart')) {
_data['value'] = data.value;
_data['unit'] = data.unit;
_data['chart'] = getChartFormatter(period)(data.chart);
_data['namesMap'] = data.chart
.map((i: any) => Object.keys(i))
.flat()
.filter((i: any) => i !== 'time' && i !== 'timestamp')
.reduce((unique: any, item: any) => {
if (!unique.includes(item)) {
unique.push(item);
updateKey(key: string, value: any) {
this[key] = value;
}
removeSeries(index: number) {
this.series.splice(index, 1);
}
setSeries(series: FilterSeries[]) {
this.series = series;
}
addSeries() {
const series = new FilterSeries();
series.name = 'Series ' + (this.series.length + 1);
this.series.push(series);
}
createSeries(filters: Record<string, any>) {
const series = new FilterSeries().fromData({filter: {filters}, name: 'AI Query', seriesId: 1})
this.setSeries([series])
}
fromJson(json: any, period?: any) {
json.config = json.config || {};
runInAction(() => {
this.metricId = json.metricId;
this.widgetId = json.widgetId;
this.metricValue = this.metricValueFromArray(json.metricValue, json.metricType);
this.metricOf = json.metricOf;
this.metricType = json.metricType;
this.metricFormat = json.metricFormat;
this.viewType = json.viewType;
this.name = json.name;
this.series =
json.series && json.series.length > 0
? json.series.map((series: any) => new FilterSeries().fromJson(series))
: [new FilterSeries()];
this.dashboards = json.dashboards || [];
this.owner = json.ownerEmail;
this.lastModified =
json.editedAt || json.createdAt
? DateTime.fromMillis(json.editedAt || json.createdAt)
: null;
this.config = json.config;
this.position = json.config.position;
this.predefinedKey = json.predefinedKey;
this.category = json.category;
this.thumbnail = json.thumbnail;
this.isPublic = json.isPublic;
if (this.metricType === FUNNEL) {
this.series[0].filter.eventsOrder = 'then';
this.series[0].filter.eventsOrderSupport = ['then'];
}
return unique;
}, []);
} else {
_data['chart'] = getChartFormatter(period)(Array.isArray(data) ? data : []);
_data['namesMap'] = Array.isArray(data)
? data
.map((i) => Object.keys(i))
.flat()
.filter((i) => i !== 'time' && i !== 'timestamp')
.reduce((unique: any, item: any) => {
if (!unique.includes(item)) {
unique.push(item);
}
return unique;
}, [])
: [];
}
}
Object.assign(this.data, _data);
return _data;
}
if (this.metricType === USER_PATH) {
this.hideExcess = json.hideExcess;
this.startType = json.startType;
if (json.startPoint) {
if (Array.isArray(json.startPoint) && json.startPoint.length > 0) {
this.startPoint = new FilterItem().fromJson(json.startPoint[0]);
}
fetchSessions(metricId: any, filter: any): Promise<any> {
return new Promise((resolve) => {
metricService.fetchSessions(metricId, filter).then((response: any[]) => {
resolve(
response.map((cat: { sessions: any[] }) => {
return {
...cat,
sessions: cat.sessions.map((s: any) => new Session().fromJson(s))
};
})
);
});
});
}
if (json.startPoint == typeof Object) {
this.startPoint = json.startPoint;
}
}
// TODO change this to excludes after the api change
if (json.exclude) {
this.series[0].filter.excludes = json.exclude.map((i: any) => new FilterItem().fromJson(i));
}
}
async fetchIssues(card: any): Promise<any> {
try {
const response = await metricService.fetchIssues(card);
if (card.metricType === USER_PATH) {
return {
total: response.count,
issues: response.values.map((issue: any) => new Issue().fromJSON(issue))
};
} else {
const mapIssue = (issue: any) => new Funnelissue().fromJSON(issue);
const significantIssues = response.issues.significant?.map(mapIssue) || [];
const insignificantIssues = response.issues.insignificant?.map(mapIssue) || [];
return {
issues: significantIssues.length > 0 ? significantIssues : insignificantIssues
};
}
} catch (error) {
console.error('Error fetching issues:', error);
return {
issues: []
};
}
}
fetchIssue(funnelId: any, issueId: any, params: any): Promise<any> {
return new Promise((resolve, reject) => {
metricService
.fetchIssue(funnelId, issueId, params)
.then((response: any) => {
resolve({
issue: new Funnelissue().fromJSON(response.issue),
sessions: response.sessions.sessions.map((s: any) => new Session().fromJson(s))
});
})
.catch((error: any) => {
reject(error);
if (period) {
this.period = period;
}
});
});
}
private metricValueFromArray(metricValue: any, metricType: string) {
if (!Array.isArray(metricValue)) return metricValue;
if (metricType === TABLE) {
return issueOptions.filter((i: any) => metricValue.includes(i.value));
} else if (metricType === INSIGHTS) {
return issueCategories.filter((i: any) => metricValue.includes(i.value));
} else if (metricType === USER_PATH) {
return pathAnalysisEvents.filter((i: any) => metricValue.includes(i.value));
return this;
}
}
private metricValueToArray(metricValue: any) {
if (!Array.isArray(metricValue)) return metricValue;
return metricValue.map((i: any) => i.value);
}
toWidget(): any {
return {
config: {
position: this.position,
col: this.config.col,
row: this.config.row
}
};
}
toJson() {
const data: any = {
metricId: this.metricId,
widgetId: this.widgetId,
metricOf: this.metricOf,
metricValue: this.metricValueToArray(this.metricValue),
metricType: this.metricType,
metricFormat: this.metricFormat,
viewType: this.viewType,
name: this.name,
series: this.series.map((series: any) => series.toJson()),
thumbnail: this.thumbnail,
page: this.page,
limit: this.limit,
config: {
...this.config,
col:
this.metricType === FUNNEL ||
this.metricOf === FilterKey.ERRORS ||
this.metricOf === FilterKey.SESSIONS ||
this.metricOf === FilterKey.SLOWEST_RESOURCES ||
this.metricOf === FilterKey.MISSING_RESOURCES ||
this.metricOf === FilterKey.PAGES_RESPONSE_TIME_DISTRIBUTION ||
this.metricType === USER_PATH
? 4
: this.metricType === WEB_VITALS
? 1
: 2
}
};
if (this.metricType === USER_PATH) {
data.hideExcess = this.hideExcess;
data.startType = this.startType;
data.startPoint = [this.startPoint.toJson()];
data.excludes = this.series[0].filter.excludes.map((i: any) => i.toJson());
data.metricOf = 'sessionCount';
}
return data;
}
updateStartPoint(startPoint: any) {
runInAction(() => {
this.startPoint = new FilterItem(startPoint);
});
}
validate() {
this.isValid = this.name.length > 0;
}
update(data: any) {
runInAction(() => {
Object.assign(this, data);
});
}
exists() {
return this.metricId !== undefined;
}
setData(data: any, period: any) {
const _data: any = {...data};
if (this.metricType === USER_PATH) {
// Ensure nodes have unique IDs
const _data = processData(data);
// const nodes = data.nodes.map(node => ({
// ...node,
// avgTimeFromPrevious: node.avgTimeFromPrevious ? durationFormatted(node.avgTimeFromPrevious) : null,
// idd: node.idd || Math.random().toString(36).substring(7),
// }));
//
// // Ensure links have unique IDs and use node IDs
// const links = data.links.map(link => ({
// ...link,
// value: Math.round(link.value),
// id: link.id || Math.random().toString(36).substring(7),
// }));
//
// const _data = { nodes, links };
// _data['nodes'] = data.nodes.map((s: any) => ({
// ...s,
// avgTimeFromPrevious: s.avgTimeFromPrevious ? durationFormatted(s.avgTimeFromPrevious) : null,
// idd: Math.random().toString(36).substring(7),
// }));
// _data['links'] = data.links.map((s: any) => ({
// ...s,
// value: Math.round(s.value),
// id: Math.random().toString(36).substring(7),
// }));
Object.assign(this.data, _data);
console.log('data', _data);
return _data;
}
if (this.metricOf === FilterKey.ERRORS) {
_data['errors'] = data.errors.map((s: any) => new Error().fromJSON(s));
} else if (this.metricType === INSIGHTS) {
_data['issues'] = data
.filter((i: any) => i.change > 0 || i.change < 0)
.map(
(i: any) =>
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);
} else {
if (data.hasOwnProperty('chart')) {
_data['value'] = data.value;
_data['unit'] = data.unit;
_data['chart'] = getChartFormatter(period)(data.chart);
_data['namesMap'] = data.chart
.map((i: any) => Object.keys(i))
.flat()
.filter((i: any) => i !== 'time' && i !== 'timestamp')
.reduce((unique: any, item: any) => {
if (!unique.includes(item)) {
unique.push(item);
}
return unique;
}, []);
} else {
_data['chart'] = getChartFormatter(period)(Array.isArray(data) ? data : []);
_data['namesMap'] = Array.isArray(data)
? data
.map((i) => Object.keys(i))
.flat()
.filter((i) => i !== 'time' && i !== 'timestamp')
.reduce((unique: any, item: any) => {
if (!unique.includes(item)) {
unique.push(item);
}
return unique;
}, [])
: [];
}
}
Object.assign(this.data, _data);
return _data;
}
fetchSessions(metricId: any, filter: any): Promise<any> {
return new Promise((resolve) => {
metricService.fetchSessions(metricId, filter).then((response: any[]) => {
resolve(
response.map((cat: { sessions: any[] }) => {
return {
...cat,
sessions: cat.sessions.map((s: any) => new Session().fromJson(s))
};
})
);
});
});
}
async fetchIssues(card: any): Promise<any> {
try {
const response = await metricService.fetchIssues(card);
if (card.metricType === USER_PATH) {
return {
total: response.count,
issues: response.values.map((issue: any) => new Issue().fromJSON(issue))
};
} else {
const mapIssue = (issue: any) => new Funnelissue().fromJSON(issue);
const significantIssues = response.issues.significant?.map(mapIssue) || [];
const insignificantIssues = response.issues.insignificant?.map(mapIssue) || [];
return {
issues: significantIssues.length > 0 ? significantIssues : insignificantIssues
};
}
} catch (error) {
console.error('Error fetching issues:', error);
return {
issues: []
};
}
}
fetchIssue(funnelId: any, issueId: any, params: any): Promise<any> {
return new Promise((resolve, reject) => {
metricService
.fetchIssue(funnelId, issueId, params)
.then((response: any) => {
resolve({
issue: new Funnelissue().fromJSON(response.issue),
sessions: response.sessions.sessions.map((s: any) => new Session().fromJson(s))
});
})
.catch((error: any) => {
reject(error);
});
});
}
private metricValueFromArray(metricValue: any, metricType: string) {
if (!Array.isArray(metricValue)) return metricValue;
if (metricType === TABLE) {
return issueOptions.filter((i: any) => metricValue.includes(i.value));
} else if (metricType === INSIGHTS) {
return issueCategories.filter((i: any) => metricValue.includes(i.value));
} else if (metricType === USER_PATH) {
return pathAnalysisEvents.filter((i: any) => metricValue.includes(i.value));
}
}
private metricValueToArray(metricValue: any) {
if (!Array.isArray(metricValue)) return metricValue;
return metricValue.map((i: any) => i.value);
}
}
interface Node {
name: string;
eventType: string;
avgTimeFromPrevious: number | null;
idd?: string; // Making idd optional since it might not be present in raw data
}
interface Link {
eventType: string;
value: number;
source: number;
target: number;
id?: string; // Making id optional since it might not be present in raw data
}
interface Data {
nodes: Node[];
links: Link[];
}
const generateUniqueId = (): string => Math.random().toString(36).substring(2, 15);
const processData = (data: Data): { nodes: Node[], links: { source: number, target: number, value: number, id: string }[] } => {
// Ensure nodes have unique IDs
const nodes = data.nodes.map(node => ({
...node,
avgTimeFromPrevious: node.avgTimeFromPrevious ? durationFormatted(node.avgTimeFromPrevious) : null,
idd: node.idd || generateUniqueId(),
}));
// Ensure links have unique IDs
const links = data.links.map(link => ({
...link,
id: link.id || generateUniqueId(),
}));
// Sort links by source and then by target
links.sort((a, b) => {
if (a.source === b.source) {
return a.target - b.target;
}
return a.source - b.source;
});
// Sort nodes based on their first appearance in the sorted links to maintain visual consistency
const sortedNodes = nodes.slice().sort((a, b) => {
const aIndex = links.findIndex(link => link.source === nodes.indexOf(a) || link.target === nodes.indexOf(a));
const bIndex = links.findIndex(link => link.source === nodes.indexOf(b) || link.target === nodes.indexOf(b));
return aIndex - bIndex;
});
return { nodes: sortedNodes, links };
};

View file

@ -78,7 +78,7 @@
"react-toastify": "^9.1.1",
"react-virtualized": "^9.22.3",
"react18-json-view": "^0.2.8",
"recharts": "^2.8.0",
"recharts": "^2.12.6",
"redux": "^4.0.5",
"redux-immutable": "^4.0.0",
"redux-thunk": "^2.3.0",

View file

@ -18352,7 +18352,7 @@ __metadata:
react-toastify: ^9.1.1
react-virtualized: ^9.22.3
react18-json-view: ^0.2.8
recharts: ^2.8.0
recharts: ^2.12.6
redux: ^4.0.5
redux-immutable: ^4.0.0
redux-thunk: ^2.3.0
@ -21455,9 +21455,9 @@ __metadata:
languageName: node
linkType: hard
"recharts@npm:^2.8.0":
version: 2.12.4
resolution: "recharts@npm:2.12.4"
"recharts@npm:^2.12.6":
version: 2.12.7
resolution: "recharts@npm:2.12.7"
dependencies:
clsx: ^2.0.0
eventemitter3: ^4.0.1
@ -21470,7 +21470,7 @@ __metadata:
peerDependencies:
react: ^16.0.0 || ^17.0.0 || ^18.0.0
react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0
checksum: 15b250b0b45bf26cb3e3913aa0f2ef931fbf1511d5e535c871f0ac3443a9e71bd1c105ec673761d73cbe492d83e98f66f03bea3e30f905118a606249733d0f5c
checksum: 2522d841a1f4e4c0a37046ddb61fa958ac37a66df63dcd4c6cb9113e3f7a71892d74e44494a55bc40faa0afd74d9cf58fec3d2ce53a8ddf997e75367bdd033fc
languageName: node
linkType: hard