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:
parent
48038b4fc6
commit
488dfcd849
25 changed files with 554 additions and 364 deletions
|
|
@ -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;
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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()}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
),
|
||||
})),
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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) =>
|
||||
|
|
|
|||
|
|
@ -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={() => {
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue