diff --git a/frontend/app/components/Dashboard/components/CardIssues/CardIssues.tsx b/frontend/app/components/Dashboard/components/CardIssues/CardIssues.tsx
index d5f68ef68..9495d37d0 100644
--- a/frontend/app/components/Dashboard/components/CardIssues/CardIssues.tsx
+++ b/frontend/app/components/Dashboard/components/CardIssues/CardIssues.tsx
@@ -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(, { 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(() => (
@@ -94,7 +101,8 @@ function CardIssues() {
)}
-
+
+ {hasFilters && }
diff --git a/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx b/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx
index 59ff4780a..2416152dc 100644
--- a/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx
+++ b/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx
@@ -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();
- const isMounted = useIsMounted();
- const [data, setData] = useState(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();
+ const isMounted = useIsMounted();
+ const [data, setData] = useState(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 ;
- }
+ if (metricType === FUNNEL) {
+ return ;
+ }
- 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 ;
- }
- return ;
- }
+ 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 ;
+ }
+ return ;
+ }
- if (metricType === TIMESERIES) {
- if (viewType === 'lineChart') {
- return (
-
- );
- } else if (viewType === 'progress') {
- return (
-
- );
- }
- }
+ if (metricType === TIMESERIES) {
+ if (viewType === 'lineChart') {
+ return (
+
+ );
+ } else if (viewType === 'progress') {
+ return (
+
+ );
+ }
+ }
- if (metricType === TABLE) {
- if (metricOf === FilterKey.SESSIONS) {
- return (
-
- );
- }
- if (metricOf === FilterKey.ERRORS) {
- return (
-
- );
- }
- if (viewType === TABLE) {
- return (
-
- );
- } else if (viewType === 'pieChart') {
- return (
-
- );
- }
- }
- if (metricType === CLICKMAP) {
- if (!props.isPreview) {
- return (
-
-

-
- );
- }
- return (
-
- );
- }
+ if (metricType === TABLE) {
+ if (metricOf === FilterKey.SESSIONS) {
+ return (
+
+ );
+ }
+ if (metricOf === FilterKey.ERRORS) {
+ return (
+
+ );
+ }
+ if (viewType === TABLE) {
+ return (
+
+ );
+ } else if (viewType === 'pieChart') {
+ return (
+
+ );
+ }
+ }
+ if (metricType === CLICKMAP) {
+ if (!props.isPreview) {
+ return (
+
+

+
+ );
+ }
+ return (
+
+ );
+ }
- if (metricType === INSIGHTS) {
- return ;
- }
+ if (metricType === INSIGHTS) {
+ return ;
+ }
- if (metricType === USER_PATH && data && data.links) {
- return {
- dashboardStore.drillDownFilter.merge({ filters, page: 1 });
- }} />;
- }
+ if (metricType === USER_PATH && data && data.links) {
+ // return ;
+ return {
+ dashboardStore.drillDownFilter.merge({filters, page: 1});
+ }}/>;
+ }
- if (metricType === RETENTION) {
- if (viewType === 'trend') {
- return (
-
- );
- } else if (viewType === 'cohort') {
- return (
-
- );
- }
- }
+ if (metricType === RETENTION) {
+ if (viewType === 'trend') {
+ return (
+
+ );
+ } else if (viewType === 'cohort') {
+ return (
+
+ );
+ }
+ }
- return Unknown metric type
;
- };
- return (
-
- {renderChart()}
-
- );
+ return Unknown metric type
;
+ };
+ return (
+
+ {renderChart()}
+
+ );
}
export default observer(WidgetChart);
diff --git a/frontend/app/components/shared/Insights/SankeyChart/CustomLink.tsx b/frontend/app/components/shared/Insights/SankeyChart/CustomLink.tsx
index e8c185558..7749fa1f6 100644
--- a/frontend/app/components/shared/Insights/SankeyChart/CustomLink.tsx
+++ b/frontend/app/components/shared/Insights/SankeyChart/CustomLink.tsx
@@ -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 = (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 (
-
- {
- setFill('rgba(57, 78, 255, 0.5)');
- }}
- onMouseLeave={() => {
- setFill('url(#linkGradient)');
- }}
- />
-
+
+ {
+ setFill('rgba(57, 78, 255, 0.5)');
+ }}
+ onMouseLeave={() => {
+ setFill('url(#linkGradient)');
+ }}
+ />
+
);
-}
+};
export default CustomLink;
diff --git a/frontend/app/components/shared/Insights/SankeyChart/CustomNode.tsx b/frontend/app/components/shared/Insights/SankeyChart/CustomNode.tsx
index dc54d435e..330753d3f 100644
--- a/frontend/app/components/shared/Insights/SankeyChart/CustomNode.tsx
+++ b/frontend/app/components/shared/Insights/SankeyChart/CustomNode.tsx
@@ -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 (
-
-
-
- {/**/}
- {/* */}
- {/**/}
-
-
-
-
-
- );
+interface CustomNodeProps {
+ x: number;
+ y: number;
+ width: number;
+ height: number;
+ index: number;
+ payload: any;
+ containerWidth: number;
+ activeNodes: any[];
}
-export default CustomNode;
\ No newline at end of file
+const CustomNode: React.FC = (props) => {
+ const { x, y, width, height, index, payload, containerWidth } = props;
+ const isOut = x + width + 6 > containerWidth;
+
+ return (
+
+
+
+
+
+
+ );
+};
+
+export default CustomNode;
diff --git a/frontend/app/components/shared/Insights/SankeyChart/NodeButton.tsx b/frontend/app/components/shared/Insights/SankeyChart/NodeButton.tsx
index f20ee3278..8468ed553 100644
--- a/frontend/app/components/shared/Insights/SankeyChart/NodeButton.tsx
+++ b/frontend/app/components/shared/Insights/SankeyChart/NodeButton.tsx
@@ -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 (
-
-
-
-
-
-
-
-
Continuing {Math.round(payload.value)}%
-
- {payload.avgTimeFromPrevious && (
-
-
-
-
+ return (
+
+
+
+
+
+
+
+
Continuing {Math.round(payload.value)}%
+
+ {payload.avgTimeFromPrevious && (
+
+
+
+
-
- Average time from previous step {payload.avgTimeFromPrevious}
-
-
- )}
- }>
-
-
{payload.name}
-
{Math.round(payload.value) + '%'}
+
+ Average time from previous step {payload.avgTimeFromPrevious}
+
+
+ )}
+
}>
+
+
{payload.name}
+
{Math.round(payload.value) + '%'}
+
+
-
-
- );
+ );
}
export default NodeButton;
diff --git a/frontend/app/components/shared/Insights/SankeyChart/SankeyChart.tsx b/frontend/app/components/shared/Insights/SankeyChart/SankeyChart.tsx
index 2e65e75cd..02670e875 100644
--- a/frontend/app/components/shared/Insights/SankeyChart/SankeyChart.tsx
+++ b/frontend/app/components/shared/Insights/SankeyChart/SankeyChart.tsx
@@ -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 = ({
- data,
- height = 240,
- onChartClick
- }: Props) => {
- const [highlightedLinks, setHighlightedLinks] = useState([]);
- const [hoveredLinks, setHoveredLinks] = useState([]);
+const SankeyChart: React.FC = ({data, height = 240, onChartClick}: Props) => {
+ const [highlightedLinks, setHighlightedLinks] = useState([]);
+ const [hoveredLinks, setHoveredLinks] = useState([]);
- function findPreviousLinks(targetNodeIndex: number): Link[] {
- const previousLinks: Link[] = [];
- const visitedNodes: Set = new Set();
+ function findPreviousLinks(targetNodeIndex: number): Link[] {
+ const previousLinks: Link[] = [];
+ const visitedNodes: Set = 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 (
-
-
- }
- sort={true}
- onClick={clickHandler}
- link={({ source, target, id, ...linkProps }, index) => (
- handleLinkMouseEnter(linkProps)}
- onMouseLeave={() => setHoveredLinks([])}
- />
- )}
- margin={{ right: 200, bottom: 50 }}
+ return (
+
-
-
-
-
-
-
-
-
-
- );
+
+ }
+ nodePadding={20}
+ sort={true}
+ nodeWidth={4}
+ iterations={128}
+ // linkCurvature={0.9}
+ onClick={clickHandler}
+ link={({source, target, id, ...linkProps}, index) => (
+ handleLinkMouseEnter(linkProps)}
+ onMouseLeave={() => setHoveredLinks([])}
+ />
+ )}
+ margin={{right: 130, bottom: 50}}
+ >
+
+
+
+
+
+
+
+
+
+ );
};
export default SankeyChart;
diff --git a/frontend/app/mstore/types/widget.ts b/frontend/app/mstore/types/widget.ts
index 511794cfa..61bcc863f 100644
--- a/frontend/app/mstore/types/widget.ts
+++ b/frontend/app/mstore/types/widget.ts
@@ -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 = 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) {
- 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 = 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) {
+ 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 {
- 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 {
- 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 {
- 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 {
+ 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 {
+ 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 {
+ 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 };
+};
diff --git a/frontend/package.json b/frontend/package.json
index c7e068078..d147175b1 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -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",
diff --git a/frontend/yarn.lock b/frontend/yarn.lock
index 9daf50ca9..7eaa2109f 100644
--- a/frontend/yarn.lock
+++ b/frontend/yarn.lock
@@ -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