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 { sankeyTooltip, getEventPriority, getNodeName } from './sankeyUtils';
|
||||
import { NoContent } from 'App/components/ui';
|
||||
import {InfoCircleOutlined} from '@ant-design/icons';
|
||||
import { InfoCircleOutlined } from '@ant-design/icons';
|
||||
import { X } from 'lucide-react';
|
||||
|
||||
echarts.use([SankeyChart]);
|
||||
|
||||
interface SankeyNode {
|
||||
|
|
@ -15,9 +16,9 @@ interface SankeyNode {
|
|||
}
|
||||
|
||||
interface SankeyLink {
|
||||
source: number;
|
||||
target: number;
|
||||
value: number;
|
||||
source: number | string;
|
||||
target: number | string;
|
||||
value: number;
|
||||
sessionsCount: number;
|
||||
eventType?: string;
|
||||
}
|
||||
|
|
@ -34,6 +35,42 @@ interface Props {
|
|||
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 { data, height = 240, onChartClick, isUngrouped } = props;
|
||||
const chartRef = React.useRef<HTMLDivElement>(null);
|
||||
|
|
@ -44,31 +81,53 @@ const EChartsSankey: React.FC<Props> = (props) => {
|
|||
style={{ minHeight: height }}
|
||||
title={
|
||||
<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.
|
||||
</div>
|
||||
|
||||
}
|
||||
show={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
const [finalNodeCount, setFinalNodeCount] = React.useState(data.nodes.length);
|
||||
|
||||
React.useEffect(() => {
|
||||
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 maxDepth = 4;
|
||||
const filteredNodes = data.nodes.filter((n) => (n.depth ?? 0) <= maxDepth);
|
||||
const filteredLinks = data.links.filter((l) => {
|
||||
const sourceNode = data.nodes.find((n) => n.id === l.source);
|
||||
const targetNode = data.nodes.find((n) => n.id === l.target);
|
||||
return (sourceNode?.depth ?? 0) <= maxDepth && (targetNode?.depth ?? 0) <= maxDepth;
|
||||
const filteredNodes = finalNodes.filter((n) => (n.depth ?? 0) <= maxDepth);
|
||||
const filteredLinks = finalLinks.filter((l) => {
|
||||
const sourceNode = finalNodes.find((n) => n.id === l.source);
|
||||
const targetNode = finalNodes.find((n) => n.id === l.target);
|
||||
return (
|
||||
(sourceNode?.depth ?? 0) <= maxDepth && (targetNode?.depth ?? 0) <= maxDepth
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
const nodeValues = new Array(filteredNodes.length).fill(0);
|
||||
setFinalNodeCount(filteredNodes.length);
|
||||
|
||||
|
||||
const echartNodes = filteredNodes
|
||||
|
|
@ -81,8 +140,9 @@ const EChartsSankey: React.FC<Props> = (props) => {
|
|||
computedName === 'Others'
|
||||
? 'rgba(34,44,154,.9)'
|
||||
: n.eventType === 'DROP'
|
||||
? '#B5B7C8'
|
||||
: '#394eff';
|
||||
? '#B5B7C8'
|
||||
: '#394eff';
|
||||
|
||||
return {
|
||||
name: computedName,
|
||||
depth: n.depth,
|
||||
|
|
@ -93,16 +153,14 @@ const EChartsSankey: React.FC<Props> = (props) => {
|
|||
};
|
||||
})
|
||||
.sort((a, b) => {
|
||||
|
||||
if (a.depth === b.depth) {
|
||||
return getEventPriority(a.type || '') - getEventPriority(b.type || '');
|
||||
} else {
|
||||
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) => ({
|
||||
source: echartNodes.findIndex((n) => n.id === l.source),
|
||||
target: echartNodes.findIndex((n) => n.id === l.target),
|
||||
|
|
@ -111,17 +169,14 @@ const EChartsSankey: React.FC<Props> = (props) => {
|
|||
lineStyle: { opacity: 0.1 },
|
||||
}));
|
||||
|
||||
|
||||
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);
|
||||
});
|
||||
if (echartNodes.length === 0) return;
|
||||
|
||||
|
||||
const startNodeValue = echartLinks
|
||||
.filter((link) => link.source === 0)
|
||||
.reduce((sum, link) => sum + link.value, 0);
|
||||
|
||||
|
||||
const option = {
|
||||
...defaultOptions,
|
||||
tooltip: {
|
||||
|
|
@ -147,31 +202,49 @@ const EChartsSankey: React.FC<Props> = (props) => {
|
|||
label: {
|
||||
show: true,
|
||||
position: 'top',
|
||||
textShadowColor: "transparent",
|
||||
textBorderColor: "transparent",
|
||||
textShadowColor: 'transparent',
|
||||
textBorderColor: 'transparent',
|
||||
align: 'left',
|
||||
overflow: "truncate",
|
||||
overflow: 'truncate',
|
||||
maxWidth: 30,
|
||||
distance: 3,
|
||||
offset: [-20, 0],
|
||||
formatter: function(params: any) {
|
||||
const totalSessions = nodeValues.reduce((sum: number, v: number) => sum + v, 0);
|
||||
const percentage = totalSessions ? ((params.value / totalSessions) * 100).toFixed(1) + '%' : '0%';
|
||||
return `{header|${params.name}}\n{body|${percentage} ${params.value} Sessions}`;
|
||||
formatter: function (params: any) {
|
||||
const nodeVal = params.value;
|
||||
const percentage = startNodeValue
|
||||
? ((nodeVal / startNodeValue) * 100).toFixed(1) + '%'
|
||||
: '0%';
|
||||
|
||||
return (
|
||||
`{header|${params.name}}\n` +
|
||||
`{body|}{percentage|${percentage}} {sessions|${nodeVal}}`
|
||||
);
|
||||
},
|
||||
rich: {
|
||||
header: {
|
||||
fontWeight: 'bold',
|
||||
fontWeight: '600',
|
||||
fontSize: 12,
|
||||
color: '#333'
|
||||
color: '#333',
|
||||
overflow: 'truncate',
|
||||
paddingBottom:'.5rem',
|
||||
},
|
||||
body: {
|
||||
fontSize: 12,
|
||||
color: '#666'
|
||||
}
|
||||
}
|
||||
color: '#000',
|
||||
},
|
||||
percentage: {
|
||||
fontSize: 12,
|
||||
color: '#454545',
|
||||
},
|
||||
sessions: {
|
||||
fontSize: 12,
|
||||
fontFamily: "mono, 'monospace', sans-serif",
|
||||
color: '#999999',
|
||||
},
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
formatter: sankeyTooltip(echartNodes, nodeValues),
|
||||
formatter: sankeyTooltip(echartNodes, []),
|
||||
},
|
||||
nodeAlign: 'left',
|
||||
nodeWidth: 40,
|
||||
|
|
@ -189,8 +262,10 @@ const EChartsSankey: React.FC<Props> = (props) => {
|
|||
],
|
||||
};
|
||||
|
||||
|
||||
chart.setOption(option);
|
||||
|
||||
|
||||
function getUpstreamNodes(nodeIdx: number, visited = new Set<number>()) {
|
||||
if (visited.has(nodeIdx)) return;
|
||||
visited.add(nodeIdx);
|
||||
|
|
@ -231,7 +306,7 @@ const EChartsSankey: React.FC<Props> = (props) => {
|
|||
const baseOpacity = connectedChain.has(idx) ? 1 : 0.35;
|
||||
const extraStyle =
|
||||
idx === hoveredIndex
|
||||
? { borderColor: '#000000', borderWidth: 1, borderType: 'dotted' }
|
||||
? { borderColor: '#000', borderWidth: 1, borderType: 'dotted' }
|
||||
: {};
|
||||
return {
|
||||
...node,
|
||||
|
|
@ -267,7 +342,6 @@ const EChartsSankey: React.FC<Props> = (props) => {
|
|||
|
||||
chart.on('mouseout', function (params: any) {
|
||||
if (params.dataType === 'node') {
|
||||
// Restore original styles on mouseout.
|
||||
chart.setOption({
|
||||
series: [
|
||||
{
|
||||
|
|
@ -279,21 +353,21 @@ const EChartsSankey: React.FC<Props> = (props) => {
|
|||
}
|
||||
});
|
||||
|
||||
|
||||
chart.on('click', function (params: any) {
|
||||
if (!onChartClick) return;
|
||||
if (params.dataType === 'node') {
|
||||
const nodeIndex = params.dataIndex;
|
||||
// Use filteredNodes here.
|
||||
const node = filteredNodes[nodeIndex];
|
||||
onChartClick([{ node }]);
|
||||
} else if (params.dataType === 'edge') {
|
||||
const linkIndex = params.dataIndex;
|
||||
// Use filteredLinks here.
|
||||
const link = filteredLinks[linkIndex];
|
||||
onChartClick([{ link }]);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
const ro = new ResizeObserver(() => chart.resize());
|
||||
ro.observe(chartRef.current);
|
||||
|
||||
|
|
@ -303,11 +377,29 @@ const EChartsSankey: React.FC<Props> = (props) => {
|
|||
};
|
||||
}, [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;
|
||||
|
|
@ -65,12 +65,12 @@ export const tabItems: Record<string, TabItem[]> = {
|
|||
type: USER_PATH,
|
||||
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} />,
|
||||
title: 'Heatmaps',
|
||||
|
|
@ -143,12 +143,12 @@ export const tabItems: Record<string, TabItem[]> = {
|
|||
type: FilterKey.USER_DEVICE,
|
||||
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) {
|
||||
cardData.series = [];
|
||||
cardData.series.push(new FilterSeries());
|
||||
|
|
@ -317,7 +317,7 @@ const AddCardSection = observer(
|
|||
return (
|
||||
<div
|
||||
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'}>
|
||||
|
|
|
|||
|
|
@ -140,12 +140,11 @@ const ListView: React.FC<Props> = ({
|
|||
);
|
||||
|
||||
const onItemClick = (metric: Widget) => {
|
||||
if (disableSelection) return;
|
||||
if (toggleSelection) {
|
||||
toggleSelection(metric.metricId);
|
||||
} else {
|
||||
if (disableSelection) {
|
||||
const path = withSiteId(`/metrics/${metric.metricId}`, siteId);
|
||||
history.push(path);
|
||||
} else {
|
||||
toggleSelection?.(metric.metricId);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ function MetricsList({
|
|||
metricStore.updateKey('showMine', !showOwn);
|
||||
};
|
||||
|
||||
// Define dimensions for the empty state illustration
|
||||
|
||||
const isFiltered =
|
||||
metricsSearch !== '' || (metricStore.filter.type && metricStore.filter.type !== 'all');
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ function LongLoader({ onClick }: { onClick: () => void }) {
|
|||
'0%': '#394EFF',
|
||||
'100%': '#394EFF'
|
||||
}}
|
||||
status="active"
|
||||
showInfo={false}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -196,14 +196,14 @@ const PathAnalysisFilter = observer(({ metric, writeOption }: any) => {
|
|||
{ value: 'custom', label: 'Custom Events' },
|
||||
];
|
||||
return (
|
||||
<Card styles={{ body: { padding: '20px 20px' } }} className="rounded-lg">
|
||||
<div className='flex flex-col lg:flex-row lg:items-center gap-6 flex-wrap'>
|
||||
<Form.Item className='mb-0 flex-1'>
|
||||
<div className="rounded-lg bg-white border">
|
||||
<div className='flex flex-col justify-start gap-2 flex-wrap'>
|
||||
<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">
|
||||
<span className="font-medium">Journeys with: </span>
|
||||
<div className="flex gap-2 items-start">
|
||||
<span className="font-medium">Journeys With </span>
|
||||
<div className="flex gap-2 items-center">
|
||||
<Select
|
||||
className="w-36 rounded-lg"
|
||||
className="w-36 rounded-lg !h-[26px]"
|
||||
name="startType"
|
||||
options={[
|
||||
{ value: 'start', label: 'Start Point' },
|
||||
|
|
@ -215,11 +215,11 @@ const PathAnalysisFilter = observer(({ metric, writeOption }: any) => {
|
|||
size="small"
|
||||
/>
|
||||
|
||||
<span className="text-neutral-400 mt-.5">showing</span>
|
||||
<span className="">showing</span>
|
||||
|
||||
<Select
|
||||
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' }}
|
||||
allowClear
|
||||
name="metricValue"
|
||||
|
|
@ -234,7 +234,7 @@ const PathAnalysisFilter = observer(({ metric, writeOption }: any) => {
|
|||
</div>
|
||||
</div>
|
||||
</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">
|
||||
<span className="font-medium mr-2">{
|
||||
metric.startType === 'start'
|
||||
|
|
@ -258,7 +258,7 @@ const PathAnalysisFilter = observer(({ metric, writeOption }: any) => {
|
|||
</div>
|
||||
</Form.Item>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useRef, useState, useEffect } from 'react';
|
||||
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 { Loader } from 'UI';
|
||||
import OutsideClickDetectingDiv from '../../OutsideClickDetectingDiv';
|
||||
|
|
@ -83,7 +83,9 @@ export function AutocompleteModal({
|
|||
|
||||
const applyQuery = () => {
|
||||
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 = () => {
|
||||
|
|
@ -134,6 +136,7 @@ export function AutocompleteModal({
|
|||
placeholder={placeholder}
|
||||
className="rounded-lg"
|
||||
autoFocus
|
||||
allowClear
|
||||
/>
|
||||
<Loader loading={isLoading}>
|
||||
<>
|
||||
|
|
@ -159,7 +162,7 @@ export function AutocompleteModal({
|
|||
<div className={'border-y border-y-gray-light py-2'}>
|
||||
<div
|
||||
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}
|
||||
>
|
||||
|
|
@ -174,10 +177,6 @@ export function AutocompleteModal({
|
|||
<Button type="primary" onClick={applyValues} className="btn-apply-event-value">
|
||||
Apply
|
||||
</Button>
|
||||
|
||||
<Button onClick={onClose} className="btn-cancel-event-value">
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Tooltip title='Clear all selection'>
|
||||
|
|
@ -191,7 +190,7 @@ export function AutocompleteModal({
|
|||
);
|
||||
}
|
||||
|
||||
// Props interface
|
||||
|
||||
interface Props {
|
||||
value: string[];
|
||||
params?: any;
|
||||
|
|
@ -202,15 +201,34 @@ interface Props {
|
|||
mapValues?: (value: string) => string;
|
||||
}
|
||||
|
||||
// AutoCompleteContainer component
|
||||
|
||||
export function AutoCompleteContainer(props: Props) {
|
||||
const filterValueContainer = useRef<HTMLDivElement>(null);
|
||||
const [showValueModal, setShowValueModal] = useState(false);
|
||||
const [hovered, setHovered] = useState(false);
|
||||
const isEmpty = props.value.length === 0 || !props.value[0];
|
||||
const onClose = () => setShowValueModal(false);
|
||||
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);
|
||||
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 (
|
||||
|
|
@ -220,10 +238,13 @@ export function AutoCompleteContainer(props: Props) {
|
|||
}
|
||||
style={{ height: 26 }}
|
||||
ref={filterValueContainer}
|
||||
onMouseEnter={() => setHovered(true)}
|
||||
onMouseLeave={() => setHovered(false)}
|
||||
onClick={handleContainerClick}
|
||||
>
|
||||
<div
|
||||
onClick={() => setTimeout(() => setShowValueModal(true), 0)}
|
||||
className={'flex items-center gap-2 cursor-pointer'}
|
||||
className={'flex items-center gap-2 cursor-pointer pr-4'}
|
||||
>
|
||||
{!isEmpty ? (
|
||||
<>
|
||||
|
|
@ -240,7 +261,7 @@ export function AutoCompleteContainer(props: Props) {
|
|||
/>
|
||||
{props.value.length > 2 && (
|
||||
<TruncatedText
|
||||
text={`+ ${props.value.length - 1} More`}
|
||||
text={`+ ${props.value.length - 2} More`}
|
||||
maxWidth="8rem"
|
||||
/>
|
||||
)}
|
||||
|
|
@ -252,6 +273,14 @@ export function AutoCompleteContainer(props: Props) {
|
|||
{props.placeholder ? props.placeholder : 'Select value(s)'}
|
||||
</div>
|
||||
)}
|
||||
{!isEmpty && hovered && (
|
||||
<div
|
||||
className="absolute right-2 cursor-pointer flex items-center justify-center"
|
||||
onClick={onClearClick}
|
||||
>
|
||||
<CloseCircleFilled className='text-neutral-200' />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{showValueModal ? (
|
||||
<props.modalRenderer
|
||||
|
|
|
|||
|
|
@ -109,7 +109,7 @@ function FilterValue(props: Props) {
|
|||
placeholder={filter.placeholder}
|
||||
options={filter.options}
|
||||
onApplyValues={onApplyValues}
|
||||
// onChange={(item, index) => onChange(null, { value: item.value }, index)}
|
||||
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
|
@ -179,7 +179,7 @@ function FilterValue(props: Props) {
|
|||
id={`ignore-outside`}
|
||||
className={cn('grid gap-3 w-fit flex-wrap my-1.5', {
|
||||
'grid-cols-2': filter.hasSource,
|
||||
//'lg:grid-cols-3': !filter.hasSource,
|
||||
|
||||
})}
|
||||
>
|
||||
{renderValueFiled(filter.value)}
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ export default class MetricService {
|
|||
}
|
||||
const path = isSaved ? `/cards/${metric.metricId}/chart` : `/cards/try`;
|
||||
if (metric.metricType === USER_PATH) {
|
||||
data.density = 3;
|
||||
data.density = 5;
|
||||
data.metricOf = 'sessionCount';
|
||||
}
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -123,10 +123,25 @@
|
|||
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 {
|
||||
background-color: $active-blue;
|
||||
}
|
||||
|
||||
|
||||
.border-b-light {
|
||||
border-bottom: solid thin $gray-light;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue