change(ui): path analysis clear filter (#2207)
This commit is contained in:
parent
45655a9608
commit
1f9019a103
9 changed files with 930 additions and 827 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue