Product analytics refinements (#3011)
* Various UX, UI and Functional Improvements in Dashboards & Cards - Depth filter of Sankey chart data in frontend - Dashboard & Cards empty state view updates - Disabled save image feature on cards * Fixed empty views and headers * Various improvements across dashboards and cards. * Dashboard and Sankey refinements. * More improvements in Sankey and Dashboard * Autocomplete with checklist -- improvements
This commit is contained in:
parent
afb08cfe6d
commit
3dc933daf3
10 changed files with 229 additions and 93 deletions
|
|
@ -3,8 +3,9 @@ import { echarts, defaultOptions } from './init';
|
||||||
import { SankeyChart } from 'echarts/charts';
|
import { SankeyChart } from 'echarts/charts';
|
||||||
import { sankeyTooltip, getEventPriority, getNodeName } from './sankeyUtils';
|
import { sankeyTooltip, getEventPriority, getNodeName } from './sankeyUtils';
|
||||||
import { NoContent } from 'App/components/ui';
|
import { NoContent } from 'App/components/ui';
|
||||||
import {InfoCircleOutlined} from '@ant-design/icons';
|
import { InfoCircleOutlined } from '@ant-design/icons';
|
||||||
import { X } from 'lucide-react';
|
import { X } from 'lucide-react';
|
||||||
|
|
||||||
echarts.use([SankeyChart]);
|
echarts.use([SankeyChart]);
|
||||||
|
|
||||||
interface SankeyNode {
|
interface SankeyNode {
|
||||||
|
|
@ -15,9 +16,9 @@ interface SankeyNode {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SankeyLink {
|
interface SankeyLink {
|
||||||
source: number;
|
source: number | string;
|
||||||
target: number;
|
target: number | string;
|
||||||
value: number;
|
value: number;
|
||||||
sessionsCount: number;
|
sessionsCount: number;
|
||||||
eventType?: string;
|
eventType?: string;
|
||||||
}
|
}
|
||||||
|
|
@ -34,6 +35,42 @@ interface Props {
|
||||||
isUngrouped?: boolean;
|
isUngrouped?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildSubgraph(
|
||||||
|
startNodeId: string | number,
|
||||||
|
nodes: SankeyNode[],
|
||||||
|
links: SankeyLink[]
|
||||||
|
) {
|
||||||
|
const visited = new Set<string | number>();
|
||||||
|
const queue = [startNodeId];
|
||||||
|
visited.add(startNodeId);
|
||||||
|
|
||||||
|
const adjacency: Record<string | number, Array<string | number>> = {};
|
||||||
|
links.forEach((link) => {
|
||||||
|
if (!adjacency[link.source]) {
|
||||||
|
adjacency[link.source] = [];
|
||||||
|
}
|
||||||
|
adjacency[link.source].push(link.target);
|
||||||
|
});
|
||||||
|
|
||||||
|
while (queue.length > 0) {
|
||||||
|
const current = queue.shift()!;
|
||||||
|
const neighbors = adjacency[current] || [];
|
||||||
|
neighbors.forEach((nbr) => {
|
||||||
|
if (!visited.has(nbr)) {
|
||||||
|
visited.add(nbr);
|
||||||
|
queue.push(nbr);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const subNodes = nodes.filter((n) => visited.has(n.id));
|
||||||
|
const subLinks = links.filter(
|
||||||
|
(l) => visited.has(l.source) && visited.has(l.target)
|
||||||
|
);
|
||||||
|
|
||||||
|
return { subNodes, subLinks };
|
||||||
|
}
|
||||||
|
|
||||||
const EChartsSankey: React.FC<Props> = (props) => {
|
const EChartsSankey: React.FC<Props> = (props) => {
|
||||||
const { data, height = 240, onChartClick, isUngrouped } = props;
|
const { data, height = 240, onChartClick, isUngrouped } = props;
|
||||||
const chartRef = React.useRef<HTMLDivElement>(null);
|
const chartRef = React.useRef<HTMLDivElement>(null);
|
||||||
|
|
@ -44,31 +81,53 @@ const EChartsSankey: React.FC<Props> = (props) => {
|
||||||
style={{ minHeight: height }}
|
style={{ minHeight: height }}
|
||||||
title={
|
title={
|
||||||
<div className="flex items-center relative">
|
<div className="flex items-center relative">
|
||||||
<InfoCircleOutlined className='hidden md:inline-block mr-1' />
|
<InfoCircleOutlined className="hidden md:inline-block mr-1" />
|
||||||
Set a start or end point to visualize the journey. If set, try adjusting filters.
|
Set a start or end point to visualize the journey. If set, try adjusting filters.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
}
|
}
|
||||||
show={true}
|
show={true}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const [finalNodeCount, setFinalNodeCount] = React.useState(data.nodes.length);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!chartRef.current) return;
|
if (!chartRef.current) return;
|
||||||
|
|
||||||
|
|
||||||
|
const startNodes = data.nodes.filter((n) => n.depth === 0);
|
||||||
|
let finalNodes = data.nodes;
|
||||||
|
let finalLinks = data.links;
|
||||||
|
|
||||||
|
if (startNodes.length > 1) {
|
||||||
|
const chosenStartNode = startNodes[0];
|
||||||
|
const { subNodes, subLinks } = buildSubgraph(
|
||||||
|
chosenStartNode.id!,
|
||||||
|
data.nodes,
|
||||||
|
data.links
|
||||||
|
);
|
||||||
|
finalNodes = subNodes;
|
||||||
|
finalLinks = subLinks;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const chart = echarts.init(chartRef.current);
|
const chart = echarts.init(chartRef.current);
|
||||||
|
|
||||||
|
|
||||||
const maxDepth = 4;
|
const maxDepth = 4;
|
||||||
const filteredNodes = data.nodes.filter((n) => (n.depth ?? 0) <= maxDepth);
|
const filteredNodes = finalNodes.filter((n) => (n.depth ?? 0) <= maxDepth);
|
||||||
const filteredLinks = data.links.filter((l) => {
|
const filteredLinks = finalLinks.filter((l) => {
|
||||||
const sourceNode = data.nodes.find((n) => n.id === l.source);
|
const sourceNode = finalNodes.find((n) => n.id === l.source);
|
||||||
const targetNode = data.nodes.find((n) => n.id === l.target);
|
const targetNode = finalNodes.find((n) => n.id === l.target);
|
||||||
return (sourceNode?.depth ?? 0) <= maxDepth && (targetNode?.depth ?? 0) <= maxDepth;
|
return (
|
||||||
|
(sourceNode?.depth ?? 0) <= maxDepth && (targetNode?.depth ?? 0) <= maxDepth
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
const nodeValues = new Array(filteredNodes.length).fill(0);
|
setFinalNodeCount(filteredNodes.length);
|
||||||
|
|
||||||
|
|
||||||
const echartNodes = filteredNodes
|
const echartNodes = filteredNodes
|
||||||
|
|
@ -81,8 +140,9 @@ const EChartsSankey: React.FC<Props> = (props) => {
|
||||||
computedName === 'Others'
|
computedName === 'Others'
|
||||||
? 'rgba(34,44,154,.9)'
|
? 'rgba(34,44,154,.9)'
|
||||||
: n.eventType === 'DROP'
|
: n.eventType === 'DROP'
|
||||||
? '#B5B7C8'
|
? '#B5B7C8'
|
||||||
: '#394eff';
|
: '#394eff';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: computedName,
|
name: computedName,
|
||||||
depth: n.depth,
|
depth: n.depth,
|
||||||
|
|
@ -93,16 +153,14 @@ const EChartsSankey: React.FC<Props> = (props) => {
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
|
|
||||||
if (a.depth === b.depth) {
|
if (a.depth === b.depth) {
|
||||||
return getEventPriority(a.type || '') - getEventPriority(b.type || '');
|
return getEventPriority(a.type || '') - getEventPriority(b.type || '');
|
||||||
} else {
|
} else {
|
||||||
return (a.depth as number) - (b.depth as number);
|
return (a.depth as number) - (b.depth as number);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const distinctSteps = new Set(data.nodes.map(node => node.depth)).size;
|
|
||||||
console.log('Number of steps returned by the backend:', distinctSteps);
|
|
||||||
|
|
||||||
const echartLinks = filteredLinks.map((l) => ({
|
const echartLinks = filteredLinks.map((l) => ({
|
||||||
source: echartNodes.findIndex((n) => n.id === l.source),
|
source: echartNodes.findIndex((n) => n.id === l.source),
|
||||||
target: echartNodes.findIndex((n) => n.id === l.target),
|
target: echartNodes.findIndex((n) => n.id === l.target),
|
||||||
|
|
@ -111,17 +169,14 @@ const EChartsSankey: React.FC<Props> = (props) => {
|
||||||
lineStyle: { opacity: 0.1 },
|
lineStyle: { opacity: 0.1 },
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
if (echartNodes.length === 0) return;
|
||||||
nodeValues.forEach((v, i) => {
|
|
||||||
const outgoingValues = echartLinks
|
|
||||||
.filter((l) => l.source === i)
|
|
||||||
.reduce((p, c) => p + c.value, 0);
|
|
||||||
const incomingValues = echartLinks
|
|
||||||
.filter((l) => l.target === i)
|
|
||||||
.reduce((p, c) => p + c.value, 0);
|
|
||||||
nodeValues[i] = Math.max(outgoingValues, incomingValues);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
const startNodeValue = echartLinks
|
||||||
|
.filter((link) => link.source === 0)
|
||||||
|
.reduce((sum, link) => sum + link.value, 0);
|
||||||
|
|
||||||
|
|
||||||
const option = {
|
const option = {
|
||||||
...defaultOptions,
|
...defaultOptions,
|
||||||
tooltip: {
|
tooltip: {
|
||||||
|
|
@ -147,31 +202,49 @@ const EChartsSankey: React.FC<Props> = (props) => {
|
||||||
label: {
|
label: {
|
||||||
show: true,
|
show: true,
|
||||||
position: 'top',
|
position: 'top',
|
||||||
textShadowColor: "transparent",
|
textShadowColor: 'transparent',
|
||||||
textBorderColor: "transparent",
|
textBorderColor: 'transparent',
|
||||||
align: 'left',
|
align: 'left',
|
||||||
overflow: "truncate",
|
overflow: 'truncate',
|
||||||
|
maxWidth: 30,
|
||||||
distance: 3,
|
distance: 3,
|
||||||
offset: [-20, 0],
|
offset: [-20, 0],
|
||||||
formatter: function(params: any) {
|
formatter: function (params: any) {
|
||||||
const totalSessions = nodeValues.reduce((sum: number, v: number) => sum + v, 0);
|
const nodeVal = params.value;
|
||||||
const percentage = totalSessions ? ((params.value / totalSessions) * 100).toFixed(1) + '%' : '0%';
|
const percentage = startNodeValue
|
||||||
return `{header|${params.name}}\n{body|${percentage} ${params.value} Sessions}`;
|
? ((nodeVal / startNodeValue) * 100).toFixed(1) + '%'
|
||||||
|
: '0%';
|
||||||
|
|
||||||
|
return (
|
||||||
|
`{header|${params.name}}\n` +
|
||||||
|
`{body|}{percentage|${percentage}} {sessions|${nodeVal}}`
|
||||||
|
);
|
||||||
},
|
},
|
||||||
rich: {
|
rich: {
|
||||||
header: {
|
header: {
|
||||||
fontWeight: 'bold',
|
fontWeight: '600',
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: '#333'
|
color: '#333',
|
||||||
|
overflow: 'truncate',
|
||||||
|
paddingBottom:'.5rem',
|
||||||
},
|
},
|
||||||
body: {
|
body: {
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: '#666'
|
color: '#000',
|
||||||
}
|
},
|
||||||
}
|
percentage: {
|
||||||
|
fontSize: 12,
|
||||||
|
color: '#454545',
|
||||||
|
},
|
||||||
|
sessions: {
|
||||||
|
fontSize: 12,
|
||||||
|
fontFamily: "mono, 'monospace', sans-serif",
|
||||||
|
color: '#999999',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
formatter: sankeyTooltip(echartNodes, nodeValues),
|
formatter: sankeyTooltip(echartNodes, []),
|
||||||
},
|
},
|
||||||
nodeAlign: 'left',
|
nodeAlign: 'left',
|
||||||
nodeWidth: 40,
|
nodeWidth: 40,
|
||||||
|
|
@ -189,8 +262,10 @@ const EChartsSankey: React.FC<Props> = (props) => {
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
chart.setOption(option);
|
chart.setOption(option);
|
||||||
|
|
||||||
|
|
||||||
function getUpstreamNodes(nodeIdx: number, visited = new Set<number>()) {
|
function getUpstreamNodes(nodeIdx: number, visited = new Set<number>()) {
|
||||||
if (visited.has(nodeIdx)) return;
|
if (visited.has(nodeIdx)) return;
|
||||||
visited.add(nodeIdx);
|
visited.add(nodeIdx);
|
||||||
|
|
@ -231,7 +306,7 @@ const EChartsSankey: React.FC<Props> = (props) => {
|
||||||
const baseOpacity = connectedChain.has(idx) ? 1 : 0.35;
|
const baseOpacity = connectedChain.has(idx) ? 1 : 0.35;
|
||||||
const extraStyle =
|
const extraStyle =
|
||||||
idx === hoveredIndex
|
idx === hoveredIndex
|
||||||
? { borderColor: '#000000', borderWidth: 1, borderType: 'dotted' }
|
? { borderColor: '#000', borderWidth: 1, borderType: 'dotted' }
|
||||||
: {};
|
: {};
|
||||||
return {
|
return {
|
||||||
...node,
|
...node,
|
||||||
|
|
@ -267,7 +342,6 @@ const EChartsSankey: React.FC<Props> = (props) => {
|
||||||
|
|
||||||
chart.on('mouseout', function (params: any) {
|
chart.on('mouseout', function (params: any) {
|
||||||
if (params.dataType === 'node') {
|
if (params.dataType === 'node') {
|
||||||
// Restore original styles on mouseout.
|
|
||||||
chart.setOption({
|
chart.setOption({
|
||||||
series: [
|
series: [
|
||||||
{
|
{
|
||||||
|
|
@ -279,21 +353,21 @@ const EChartsSankey: React.FC<Props> = (props) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
chart.on('click', function (params: any) {
|
chart.on('click', function (params: any) {
|
||||||
if (!onChartClick) return;
|
if (!onChartClick) return;
|
||||||
if (params.dataType === 'node') {
|
if (params.dataType === 'node') {
|
||||||
const nodeIndex = params.dataIndex;
|
const nodeIndex = params.dataIndex;
|
||||||
// Use filteredNodes here.
|
|
||||||
const node = filteredNodes[nodeIndex];
|
const node = filteredNodes[nodeIndex];
|
||||||
onChartClick([{ node }]);
|
onChartClick([{ node }]);
|
||||||
} else if (params.dataType === 'edge') {
|
} else if (params.dataType === 'edge') {
|
||||||
const linkIndex = params.dataIndex;
|
const linkIndex = params.dataIndex;
|
||||||
// Use filteredLinks here.
|
|
||||||
const link = filteredLinks[linkIndex];
|
const link = filteredLinks[linkIndex];
|
||||||
onChartClick([{ link }]);
|
onChartClick([{ link }]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
const ro = new ResizeObserver(() => chart.resize());
|
const ro = new ResizeObserver(() => chart.resize());
|
||||||
ro.observe(chartRef.current);
|
ro.observe(chartRef.current);
|
||||||
|
|
||||||
|
|
@ -303,11 +377,29 @@ const EChartsSankey: React.FC<Props> = (props) => {
|
||||||
};
|
};
|
||||||
}, [data, height, onChartClick]);
|
}, [data, height, onChartClick]);
|
||||||
|
|
||||||
const containerStyle: React.CSSProperties = isUngrouped
|
|
||||||
? {width: '100%', minHeight: 500, height: '100%', overflowY: 'auto' }
|
|
||||||
: { width: '100%', height };
|
|
||||||
|
let containerStyle: React.CSSProperties;
|
||||||
|
if (isUngrouped) {
|
||||||
|
|
||||||
|
|
||||||
|
const dynamicMinHeight = finalNodeCount * 15;
|
||||||
|
containerStyle = {
|
||||||
|
width: '100%',
|
||||||
|
minHeight: dynamicMinHeight,
|
||||||
|
height: '100%',
|
||||||
|
overflowY: 'auto',
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
|
||||||
|
containerStyle = {
|
||||||
|
width: '100%',
|
||||||
|
height,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return <div ref={chartRef} style={containerStyle} />;
|
return <div ref={chartRef} style={containerStyle} className='min-w-[600px] overflow-scroll' />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default EChartsSankey;
|
export default EChartsSankey;
|
||||||
|
|
@ -65,12 +65,12 @@ export const tabItems: Record<string, TabItem[]> = {
|
||||||
type: USER_PATH,
|
type: USER_PATH,
|
||||||
description: 'Understand the paths users take through your product.',
|
description: 'Understand the paths users take through your product.',
|
||||||
},
|
},
|
||||||
// { TODO: 1.23+
|
|
||||||
// icon: <Icon name={'dashboards/cohort-chart'} color={'inherit'} size={16} />,
|
|
||||||
// title: 'Retention',
|
|
||||||
// type: RETENTION,
|
|
||||||
// description: 'Analyze user retention over specific time periods.',
|
|
||||||
// },
|
|
||||||
{
|
{
|
||||||
icon: <Icon name={'dashboards/heatmap-2'} color={'inherit'} size={16} />,
|
icon: <Icon name={'dashboards/heatmap-2'} color={'inherit'} size={16} />,
|
||||||
title: 'Heatmaps',
|
title: 'Heatmaps',
|
||||||
|
|
@ -143,12 +143,12 @@ export const tabItems: Record<string, TabItem[]> = {
|
||||||
type: FilterKey.USER_DEVICE,
|
type: FilterKey.USER_DEVICE,
|
||||||
description: 'Explore the devices used by your users.',
|
description: 'Explore the devices used by your users.',
|
||||||
},
|
},
|
||||||
// { TODO: 1.23+ maybe
|
|
||||||
// icon: <ArrowDown10 width={16} />,
|
|
||||||
// title: 'Speed Index by Country',
|
|
||||||
// type: TABLE,
|
|
||||||
// description: 'Measure performance across different regions.',
|
|
||||||
// },
|
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -223,7 +223,7 @@ function CategoryTab({
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO This code here makes 0 sense
|
|
||||||
if (selectedCard.cardType === FUNNEL) {
|
if (selectedCard.cardType === FUNNEL) {
|
||||||
cardData.series = [];
|
cardData.series = [];
|
||||||
cardData.series.push(new FilterSeries());
|
cardData.series.push(new FilterSeries());
|
||||||
|
|
@ -317,7 +317,7 @@ const AddCardSection = observer(
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
'pt-4 pb-6 px-6 rounded-xl bg-white border border-gray-lighter flex flex-col gap-2'
|
'pt-4 pb-6 px-6 rounded-xl bg-white border border-gray-lighter flex flex-col gap-2 shadow-sm'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className={'flex justify-between p-2'}>
|
<div className={'flex justify-between p-2'}>
|
||||||
|
|
|
||||||
|
|
@ -140,12 +140,11 @@ const ListView: React.FC<Props> = ({
|
||||||
);
|
);
|
||||||
|
|
||||||
const onItemClick = (metric: Widget) => {
|
const onItemClick = (metric: Widget) => {
|
||||||
if (disableSelection) return;
|
if (disableSelection) {
|
||||||
if (toggleSelection) {
|
|
||||||
toggleSelection(metric.metricId);
|
|
||||||
} else {
|
|
||||||
const path = withSiteId(`/metrics/${metric.metricId}`, siteId);
|
const path = withSiteId(`/metrics/${metric.metricId}`, siteId);
|
||||||
history.push(path);
|
history.push(path);
|
||||||
|
} else {
|
||||||
|
toggleSelection?.(metric.metricId);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,7 @@ function MetricsList({
|
||||||
metricStore.updateKey('showMine', !showOwn);
|
metricStore.updateKey('showMine', !showOwn);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Define dimensions for the empty state illustration
|
|
||||||
const isFiltered =
|
const isFiltered =
|
||||||
metricsSearch !== '' || (metricStore.filter.type && metricStore.filter.type !== 'all');
|
metricsSearch !== '' || (metricStore.filter.type && metricStore.filter.type !== 'all');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ function LongLoader({ onClick }: { onClick: () => void }) {
|
||||||
'0%': '#394EFF',
|
'0%': '#394EFF',
|
||||||
'100%': '#394EFF'
|
'100%': '#394EFF'
|
||||||
}}
|
}}
|
||||||
|
status="active"
|
||||||
showInfo={false}
|
showInfo={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -196,14 +196,14 @@ const PathAnalysisFilter = observer(({ metric, writeOption }: any) => {
|
||||||
{ value: 'custom', label: 'Custom Events' },
|
{ value: 'custom', label: 'Custom Events' },
|
||||||
];
|
];
|
||||||
return (
|
return (
|
||||||
<Card styles={{ body: { padding: '20px 20px' } }} className="rounded-lg">
|
<div className="rounded-lg bg-white border">
|
||||||
<div className='flex flex-col lg:flex-row lg:items-center gap-6 flex-wrap'>
|
<div className='flex flex-col justify-start gap-2 flex-wrap'>
|
||||||
<Form.Item className='mb-0 flex-1'>
|
<Form.Item className='mb-0 hover:bg-bg-blue/30 px-4 pb-1 pt-2'>
|
||||||
<div className="flex flex-wrap gap-2 items-center justify-start">
|
<div className="flex flex-wrap gap-2 items-center justify-start">
|
||||||
<span className="font-medium">Journeys with: </span>
|
<span className="font-medium">Journeys With </span>
|
||||||
<div className="flex gap-2 items-start">
|
<div className="flex gap-2 items-center">
|
||||||
<Select
|
<Select
|
||||||
className="w-36 rounded-lg"
|
className="w-36 rounded-lg !h-[26px]"
|
||||||
name="startType"
|
name="startType"
|
||||||
options={[
|
options={[
|
||||||
{ value: 'start', label: 'Start Point' },
|
{ value: 'start', label: 'Start Point' },
|
||||||
|
|
@ -215,11 +215,11 @@ const PathAnalysisFilter = observer(({ metric, writeOption }: any) => {
|
||||||
size="small"
|
size="small"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<span className="text-neutral-400 mt-.5">showing</span>
|
<span className="">showing</span>
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
mode="multiple"
|
mode="multiple"
|
||||||
className="rounded-lg w-max min-w-44 max-w-58"
|
className="rounded-lg h-[26px] w-max min-w-44 max-w-58"
|
||||||
// style={{ width: 'auto', minWidth: '9rem', maxWidth: '12rem' }}
|
// style={{ width: 'auto', minWidth: '9rem', maxWidth: '12rem' }}
|
||||||
allowClear
|
allowClear
|
||||||
name="metricValue"
|
name="metricValue"
|
||||||
|
|
@ -234,7 +234,7 @@ const PathAnalysisFilter = observer(({ metric, writeOption }: any) => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item className='mb-0 flex-1'>
|
<Form.Item className='mb-0 hover:bg-bg-blue/30 px-4 pb-2 pt-1'>
|
||||||
<div className="flex flex-wrap items-center justify-start">
|
<div className="flex flex-wrap items-center justify-start">
|
||||||
<span className="font-medium mr-2">{
|
<span className="font-medium mr-2">{
|
||||||
metric.startType === 'start'
|
metric.startType === 'start'
|
||||||
|
|
@ -258,7 +258,7 @@ const PathAnalysisFilter = observer(({ metric, writeOption }: any) => {
|
||||||
</div>
|
</div>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { useRef, useState, useEffect } from 'react';
|
import React, { useRef, useState, useEffect } from 'react';
|
||||||
import { Button, Checkbox, Input, Tooltip } from 'antd';
|
import { Button, Checkbox, Input, Tooltip } from 'antd';
|
||||||
import { RedoOutlined } from '@ant-design/icons';
|
import { RedoOutlined, CloseCircleFilled } from '@ant-design/icons';
|
||||||
import cn from 'classnames';
|
import cn from 'classnames';
|
||||||
import { Loader } from 'UI';
|
import { Loader } from 'UI';
|
||||||
import OutsideClickDetectingDiv from '../../OutsideClickDetectingDiv';
|
import OutsideClickDetectingDiv from '../../OutsideClickDetectingDiv';
|
||||||
|
|
@ -83,7 +83,9 @@ export function AutocompleteModal({
|
||||||
|
|
||||||
const applyQuery = () => {
|
const applyQuery = () => {
|
||||||
const vals = commaQuery ? query.split(',').map((i) => i.trim()) : [query];
|
const vals = commaQuery ? query.split(',').map((i) => i.trim()) : [query];
|
||||||
onApply(vals);
|
// onApply(vals);
|
||||||
|
const merged = Array.from(new Set([...selectedValues, ...vals]));
|
||||||
|
onApply(merged);
|
||||||
};
|
};
|
||||||
|
|
||||||
const clearSelection = () => {
|
const clearSelection = () => {
|
||||||
|
|
@ -134,6 +136,7 @@ export function AutocompleteModal({
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
className="rounded-lg"
|
className="rounded-lg"
|
||||||
autoFocus
|
autoFocus
|
||||||
|
allowClear
|
||||||
/>
|
/>
|
||||||
<Loader loading={isLoading}>
|
<Loader loading={isLoading}>
|
||||||
<>
|
<>
|
||||||
|
|
@ -159,7 +162,7 @@ export function AutocompleteModal({
|
||||||
<div className={'border-y border-y-gray-light py-2'}>
|
<div className={'border-y border-y-gray-light py-2'}>
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
'whitespace-normal rounded cursor-pointer text-blue hover:bg-active-blue px-2 py-1'
|
'whitespace-normal rounded cursor-pointer text-teal hover:bg-active-blue px-2 py-1'
|
||||||
}
|
}
|
||||||
onClick={applyQuery}
|
onClick={applyQuery}
|
||||||
>
|
>
|
||||||
|
|
@ -174,10 +177,6 @@ export function AutocompleteModal({
|
||||||
<Button type="primary" onClick={applyValues} className="btn-apply-event-value">
|
<Button type="primary" onClick={applyValues} className="btn-apply-event-value">
|
||||||
Apply
|
Apply
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button onClick={onClose} className="btn-cancel-event-value">
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Tooltip title='Clear all selection'>
|
<Tooltip title='Clear all selection'>
|
||||||
|
|
@ -191,7 +190,7 @@ export function AutocompleteModal({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Props interface
|
|
||||||
interface Props {
|
interface Props {
|
||||||
value: string[];
|
value: string[];
|
||||||
params?: any;
|
params?: any;
|
||||||
|
|
@ -202,15 +201,34 @@ interface Props {
|
||||||
mapValues?: (value: string) => string;
|
mapValues?: (value: string) => string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// AutoCompleteContainer component
|
|
||||||
export function AutoCompleteContainer(props: Props) {
|
export function AutoCompleteContainer(props: Props) {
|
||||||
const filterValueContainer = useRef<HTMLDivElement>(null);
|
const filterValueContainer = useRef<HTMLDivElement>(null);
|
||||||
const [showValueModal, setShowValueModal] = useState(false);
|
const [showValueModal, setShowValueModal] = useState(false);
|
||||||
|
const [hovered, setHovered] = useState(false);
|
||||||
const isEmpty = props.value.length === 0 || !props.value[0];
|
const isEmpty = props.value.length === 0 || !props.value[0];
|
||||||
const onClose = () => setShowValueModal(false);
|
const onClose = () => setShowValueModal(false);
|
||||||
const onApply = (values: string[]) => {
|
const onApply = (values: string[]) => {
|
||||||
props.onApplyValues(values);
|
setTimeout(() => {
|
||||||
|
props.onApplyValues(values);
|
||||||
|
setShowValueModal(false);
|
||||||
|
}, 100);
|
||||||
|
console.log("closed on apply");
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const onClearClick = (event: React.MouseEvent) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
props.onApplyValues([]);
|
||||||
setShowValueModal(false);
|
setShowValueModal(false);
|
||||||
|
console.log("closed clear click");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleContainerClick = (event: React.MouseEvent) => {
|
||||||
|
if (event.target === event.currentTarget ||
|
||||||
|
event.currentTarget.contains(event.target as Node)) {
|
||||||
|
setShowValueModal(true);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -220,10 +238,13 @@ export function AutoCompleteContainer(props: Props) {
|
||||||
}
|
}
|
||||||
style={{ height: 26 }}
|
style={{ height: 26 }}
|
||||||
ref={filterValueContainer}
|
ref={filterValueContainer}
|
||||||
|
onMouseEnter={() => setHovered(true)}
|
||||||
|
onMouseLeave={() => setHovered(false)}
|
||||||
|
onClick={handleContainerClick}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
onClick={() => setTimeout(() => setShowValueModal(true), 0)}
|
onClick={() => setTimeout(() => setShowValueModal(true), 0)}
|
||||||
className={'flex items-center gap-2 cursor-pointer'}
|
className={'flex items-center gap-2 cursor-pointer pr-4'}
|
||||||
>
|
>
|
||||||
{!isEmpty ? (
|
{!isEmpty ? (
|
||||||
<>
|
<>
|
||||||
|
|
@ -240,7 +261,7 @@ export function AutoCompleteContainer(props: Props) {
|
||||||
/>
|
/>
|
||||||
{props.value.length > 2 && (
|
{props.value.length > 2 && (
|
||||||
<TruncatedText
|
<TruncatedText
|
||||||
text={`+ ${props.value.length - 1} More`}
|
text={`+ ${props.value.length - 2} More`}
|
||||||
maxWidth="8rem"
|
maxWidth="8rem"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
@ -252,6 +273,14 @@ export function AutoCompleteContainer(props: Props) {
|
||||||
{props.placeholder ? props.placeholder : 'Select value(s)'}
|
{props.placeholder ? props.placeholder : 'Select value(s)'}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{!isEmpty && hovered && (
|
||||||
|
<div
|
||||||
|
className="absolute right-2 cursor-pointer flex items-center justify-center"
|
||||||
|
onClick={onClearClick}
|
||||||
|
>
|
||||||
|
<CloseCircleFilled className='text-neutral-200' />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{showValueModal ? (
|
{showValueModal ? (
|
||||||
<props.modalRenderer
|
<props.modalRenderer
|
||||||
|
|
|
||||||
|
|
@ -109,7 +109,7 @@ function FilterValue(props: Props) {
|
||||||
placeholder={filter.placeholder}
|
placeholder={filter.placeholder}
|
||||||
options={filter.options}
|
options={filter.options}
|
||||||
onApplyValues={onApplyValues}
|
onApplyValues={onApplyValues}
|
||||||
// onChange={(item, index) => onChange(null, { value: item.value }, index)}
|
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
@ -179,7 +179,7 @@ function FilterValue(props: Props) {
|
||||||
id={`ignore-outside`}
|
id={`ignore-outside`}
|
||||||
className={cn('grid gap-3 w-fit flex-wrap my-1.5', {
|
className={cn('grid gap-3 w-fit flex-wrap my-1.5', {
|
||||||
'grid-cols-2': filter.hasSource,
|
'grid-cols-2': filter.hasSource,
|
||||||
//'lg:grid-cols-3': !filter.hasSource,
|
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{renderValueFiled(filter.value)}
|
{renderValueFiled(filter.value)}
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,7 @@ export default class MetricService {
|
||||||
}
|
}
|
||||||
const path = isSaved ? `/cards/${metric.metricId}/chart` : `/cards/try`;
|
const path = isSaved ? `/cards/${metric.metricId}/chart` : `/cards/try`;
|
||||||
if (metric.metricType === USER_PATH) {
|
if (metric.metricType === USER_PATH) {
|
||||||
data.density = 3;
|
data.density = 5;
|
||||||
data.metricOf = 'sessionCount';
|
data.metricOf = 'sessionCount';
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -123,10 +123,25 @@
|
||||||
box-shadow: 1px 1px 1px 1px rgba(0, 0, 0 0.3);
|
box-shadow: 1px 1px 1px 1px rgba(0, 0, 0 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.rounded-sm .ant-select-selector{
|
||||||
|
border-radius: .25rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.rounded-lg .ant-select-selector{
|
||||||
|
border-radius: .5rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.rounded-xl .ant-select-selector{
|
||||||
|
border-radius: .75rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
.active-bg {
|
.active-bg {
|
||||||
background-color: $active-blue;
|
background-color: $active-blue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.border-b-light {
|
.border-b-light {
|
||||||
border-bottom: solid thin $gray-light;
|
border-bottom: solid thin $gray-light;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue