Various improvements in graphs, and analytics pages. (#2908)

* Various improvements Cards, OmniSearch and Cards  Listing

* Improved cards listing page

* Various improvements in product analytics

* Charts UI improvements

* ui crash

* Chart improvements and layout toggling

* Various improvements

* Tooltips

---------

Co-authored-by: nick-delirium <nikita@openreplay.com>
This commit is contained in:
Sudheer Salavadi 2024-12-24 04:44:24 -05:00 committed by GitHub
parent 48038b4fc6
commit 488dfcd849
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 554 additions and 364 deletions

View file

@ -1,4 +1,4 @@
import React from 'react';
import React, { useState } from 'react';
import CustomTooltip from "./CustomChartTooltip";
import { Styles } from '../common';
import {
@ -33,15 +33,43 @@ function CustomAreaChart(props: Props) {
inGrid,
} = props;
const [hoveredSeries, setHoveredSeries] = useState<string | null>(null);
const handleMouseOver = (key: string) => () => {
setHoveredSeries(key);
};
const handleMouseLeave = () => {
setHoveredSeries(null);
};
// Dynamically reorder namesMap to render hovered series last
const reorderedNamesMap = hoveredSeries
? [...data.namesMap.filter((key) => key !== hoveredSeries), hoveredSeries]
: data.namesMap;
return (
<ResponsiveContainer height={240} width="100%">
<AreaChart
data={data.chart}
margin={Styles.chartMargins}
onClick={onClick}
onMouseLeave={handleMouseLeave} // Reset hover state on mouse leave
>
{!hideLegend && (
<Legend iconType={'circle'} wrapperStyle={{ top: inGrid ? undefined : -18 }} />
<Legend
iconType={'wye'}
className='font-normal'
wrapperStyle={{ top: inGrid ? undefined : -18 }}
payload={
data.namesMap.map((key, index) => ({
value: key,
type: 'line',
color: colors[index],
id: key,
}))
}
/>
)}
<CartesianGrid
strokeDasharray="1 3"
@ -58,25 +86,45 @@ function CustomAreaChart(props: Props) {
value: label || 'Number of Sessions',
}}
/>
<Tooltip {...Styles.tooltip} content={CustomTooltip} />
{Array.isArray(data.namesMap) &&
data.namesMap.map((key, index) => (
<Area
key={key}
name={key}
type="linear"
dataKey={key}
stroke={colors[index]}
fill={colors[index]}
fillOpacity={0.3}
legendType={key === 'Total' ? 'none' : 'line'}
dot={false}
// strokeDasharray={'4 3'} FOR COPMARISON ONLY
/>
))}
<Tooltip
{...Styles.tooltip}
content={<CustomTooltip hoveredSeries={hoveredSeries} />} // Pass hoveredSeries to tooltip
/>
{Array.isArray(reorderedNamesMap) &&
reorderedNamesMap.map((key, index) => (
<Area
key={key}
name={key}
type="linear"
dataKey={key}
stroke={colors[data.namesMap.indexOf(key)]} // Match original color
fill={colors[data.namesMap.indexOf(key)]}
fillOpacity={
hoveredSeries && hoveredSeries !== key ? 0.2 : 0.1
} // Adjust opacity for non-hovered lines
strokeOpacity={
hoveredSeries && hoveredSeries !== key ? 0.2 : 1
} // Adjust stroke opacity
legendType={key === 'Total' ? 'none' : 'line'}
dot={false}
activeDot={
hoveredSeries === key
? {
r: 8,
stroke: '#fff',
strokeWidth: 2,
fill: colors[data.namesMap.indexOf(key)],
filter: 'drop-shadow(0px 0px 1px rgba(0, 0, 0, 0.2))',
}
: false
} // Show active dot only for the hovered line
onMouseOver={handleMouseOver(key)} // Set hover state on mouse over
style={{ cursor: 'pointer' }}
/>
))}
</AreaChart>
</ResponsiveContainer>
);
}
export default CustomAreaChart;
export default CustomAreaChart;

View file

@ -1,4 +1,4 @@
import React from 'react';
import React, { useState } from 'react';
import CustomTooltip from "./CustomChartTooltip";
import { Styles } from '../common';
import {
@ -71,8 +71,26 @@ function CustomBarChart(props: Props) {
inGrid,
} = props;
const [hoveredSeries, setHoveredSeries] = useState<string | null>(null);
const handleMouseOver = (key) => () => {
setHoveredSeries(key);
};
const handleMouseLeave = () => {
setHoveredSeries(null);
};
const resultChart = data.chart.map((item, i) => {
if (compData && compData.chart[i]) return { ...compData.chart[i], ...item };
if (compData && compData.chart[i]) {
const comparisonItem: Record<string, any> = {};
Object.keys(compData.chart[i]).forEach(key => {
if (key !== 'time') {
comparisonItem[`${key}_comparison`] = (compData.chart[i] as any)[key];
}
});
return { ...item, ...comparisonItem };
}
return item;
});
@ -84,7 +102,6 @@ function CustomBarChart(props: Props) {
}
}
// Filter out comparison items for legend
const legendItems = mergedNameMap.filter(item => !item.isComp);
return (
@ -93,7 +110,9 @@ function CustomBarChart(props: Props) {
data={resultChart}
margin={Styles.chartMargins}
onClick={onClick}
onMouseLeave={handleMouseLeave}
barSize={10}
style={{ backgroundColor: 'transparent' }}
>
<defs>
<clipPath id="pillClip">
@ -147,15 +166,16 @@ function CustomBarChart(props: Props) {
value: label || 'Number of Sessions',
}}
/>
<Tooltip {...Styles.tooltip} content={CustomTooltip} />
<Tooltip {...Styles.tooltip} content={<CustomTooltip hoveredSeries={hoveredSeries} />} />
{mergedNameMap.map((item) => (
<Bar
key={item.data}
name={item.data}
name={item.isComp ? `${item.data} (Comparison)` : item.data}
type="monotone"
dataKey={item.data}
dataKey={item.isComp ? `${item.data}_comparison` : item.data}
fill={colors[item.index]}
stroke={colors[item.index]}
onMouseOver={handleMouseOver(item.isComp ? `${item.data} (Comparison)` : item.data)}
shape={(barProps: any) => (
<PillBar
{...barProps}
@ -165,6 +185,11 @@ function CustomBarChart(props: Props) {
striped={item.isComp}
/>
)}
fillOpacity={
hoveredSeries &&
hoveredSeries !== item.data &&
hoveredSeries !== `${item.data} (Comparison)` ? 0.2 : 1
}
legendType="rect"
activeBar={
<PillBar

View file

@ -16,7 +16,7 @@ function BigNumChart(props: Props) {
values,
} = props;
return (
<div className={'flex flex-row flex-wrap gap-2 -mt-6'} style={{ height: 240 }}>
<div className={'flex flex-row flex-wrap gap-2'} style={{ height: 240 }}>
{values.map((val, i) => (
<BigNum
key={i}
@ -49,7 +49,7 @@ function BigNum({ color, series, value, label, compData, valueLabel }: {
return `${(((value - compData) / compData) * 100).toFixed(2)}%`;
}, [value, compData])
return (
<div className={'flex flex-col flex-auto justify-center items-center hover:bg-teal/5'}>
<div className={'flex flex-col flex-auto justify-center items-center rounded-lg transition-all hover:transition-all ease-in-out hover:ease-in-out hover:bg-teal/5 hover:cursor-pointer'}>
<div className={'flex items-center gap-2 font-medium text-gray-darkest'}>
<div className={'rounded w-4 h-4'} style={{ background: color }} />
<div>{series}</div>

View file

@ -16,27 +16,32 @@ interface Props {
active: boolean;
payload: PayloadItem[];
label: string;
hoveredSeries: string | null;
hoveredSeries?: string | null;
}
function CustomTooltip(props: Props) {
const { active, payload, label, hoveredSeries } = props;
const { active, payload, label, hoveredSeries = null } = props;
// Return null if tooltip is not active or there is no valid payload
if (!active || !payload?.length || !hoveredSeries) return null;
// Find the current and comparison payloads
const currentPayload = payload.find(p => p.name === hoveredSeries);
const comparisonPayload = payload.find(p =>
p.name === `${hoveredSeries.replace(' (Comparison)', '')} (Comparison)` ||
p.name === `${hoveredSeries} (Comparison)`
const currentPayload = payload.find((p) => p.name === hoveredSeries);
const comparisonPayload = payload.find(
(p) =>
p.name === `${hoveredSeries.replace(' (Comparison)', '')} (Comparison)` ||
p.name === `${hoveredSeries} (Comparison)`
);
if (!currentPayload) return null;
// Create transformed array with comparison data
const transformedArray = [{
...currentPayload,
prevValue: comparisonPayload ? comparisonPayload.value : null
}];
const transformedArray = [
{
...currentPayload,
prevValue: comparisonPayload ? comparisonPayload.value : null,
},
];
const isHigher = (item: { value: number; prevValue: number }) =>
item.prevValue !== null && item.prevValue < item.value;
@ -45,34 +50,36 @@ function CustomTooltip(props: Props) {
(((val - prevVal) / prevVal) * 100).toFixed(2);
return (
<div className={'flex flex-col gap-1 bg-white shadow border rounded p-2 z-50'}>
<div className="flex flex-col gap-1 bg-white shadow border rounded p-2 z-50">
{transformedArray.map((p, index) => (
<React.Fragment key={p.name + index}>
<div className={'flex gap-2 items-center'}>
<div className="flex gap-2 items-center">
<div
style={{ borderRadius: 99, background: p.color }}
className={'h-5 w-5 flex items-center justify-center'}
className="h-5 w-5 flex items-center justify-center"
>
<div className={'invert text-sm'}>{index + 1}</div>
<div className="invert text-sm">{index + 1}</div>
</div>
<div className={'font-medium'}>{p.name}</div>
<div className="font-medium">{p.name}</div>
</div>
<div
style={{ borderLeft: `2px solid ${p.color}` }}
className={'flex flex-col px-2 ml-2 '}
className="flex flex-col px-2 ml-2"
>
<div className={'text-neutral-600 text-sm'}>
{label}, {formatTimeOrDate(p.payload.timestamp)}
<div className="text-neutral-600 text-sm">
{label},{' '}
{p.payload?.timestamp
? formatTimeOrDate(p.payload.timestamp)
: <div className='hidden'>'Timestamp is not Applicable'</div>}
</div>
<div className={'flex items-center gap-1'}>
<div className={'font-medium'}>{p.value}</div>
{p.prevValue !== null && (
<CompareTag
isHigher={isHigher(p)}
absDelta={Math.abs(p.value - p.prevValue)}
delta={getPercentDelta(p.value, p.prevValue)}
/>
)}
<div className="flex items-center gap-1">
<div className="font-medium">{p.value}</div>
<CompareTag
isHigher={isHigher(p)}
absDelta={Math.abs(p.value - p.prevValue)}
delta={getPercentDelta(p.value, p.prevValue)}
/>
</div>
</div>
</React.Fragment>
@ -86,20 +93,30 @@ export function CompareTag({
absDelta,
delta,
}: {
isHigher: boolean;
absDelta?: number | string;
delta?: string;
isHigher: boolean | null; // Allow null for default view
absDelta?: number | string | null;
delta?: string | null;
}) {
return (
<div
className={cn(
'px-2 py-1 w-fit rounded flex items-center gap-1',
isHigher ? 'bg-green2/10 text-xs' : 'bg-red2/10 text-xs'
isHigher === null
? 'bg-neutral-200 text-neutral-600 text-xs'
: isHigher
? 'bg-green2/10 text-xs'
: 'bg-red2/10 text-xs'
)}
>
{!isHigher ? <ArrowDown size={12} /> : <ArrowUp size={12} />}
<div>{absDelta}</div>
<div>({delta}%)</div>
{isHigher === null ? (
<div>No Comparison</div>
) : (
<>
{!isHigher ? <ArrowDown size={12} /> : <ArrowUp size={12} />}
<div>{absDelta}</div>
<div>({delta}%)</div>
</>
)}
</div>
);
}

View file

@ -98,71 +98,74 @@ function CustomMetricLineChart(props: Props) {
<Tooltip {...Styles.tooltip} content={<CustomTooltip hoveredSeries={hoveredSeries} />} />
{Array.isArray(data.namesMap) &&
data.namesMap.map((key, index) =>
key ? (
<Line
key={key}
name={key}
type="linear"
dataKey={key}
stroke={colors[index]}
fillOpacity={1}
strokeWidth={2}
strokeOpacity={hoveredSeries && hoveredSeries !== key ? 0.2 : 1}
legendType={key === 'Total' ? 'none' : 'line'}
dot={false}
activeDot={
hoveredSeries === key
? {
r: 8,
stroke: '#fff',
strokeWidth: 2,
fill: colors[index],
filter: 'drop-shadow(0px 0px 1px rgba(0, 0, 0, 0.2))',
}
: false
}
onMouseOver={handleMouseOver(key)}
style={{ cursor: 'pointer' }}
animationDuration={1000}
animationEasing="ease-in-out"
/>
) : null
)}
{compData?.namesMap?.map((key, i) =>
data.namesMap[i] ? (
<Line
key={key}
name={`${key} (Comparison)`}
type="linear"
dataKey={`${key}_comparison`}
stroke={colors[i]}
fillOpacity={1}
strokeWidth={2}
strokeOpacity={0.6}
legendType="line"
dot={false}
strokeDasharray="4 2"
onMouseOver={handleMouseOver(`${key} (Comparison)`)}
activeDot={
hoveredSeries === `${key} (Comparison)`
? {
r: 8,
stroke: '#fff',
strokeWidth: 2,
fill: colors[i],
filter: 'drop-shadow(0px 0px 0px rgba(0, 0, 0, 0.2))',
}
: false
}
style={{ cursor: 'pointer' }}
animationDuration={1000}
animationEasing="ease-in-out"
/>
) : null
)}
data.namesMap.map((key, index) =>
key ? (
<Line
key={key}
name={key}
type="linear"
dataKey={key}
stroke={colors[index]}
fillOpacity={1}
strokeWidth={2}
strokeOpacity={
hoveredSeries && hoveredSeries !== key && hoveredSeries !== `${key} (Comparison)` ? 0.2 : 1
}
legendType={key === 'Total' ? 'none' : 'line'}
dot={false}
activeDot={
hoveredSeries === key
? {
r: 8,
stroke: '#fff',
strokeWidth: 2,
fill: colors[index],
filter: 'drop-shadow(0px 0px 1px rgba(0, 0, 0, 0.2))',
}
: false
}
onMouseOver={handleMouseOver(key)}
style={{ cursor: 'pointer' }}
animationDuration={200}
animationEasing="ease-in-out"
/>
) : null
)}
{compData?.namesMap?.map((key, i) =>
data.namesMap[i] ? (
<Line
key={key}
name={`${key} (Comparison)`}
type="linear"
dataKey={`${key}_comparison`}
stroke={colors[i]}
fillOpacity={1}
strokeWidth={2}
strokeOpacity={
hoveredSeries && hoveredSeries !== key && hoveredSeries !== `${key} (Comparison)` ? 0.2 : 1
}
legendType="line"
dot={false}
strokeDasharray="4 2"
onMouseOver={handleMouseOver(`${key} (Comparison)`)}
activeDot={
hoveredSeries === `${key} (Comparison)`
? {
r: 8,
stroke: '#fff',
strokeWidth: 2,
fill: colors[i],
filter: 'drop-shadow(0px 0px 0px rgba(0, 0, 0, 0.2))',
}
: false
}
style={{ cursor: 'pointer' }}
animationDuration={1000}
animationEasing="ease-in-out"
/>
) : null
)}
</LineChart>
</ResponsiveContainer>
);

View file

@ -1,10 +1,11 @@
import React from 'react';
import React, { useState } from 'react';
import { ResponsiveContainer, Tooltip } from 'recharts';
import { PieChart, Pie, Cell, Legend } from 'recharts';
import { Styles } from '../../common';
import { NoContent } from 'UI';
import { filtersMap } from 'Types/filter/newFilter';
import { numberWithCommas } from 'App/utils';
import CustomTooltip from '../CustomChartTooltip';
interface Props {
metric: {
@ -23,6 +24,8 @@ interface Props {
function CustomMetricPieChart(props: Props) {
const { metric, data, onClick = () => null, inGrid } = props;
const [hoveredSeries, setHoveredSeries] = useState<string | null>(null);
const onClickHandler = (event) => {
if (event && !event.payload.group) {
const filters = Array<any>();
@ -41,19 +44,22 @@ function CustomMetricPieChart(props: Props) {
}
};
const getTotalForSeries = (series: string) => {
return data.chart ? data.chart.reduce((acc, curr) => acc + curr[series], 0) : 0
}
const values = data.namesMap.map((k, i) => {
return {
name: k,
value: getTotalForSeries(k)
}
})
const highest = values.reduce(
(acc, curr) =>
acc.value > curr.value ? acc : curr,
{ name: '', value: 0 });
const handleMouseOver = (name: string) => setHoveredSeries(name);
const handleMouseLeave = () => setHoveredSeries(null);
const getTotalForSeries = (series: string) =>
data.chart ? data.chart.reduce((acc, curr) => acc + curr[series], 0) : 0;
const values = data.namesMap.map((k) => ({
name: k,
value: getTotalForSeries(k),
}));
const highest = values.reduce(
(acc, curr) => (acc.value > curr.value ? acc : curr),
{ name: '', value: 0 }
);
return (
<NoContent
size="small"
@ -63,16 +69,21 @@ function CustomMetricPieChart(props: Props) {
>
<ResponsiveContainer height={240} width="100%">
<PieChart>
<Legend iconType={'triangle'} wrapperStyle={{ top: inGrid ? undefined : -18 }} />
<Legend iconType={'triangle'} wrapperStyle={{ top: inGrid ? undefined : -18 }} />
<Tooltip
content={<CustomTooltip hoveredSeries={hoveredSeries} />}
/>
<Pie
isAnimationActive={false}
data={values}
cx="50%"
cy="50%"
innerRadius={20}
outerRadius={60}
cy="60%"
innerRadius={60}
outerRadius={100}
activeIndex={1}
onClick={onClickHandler}
onMouseOver={({ name }) => handleMouseOver(name)}
onMouseLeave={handleMouseLeave}
labelLine={({
cx,
cy,
@ -89,9 +100,7 @@ function CustomMetricPieChart(props: Props) {
let x1 = cx + radius2 * Math.cos(-midAngle * RADIAN);
let y1 = cy + radius2 * Math.sin(-midAngle * RADIAN);
const percentage =
(value * 100) /
highest.value;
const percentage = (value * 100) / highest.value;
if (percentage < 3) {
return null;
@ -121,9 +130,7 @@ function CustomMetricPieChart(props: Props) {
let radius = 20 + innerRadius + (outerRadius - innerRadius);
let x = cx + radius * Math.cos(-midAngle * RADIAN);
let y = cy + radius * Math.sin(-midAngle * RADIAN);
const percentage =
(value / highest.value) *
100;
const percentage = (value / highest.value) * 100;
let name = values[index].name || 'Unidentified';
name = name.length > 20 ? name.substring(0, 20) + '...' : name;
if (percentage < 3) {
@ -135,7 +142,6 @@ function CustomMetricPieChart(props: Props) {
y={y}
fontWeight="400"
fontSize="12px"
// fontFamily="'Source Sans Pro', 'Roboto', 'Helvetica Neue', 'Helvetica', 'Arial', 'sans-serif'"
textAnchor={x > cx ? 'start' : 'end'}
dominantBaseline="central"
fill="#666"
@ -145,18 +151,17 @@ function CustomMetricPieChart(props: Props) {
);
}}
>
{values && values.map((entry, index) => (
<Cell
key={`cell-${index}`}
fill={Styles.safeColors[index % Styles.safeColors.length]}
/>
))}
{values.map((entry, index) => (
<Cell
key={`cell-${index}`}
fill={Styles.safeColors[index % Styles.safeColors.length]}
/>
))}
</Pie>
<Tooltip {...Styles.tooltip} />
</PieChart>
</ResponsiveContainer>
</NoContent>
);
}
export default CustomMetricPieChart;
export default CustomMetricPieChart;

View file

@ -32,54 +32,90 @@ function ProgressBarChart(props: Props) {
return Intl.NumberFormat().format(num);
}
// we mix 1 original, then 1 comparison, etc
const mergedNameMap: { data: any, isComp: boolean, index: number }[] = [];
// Group the data into pairs (original + comparison)
const groupedData: Array<{ original: any, comparison: any }> = [];
for (let i = 0; i < data.namesMap.length; i++) {
if (!data.namesMap[i]) {
continue;
}
mergedNameMap.push({ data: data.namesMap[i], isComp: false, index: i });
if (compData && compData.namesMap[i]) {
mergedNameMap.push({ data: compData.namesMap[i], isComp: true, index: i });
}
if (!data.namesMap[i]) continue;
const original = {
name: data.namesMap[i],
value: getTotalForSeries(data.namesMap[i], false),
isComp: false,
index: i
};
const comparison = compData && compData.namesMap[i] ? {
name: compData.namesMap[i],
value: getTotalForSeries(compData.namesMap[i], true),
isComp: true,
index: i
} : null;
groupedData.push({ original, comparison });
}
const values = mergedNameMap.map((k, i) => {
return {
name: k.data,
value: getTotalForSeries(k.data, k.isComp),
isComp: k.isComp,
index: k.index,
}
})
const highest = values.reduce(
(acc, curr) =>
acc.value > curr.value ? acc : curr,
{ name: '', value: 0 });
// Find highest value among all data points
const highest = groupedData.reduce((acc, curr) => {
const maxInGroup = Math.max(
curr.original.value,
curr.comparison ? curr.comparison.value : 0
);
return Math.max(acc, maxInGroup);
}, 0);
return (
<div className={'w-full'} style={{ height: 240 }}>
{values.map((val, i) => (
<div key={i} className={'flex items-center gap-1'}>
<div className={'flex items-center'} style={{ flex: 1}}>
<div className={'w-4 h-4 rounded-full mr-2'} style={{ backgroundColor: colors[val.index] }} />
<span>{val.name}</span>
<div className="w-full flex flex-col gap-3 ps-3 justify-center" style={{ height: 240 }}>
{groupedData.map((group, i) => (
<div key={i} className={`flex flex-col ${i < groupedData.length - 1 && group.comparison ? 'border-b border-dashed border-[0,0,0,.15] pb-3' : ''}`}>
<div className="flex items-center">
<div className="flex items-center" style={{ flex: 1 }}>
<div
className="w-4 h-4 rounded-full mr-2"
style={{ backgroundColor: colors[group.original.index] }}
/>
<span>{group.original.name}</span>
</div>
<div className="flex items-center gap-2" style={{ flex: 4 }}>
<div
style={{
height: 8,
borderRadius: 8,
backgroundColor: colors[group.original.index],
width: `${(group.original.value/highest)*100}%`
}}
/>
<div>{formattedNumber(group.original.value)}</div>
</div>
<div style={{ flex: 1 }} />
</div>
<div className={'flex items-center gap-2'} style={{ flex: 4 }}>
<div style={{
height: 16,
borderRadius: 16,
backgroundImage: val.isComp ? `linear-gradient(45deg, #ffffff 25%, ${colors[val.index]} 25%, ${colors[val.index]} 50%, #ffffff 50%, #ffffff 75%, ${colors[val.index]} 75%, ${colors[val.index]} 100%)` : undefined,
backgroundSize: val.isComp ? '20px 20px' : undefined,
backgroundColor: val.isComp ? undefined : colors[val.index],
width: `${(val.value/highest.value)*100}%` }}
/>
<div>{formattedNumber(val.value)}</div>
</div>
<div style={{ flex: 1}}/>
{group.comparison && (
<div className="flex items-center">
<div className="invisible flex items-center" style={{ flex: 1 }}>
<div
className="w-4 h-4 rounded-full mr-2"
style={{ backgroundColor: colors[group.comparison.index] }}
/>
<span>{group.comparison.name}</span>
</div>
<div className="flex items-center gap-2" style={{ flex: 4 }}>
<div
style={{
height: 8,
borderRadius: 8,
backgroundImage: `repeating-linear-gradient(45deg, #ffffff 0px, #ffffff 1.5px, ${colors[group.comparison.index]} 1.5px, ${colors[group.comparison.index]} 4.5px)`,
backgroundSize: '20px 20px',
width: `${(group.comparison.value/highest)*100}%`
}}
/>
<div>{formattedNumber(group.comparison.value)}</div>
</div>
<div style={{ flex: 1 }} />
</div>
)}
</div>
))}
</div>
);
}
export default ProgressBarChart;
export default ProgressBarChart;

View file

@ -16,10 +16,10 @@ const FilterCountLabels = observer(
<Space>
{events > 0 && (
<Button
type="primary"
ghost
type="text"
size="small"
onClick={props.toggleExpand}
className='btn-series-event-count'
>
{`${events} Event${events > 1 ? 's' : ''}`}
</Button>
@ -27,10 +27,10 @@ const FilterCountLabels = observer(
{filters > 0 && (
<Button
type="primary"
ghost
type="text"
size="small"
onClick={props.toggleExpand}
className='btn-series-filter-count'
>
{`${filters} Filter${filters > 1 ? 's' : ''}`}
</Button>
@ -69,23 +69,25 @@ const FilterSeriesHeader = observer(
onUpdate={onUpdate}
onChange={props.onChange}
/>
{!props.expanded && (
</Space>
<Space>
{!props.expanded && (
<FilterCountLabels
filters={props.series.filter.filters}
toggleExpand={props.toggleExpand}
/>
)}
</Space>
<Space>
<Button
onClick={props.onRemove}
size="small"
disabled={!props.canDelete}
icon={<Trash size={14} />}
type='text'
className='btn-delete-series'
/>
className={cn(
'btn-delete-series', 'disabled:hidden'
)}
/>
<Button
onClick={props.toggleExpand}
size="small"

View file

@ -51,13 +51,14 @@ function SeriesName(props: Props) {
onChange={write}
onBlur={onBlur}
onKeyDown={onKeyDown}
className="bg-white text-lg border-transparent rounded-lg font-medium ps-2"
className="bg-white text-lg border-transparent rounded-lg font-medium ps-2 input-rename-series"
maxLength={22}
size='small'
/>
) : (
<Tooltip title="Double click to rename.">
<Tooltip title="Click to rename">
<div
className="text-lg font-medium h-8 flex items-center border-transparent p-2 hover:bg-teal/10 cursor-pointer rounded-lg input-rename-series"
className="text-lg font-medium h-8 flex items-center border-transparent p-2 hover:bg-teal/10 cursor-pointer rounded-lg btn-input-rename-series"
onClick={() => setEditing(true)}
data-event='input-rename-series'
>

View file

@ -176,7 +176,7 @@ const MetricListItem: React.FC<Props> = ({
menu={{ items: menuItems, onClick: onMenuClick }}
trigger={['click']}
>
<Button id={'ignore-prop'} icon={<EllipsisVertical size={16} />} className='btn-cards-list-item-more-options' />
<Button id={'ignore-prop'} icon={<EllipsisVertical size={16} />} className='btn-cards-list-item-more-options' type='text'/>
</Dropdown>
</div>
{renderModal()}

View file

@ -4,6 +4,7 @@ import MetricListItem from '../MetricListItem';
import { TablePaginationConfig, SorterResult } from 'antd/lib/table/interface';
import Widget from 'App/mstore/types/widget';
import { LockOutlined, TeamOutlined } from "@ant-design/icons";
import classNames from 'classnames';
const { Text } = Typography;
@ -172,16 +173,15 @@ const ListView: React.FC<Props> = (props: Props) => {
<div className={'flex items-center justify-start gap-2'}>
<div>Visibility</div>
<Tooltip
title="Toggle to view your own or team's cards."
title="Toggle to view your or team's cards."
placement="topRight"
>
<Switch
checked={!showOwn}
onChange={() =>
toggleOwn()
}
onChange={() => toggleOwn()}
checkedChildren={'Team'}
unCheckedChildren={'Private'}
className={classNames( '!bg-tealx')}
/>
</Tooltip>
</div>

View file

@ -284,6 +284,7 @@ function WidgetChart(props: Props) {
: chartData.namesMap;
if (viewType === 'lineChart') {
return (
<div className='pt-3'>
<CustomMetricLineChart
inGrid={!props.isPreview}
data={chartData}
@ -297,10 +298,12 @@ function WidgetChart(props: Props) {
: 'Number of Users'
}
/>
</div>
);
}
if (viewType === 'areaChart') {
return (
<div className='pt-3'>
<AreaChart
data={chartData}
inGrid={!props.isPreview}
@ -313,10 +316,12 @@ function WidgetChart(props: Props) {
: 'Number of Users'
}
/>
</div>
);
}
if (viewType === 'barChart') {
return (
<div className='pt-3'>
<BarChart
inGrid={!props.isPreview}
data={chartData}
@ -330,8 +335,10 @@ function WidgetChart(props: Props) {
: 'Number of Users'
}
/>
</div>
);
}
if (viewType === 'progressChart') {
return (
<ProgressBarChart
@ -351,6 +358,7 @@ function WidgetChart(props: Props) {
}
if (viewType === 'pieChart') {
return (
<div className='pt-3'>
<CustomMetricPieChart
inGrid={!props.isPreview}
metric={_metric}
@ -363,6 +371,7 @@ function WidgetChart(props: Props) {
: 'Number of Users'
}
/>
</div>
);
}
if (viewType === 'progress') {
@ -507,15 +516,7 @@ function WidgetChart(props: Props) {
return (
<div ref={ref}>
<Loader loading={loading} style={{ height: `240px` }}>
<div
style={{
minHeight: props.isPreview ? undefined : 240,
paddingTop:
props.isPreview && _metric.metricType === TIMESERIES
? '1.5rem'
: 0,
}}
>
<div style={{ minHeight: props.isPreview ? undefined : 240 }}>
{renderChart()}
{props.isPreview && _metric.metricType === TIMESERIES ? (
<WidgetDatatable

View file

@ -1,4 +1,4 @@
import { Button, Table } from 'antd';
import { Button, Table, Divider } from 'antd';
import type { TableProps } from 'antd';
import { Eye, EyeOff } from 'lucide-react';
@ -28,8 +28,7 @@ interface Props {
enabledRows: string[];
setEnabledRows: (rows: string[]) => void;
defaultOpen?: boolean;
metric: { name: string };
isTableView?: boolean;
metric: { name: string; viewType: string };
}
function WidgetDatatable(props: Props) {
@ -112,33 +111,28 @@ function WidgetDatatable(props: Props) {
}),
type: 'checkbox',
};
const isTableOnlyMode = props.metric.viewType === 'table';
return (
<div className={cn('relative -mx-4 px-2', !props.isTableView && showTable ? 'pt-6' : '')}>
{props.isTableView ? null : (
<>
<div
className={cn(
'absolute left-0 right-0 -top-3 border-t border-t-gray-lighter',
{ hidden: !showTable }
)}
/>
<div
className={'absolute -top-3 left-1/2 z-10'}
style={{ transform: 'translate(-50%, -50%)' }}
>
<div className={cn('relative -mx-4 px-2', showTable ? '' : '')}>
{!isTableOnlyMode && (
<div className='flex gap-2'>
<Divider style={{ borderColor: showTable ? '#efefef' : 'transparent', borderStyle: 'dashed'}} variant="dashed">
<Button
icon={showTable ? <EyeOff size={16} /> : <Eye size={16} />}
size={'small'}
type={'default'}
onClick={() => setShowTable(!showTable)}
className="btn-show-hide-table"
className='btn-show-hide-table'
>
{showTable ? 'Hide Table' : 'Show Table'}
</Button>
</div>
</>
</Divider>
</div>
)}
{showTable ? (
{(showTable || isTableOnlyMode) ? (
<div className={'relative pb-2'}>
<Table
columns={tableProps}

View file

@ -60,9 +60,9 @@ function WidgetName(props: Props) {
/>
) : (
// @ts-ignore
<Tooltip delay={200} title="Double click to edit" disabled={!canEdit}>
<Tooltip delay={200} title="Click to edit" disabled={!canEdit}>
<div
onDoubleClick={() => setEditing(true)}
onClick={() => setEditing(true)}
className={cn(
"text-2xl h-8 flex items-center p-2 rounded-lg",
canEdit && 'cursor-pointer select-none ps-2 hover:bg-teal/10'

View file

@ -24,7 +24,7 @@ import {
Library,
ChartColumnBig,
ChartBarBig,
} from 'lucide-react';
} from 'lucide-react';
function WidgetOptions() {
const { metricStore } = useStore();
@ -32,6 +32,7 @@ function WidgetOptions() {
const handleChange = (value: any) => {
metric.update({ metricFormat: value });
metric.updateKey('hasChanged', true);
};
// const hasSeriesTypes = [TIMESERIES, FUNNEL, TABLE].includes(metric.metricType);
@ -54,12 +55,10 @@ function WidgetOptions() {
</a>
)}
{metric.metricType === TIMESERIES ? (
<SeriesTypeOptions metric={metric} />
) : null}
{metric.metricType === TIMESERIES && <SeriesTypeOptions metric={metric} />}
{(metric.metricType === FUNNEL || metric.metricType === TABLE) &&
metric.metricOf != FilterKey.USERID &&
metric.metricOf != FilterKey.ERRORS && (
metric.metricOf !== FilterKey.USERID &&
metric.metricOf !== FilterKey.ERRORS && (
<Dropdown
trigger={['click']}
menu={{
@ -70,6 +69,7 @@ function WidgetOptions() {
],
onClick: (info: { key: string }) => handleChange(info.key),
}}
>
<Button type="text" variant="text" size="small">
{metric.metricFormat === 'sessionCount'
@ -79,9 +79,8 @@ function WidgetOptions() {
</Button>
</Dropdown>
)}
{hasViewTypes ? <WidgetViewTypeOptions metric={metric} /> : null}
{metric.metricType === HEATMAP ? <ClickMapRagePicker /> : null}
{hasViewTypes && <WidgetViewTypeOptions metric={metric} />}
{metric.metricType === HEATMAP && <ClickMapRagePicker />}
</div>
);
}
@ -114,7 +113,7 @@ const SeriesTypeOptions = observer(({ metric }: { metric: any }) => {
})),
onClick: ({ key }: any) => {
metric.updateKey('metricOf', key);
metric.updateKey('hasChanged', true)
metric.updateKey('hasChanged', true);
},
}}
>
@ -137,23 +136,22 @@ const SeriesTypeOptions = observer(({ metric }: { metric: any }) => {
const WidgetViewTypeOptions = observer(({ metric }: { metric: any }) => {
const chartTypes = {
lineChart: 'Line',
barChart: 'Column',
areaChart: 'Area',
barChart: 'Column',
progressChart: 'Vertical Bar',
columnChart: 'Horizontal Bar',
pieChart: 'Pie',
progressChart: 'Bar',
table: 'Table',
metric: 'Metric',
chart: 'Funnel Bar',
columnChart: 'Funnel Column',
table: 'Table',
};
const chartIcons = {
lineChart: <ChartLine size={16} strokeWidth={1} />,
lineChart: <ChartLine size={16} strokeWidth={1} /> ,
barChart: <ChartColumn size={16} strokeWidth={1} />,
areaChart: <ChartArea size={16} strokeWidth={1} />,
pieChart: <ChartPie size={16} strokeWidth={1} />,
progressChart: <ChartBar size={16} strokeWidth={1} />,
table: <Table size={16} strokeWidth={1} />,
metric: <Hash size={16} strokeWidth={1} />,
table: <Table size={16} strokeWidth={1} />,
// funnel specific
columnChart: <ChartColumnBig size={16} strokeWidth={1} />,
chart: <ChartBarBig size={16} strokeWidth={1} />,
@ -161,14 +159,14 @@ const WidgetViewTypeOptions = observer(({ metric }: { metric: any }) => {
const allowedTypes = {
[TIMESERIES]: [
'lineChart',
'barChart',
'areaChart',
'pieChart',
'barChart',
'progressChart',
'table',
'pieChart',
'metric',
'table',
],
[FUNNEL]: ['chart', 'columnChart', 'metric', 'table'],
[FUNNEL]: ['lineChart', 'areaChart', 'barChart', 'progressChart', 'pieChart', 'metric', 'table'],
};
return (
<Dropdown
@ -179,10 +177,8 @@ const WidgetViewTypeOptions = observer(({ metric }: { metric: any }) => {
key,
label: (
<div className="flex gap-2 items-center">
<>
{chartIcons[key]}
<div>{chartTypes[key]}</div>
</>
</div>
),
})),

View file

@ -21,9 +21,11 @@ import {
import CardUserList from '../CardUserList/CardUserList';
import WidgetViewHeader from 'Components/Dashboard/components/WidgetView/WidgetViewHeader';
import WidgetFormNew from 'Components/Dashboard/components/WidgetForm/WidgetFormNew';
import { Space } from 'antd';
import { Space, Segmented, Tooltip } from 'antd';
import { renderClickmapThumbnail } from 'Components/Dashboard/components/WidgetForm/renderMap';
import Widget from 'App/mstore/types/widget';
import { LayoutPanelTop, LayoutPanelLeft } from 'lucide-react';
import classNames from 'classnames';
interface Props {
history: any;
@ -32,6 +34,7 @@ interface Props {
}
function WidgetView(props: Props) {
const [layout, setLayout] = useState('horizontal');
const {
match: {
params: { siteId, dashboardId, metricId },
@ -137,21 +140,57 @@ function WidgetView(props: Props) {
}
>
<Space direction="vertical" className="w-full" size={14}>
<WidgetViewHeader onSave={onSave} undoChanges={undoChanges} />
<WidgetFormNew />
<WidgetPreview name={widget.name} isEditing={expanded} />
<WidgetViewHeader
onSave={onSave}
undoChanges={undoChanges}
layoutControl={
<Segmented
size='small'
value={layout}
onChange={setLayout}
options={[
{
value: 'horizontal',
icon: (
<Tooltip title="Horizontal Layout">
<LayoutPanelLeft size={16} />
</Tooltip>
)
},
{
value: 'vertical',
icon: (
<Tooltip title="Vertical Layout">
<LayoutPanelTop size={16} />
</Tooltip>
)
}
]}
/>
}
/>
<div className={layout === 'horizontal' ? 'flex gap-4' : ''}>
<div className={layout === 'horizontal' ? 'w-1/3 ' : 'w-full'}>
<WidgetFormNew />
</div>
<div className={layout === 'horizontal' ? 'w-2/3' : 'w-full'}>
<WidgetPreview name={widget.name} isEditing={expanded} />
{widget.metricOf !== FilterKey.SESSIONS &&
widget.metricOf !== FilterKey.ERRORS &&
(widget.metricType === TABLE ||
widget.metricType === TIMESERIES ||
widget.metricType === HEATMAP ||
widget.metricType === INSIGHTS ||
widget.metricType === FUNNEL ||
widget.metricType === USER_PATH ? (
<WidgetSessions />
) : null)}
{widget.metricType === RETENTION && <CardUserList />}
{widget.metricOf !== FilterKey.SESSIONS &&
widget.metricOf !== FilterKey.ERRORS &&
(widget.metricType === TABLE ||
widget.metricType === TIMESERIES ||
widget.metricType === HEATMAP ||
widget.metricType === INSIGHTS ||
widget.metricType === FUNNEL ||
widget.metricType === USER_PATH ? (
<WidgetSessions />
) : null)}
{widget.metricType === RETENTION && <CardUserList />}
</div>
</div>
</Space>
</NoContent>
</div>

View file

@ -12,11 +12,13 @@ import copy from 'copy-to-clipboard';
interface Props {
onClick?: () => void;
onSave: () => void;
undoChanges: () => void;
layoutControl?: React.ReactNode;
}
const defaultText = 'Copy link to clipboard'
function WidgetViewHeader({ onClick, onSave }: Props) {
function WidgetViewHeader({ onClick, onSave, layoutControl }: Props) {
const [tooltipText, setTooltipText] = React.useState(defaultText);
const { metricStore } = useStore();
const widget = metricStore.instance;
@ -48,25 +50,25 @@ function WidgetViewHeader({ onClick, onSave }: Props) {
/>
</h1>
<Space>
<Button
type={
metricStore.isSaving || (widget.exists() && !widget.hasChanged) ? 'text' : 'primary'
}
onClick={handleSave}
loading={metricStore.isSaving}
disabled={metricStore.isSaving || (widget.exists() && !widget.hasChanged)}
className='font-medium btn-update-card'
>
{widget.exists() ? 'Update' : 'Create'}
</Button>
<Button
type={
metricStore.isSaving || (widget.exists() && !widget.hasChanged) ? 'text' : 'primary'
}
onClick={handleSave}
loading={metricStore.isSaving}
disabled={metricStore.isSaving || (widget.exists() && !widget.hasChanged)}
className='font-medium btn-update-card'
size='small'
>
{widget.exists() ? 'Update' : 'Create'}
</Button>
{/* <MetricTypeSelector /> */}
<Tooltip title={tooltipText}>
<Button type='text' className='btn-copy-card-url' disabled={!widget.exists()} onClick={copyUrl} icon={<Link2 size={16} strokeWidth={1}/> }></Button>
</Tooltip>
{layoutControl}
<CardViewMenu />
</Space>
</div>

View file

@ -1,9 +1,39 @@
import React, { useRef, useState } from 'react';
import { Button, Checkbox, Input } from 'antd';
import React, { useRef, useState, useEffect } from 'react';
import { Button, Checkbox, Input, Tooltip } from 'antd';
import cn from 'classnames';
import { Loader } from 'UI';
import OutsideClickDetectingDiv from '../../OutsideClickDetectingDiv';
function TruncatedText({ text, maxWidth }: { text?: string; maxWidth?: string;}) {
const textRef = useRef<HTMLDivElement>(null);
const [isTruncated, setIsTruncated] = useState(false);
useEffect(() => {
if (textRef.current) {
setIsTruncated(textRef.current.scrollWidth > textRef.current.offsetWidth);
}
}, [text]);
return (
<Tooltip title={isTruncated ? text : ''}>
<div
ref={textRef}
className="truncate"
style={{
maxWidth,
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
}}
>
{text}
</div>
</Tooltip>
);
}
export function AutocompleteModal({
onClose,
onApply,
@ -51,9 +81,9 @@ export function AutocompleteModal({
};
const applyQuery = () => {
const vals = commaQuery ? query.split(',').map(i => i.trim()) : [query];
const vals = commaQuery ? query.split(',').map((i) => i.trim()) : [query];
onApply(vals);
}
};
const sortedOptions = React.useMemo(() => {
if (values[0] && values[0].length) {
@ -67,20 +97,20 @@ export function AutocompleteModal({
const queryBlocks = commaQuery ? query.split(',') : [query];
const blocksAmount = queryBlocks.length;
// x,y and z
const queryStr = React.useMemo(() => {
let str = ''
let str = '';
queryBlocks.forEach((block, index) => {
if (index === blocksAmount - 1 && blocksAmount > 1) {
str += ' and '
str += ' and ';
}
str += `"${block.trim()}"`
str += `"${block.trim()}"`;
if (index < blocksAmount - 2) {
str += ', '
str += ', ';
}
})
});
return str;
}, [query])
}, [query]);
return (
<OutsideClickDetectingDiv
className={cn(
@ -134,15 +164,18 @@ export function AutocompleteModal({
</>
</Loader>
<div className={'flex gap-2 items-center pt-2'}>
<Button type={'primary'} onClick={applyValues} className='btn-apply-event-value'>
<Button type={'primary'} onClick={applyValues} className="btn-apply-event-value">
Apply
</Button>
<Button onClick={onClose} className='btn-cancel-event-value'>Cancel</Button>
<Button onClick={onClose} className="btn-cancel-event-value">
Cancel
</Button>
</div>
</OutsideClickDetectingDiv>
);
}
// Props interface
interface Props {
value: string[];
params?: any;
@ -153,6 +186,7 @@ interface Props {
mapValues?: (value: string) => string;
}
// AutoCompleteContainer component
export function AutoCompleteContainer(props: Props) {
const filterValueContainer = useRef<HTMLDivElement>(null);
const [showValueModal, setShowValueModal] = useState(false);
@ -177,40 +211,28 @@ export function AutoCompleteContainer(props: Props) {
>
{!isEmpty ? (
<>
<div
className={'rounded-xl bg-gray-lighter leading-none px-1 py-0.5'}
>
{props.mapValues
? props.mapValues(props.value[0])
: props.value[0]}
</div>
{props.value.length > 1 ? (
<TruncatedText
text={props.mapValues ? props.mapValues(props.value[0]) : props.value[0]}
maxWidth="8rem"
/>
{props.value.length > 1 && (
<>
<span className='text-neutral-500/90 '>or</span>
{props.value.length === 2 ? (
<div
className={
'rounded-xl bg-gray-lighter leading-none px-1 py-0.5'
}
>
{props.mapValues
? props.mapValues(props.value[1])
: props.value[1]}
</div>
) : (
<div
className={
'rounded-xl bg-gray-lighter leading-none px-1 py-0.5'
}
>
+ {props.value.length - 1} More
</div>
<span className="text-neutral-500/90">or</span>
<TruncatedText
text={props.mapValues ? props.mapValues(props.value[1]) : props.value[1]}
maxWidth="8rem"
/>
{props.value.length > 2 && (
<TruncatedText
text={`+ ${props.value.length - 1} More`}
maxWidth="8rem"
/>
)}
</>
) : null}
)}
</>
) : (
<div className={'text-neutral-500/90'}>
<div className="text-neutral-500/90">
{props.placeholder ? props.placeholder : 'Select value(s)'}
</div>
)}
@ -226,4 +248,4 @@ export function AutoCompleteContainer(props: Props) {
) : null}
</div>
);
}
}

View file

@ -69,10 +69,10 @@ function FilterItem(props: Props) {
return (
<div className="flex items-center w-full">
<div className="flex items-start w-full">
<div className="flex items-center w-full flex-wrap">
{!isFilter && !hideIndex && filterIndex >= 0 && (
<div
className="mt-1 flex-shrink-0 w-6 h-6 text-xs flex items-center justify-center rounded-full bg-gray-lighter mr-2">
className="flex-shrink-0 w-6 h-6 text-xs flex items-center justify-center rounded-full bg-gray-lighter mr-2">
<span>{filterIndex + 1}</span>
</div>
)}
@ -157,7 +157,7 @@ function FilterItem(props: Props) {
type="text"
onClick={props.onRemoveFilter}
size="small"
className='btn-remove-step'
className='btn-remove-step mt-2'
>
<CircleMinus size={14} />
</Button>

View file

@ -1,7 +1,6 @@
import React from 'react';
import { observer } from 'mobx-react-lite';
import { Tooltip } from 'UI';
import { Dropdown, Button } from 'antd';
import { Dropdown, Button, Tooltip } from 'antd';
const EventsOrder = observer(
(props: { onChange: (e: any, v: any) => void; filter: any }) => {
@ -38,17 +37,17 @@ const EventsOrder = observer(
title="Select the operator to be applied between events."
placement="bottom"
>
<div className="text-neutral-500/90 text-sm font-normal">Events Order</div>
<div className="text-neutral-500/90 text-sm font-normal cursor-default">Events Order</div>
</Tooltip>
<Dropdown
menu={{ items: menuItems, onClick }}
trigger={['click']}
placement="bottomRight"
className="bg-white border font-normal text-sm border-neutral-200 rounded-lg px-1 py-0.5 hover:border-teal btn-events-order"
className="text-sm rounded-lg px-1 py-0.5 btn-events-order "
data-event="btn-events-order"
>
<Button size={'small'}>{selected || 'Select'}</Button>
<Button size={'small'} type='text'>{selected || 'Select'} </Button>
</Dropdown>
</div>
);

View file

@ -49,7 +49,7 @@ export const FilterList = observer((props: Props) => {
<div
className={cn(
'bg-white',
borderless ? '' : 'py-2 px-4 rounded-xl border border-gray-lighter'
borderless ? '' : 'pt-2 px-4 rounded-xl border border-gray-lighter'
)}
style={{
borderBottomLeftRadius: props.mergeDown ? 0 : undefined,
@ -58,7 +58,7 @@ export const FilterList = observer((props: Props) => {
borderTopRightRadius: props.mergeUp ? 0 : undefined,
}}
>
<div className={'flex items-center mb-2'} style={{ gap: '0.65rem' }}>
<div className={'flex items-center py-2'} style={{ gap: '0.65rem' }}>
<div className="font-medium">Filters</div>
<FilterSelection
mode={'filters'}
@ -193,7 +193,7 @@ export const EventsList = observer((props: Props) => {
return (
<div
className={
'border-b border-b-gray-lighter py-2 px-4 rounded-xl bg-white border border-gray-lighter'
'border-b border-b-gray-lighter pt-2 px-4 rounded-xl bg-white border border-gray-lighter'
}
style={{
borderBottomLeftRadius: props.mergeDown ? 0 : undefined,
@ -244,12 +244,12 @@ export const EventsList = observer((props: Props) => {
pointerEvents: 'unset',
paddingTop:
hoveredItem.i === filterIndex && hoveredItem.position === 'top'
? '1.5rem'
: '0.5rem',
? ''
: '',
paddingBottom:
hoveredItem.i === filterIndex && hoveredItem.position === 'bottom'
? '1.5rem'
: '0.5rem',
? ''
: '',
marginLeft: '-1rem',
width: 'calc(100% + 2rem)',
alignItems: 'start',
@ -271,7 +271,7 @@ export const EventsList = observer((props: Props) => {
{!!props.onFilterMove && eventsNum > 1 ? (
<div
className={
'p-2 cursor-grab text-neutral-500/90 hover:bg-white px-1 pt-2 rounded-lg'
'cursor-grab text-neutral-500/90 hover:bg-white px-1 mt-2.5 rounded-lg'
}
draggable={!!props.onFilterMove}
onDragStart={(e) =>

View file

@ -41,7 +41,7 @@ function FilterSelection(props: Props) {
const label = filter?.category === 'Issue' ? 'Issue' : filter?.label;
return (
<div className="relative flex-shrink-0">
<div className="relative flex-shrink-0 my-1.5">
<OutsideClickDetectingDiv
className="relative"
onClickOutside={() => {

View file

@ -178,9 +178,9 @@ function FilterValue(props: Props) {
return (
<div
id={`ignore-outside`}
className={cn('grid gap-3', {
className={cn('grid gap-3 w-fit flex-wrap my-1.5', {
'grid-cols-2': filter.hasSource,
'grid-cols-3': !filter.hasSource,
//'lg:grid-cols-3': !filter.hasSource,
})}
>
{renderValueFiled(filter.value)}

View file

@ -18,7 +18,7 @@ export default function SubFilterItem(props: Props) {
}
return (
<div className="flex items-center hover:bg-active-blue pb-4">
<div className="flex items-center hover:bg-active-blue">
<div className="flex-shrink-0 py-1">{filter.label}</div>
<FilterOperator
options={filter.operatorOptions}

View file

@ -144,7 +144,7 @@ export default function <Value extends ValueObject>({
return (
<Select
className={className}
className={`${className} btn-event-condition`}
options={options}
isSearchable={isSearchable}
defaultValue={defaultSelected}