Product Analytics UI Improvements. (#2896)
* Various improvements Cards, OmniSearch and Cards Listing * Improved cards listing page * Various improvements in product analytics * Charts UI improvements --------- Co-authored-by: nick-delirium <nikita@openreplay.com>
This commit is contained in:
parent
415e24b9a0
commit
fe4bbcda6d
30 changed files with 444 additions and 236 deletions
|
|
@ -44,9 +44,9 @@ function CustomAreaChart(props: Props) {
|
|||
<Legend iconType={'circle'} wrapperStyle={{ top: inGrid ? undefined : -18 }} />
|
||||
)}
|
||||
<CartesianGrid
|
||||
strokeDasharray="3 3"
|
||||
strokeDasharray="1 3"
|
||||
vertical={false}
|
||||
stroke="#EEEEEE"
|
||||
stroke="rgba(0,0,0,.15)"
|
||||
/>
|
||||
<XAxis {...Styles.xaxis} dataKey="time" interval={'equidistantPreserveStart'} />
|
||||
<YAxis
|
||||
|
|
@ -64,10 +64,11 @@ function CustomAreaChart(props: Props) {
|
|||
<Area
|
||||
key={key}
|
||||
name={key}
|
||||
type="monotone"
|
||||
type="linear"
|
||||
dataKey={key}
|
||||
stroke={colors[index]}
|
||||
color={colors[index]}
|
||||
fill={colors[index]}
|
||||
fillOpacity={0.3}
|
||||
legendType={key === 'Total' ? 'none' : 'line'}
|
||||
dot={false}
|
||||
// strokeDasharray={'4 3'} FOR COPMARISON ONLY
|
||||
|
|
|
|||
|
|
@ -25,17 +25,15 @@ interface Props {
|
|||
}
|
||||
|
||||
const getPath = (x, y, width, height) => {
|
||||
const radius = Math.min(width / 2, height / 2);
|
||||
const radius = 4;
|
||||
return `
|
||||
M${x + radius},${y}
|
||||
H${x + width - radius}
|
||||
A${radius},${radius} 0 0 1 ${x + width},${y + radius}
|
||||
V${y + height - radius}
|
||||
A${radius},${radius} 0 0 1 ${x + width - radius},${y + height}
|
||||
H${x + radius}
|
||||
A${radius},${radius} 0 0 1 ${x},${y + height - radius}
|
||||
Q${x + width},${y} ${x + width},${y + radius}
|
||||
V${y + height}
|
||||
H${x}
|
||||
V${y + radius}
|
||||
A${radius},${radius} 0 0 1 ${x + radius},${y}
|
||||
Q${x},${y} ${x + radius},${y}
|
||||
Z
|
||||
`;
|
||||
};
|
||||
|
|
@ -45,17 +43,13 @@ const PillBar = (props) => {
|
|||
|
||||
return (
|
||||
<g transform={`translate(${x}, ${y})`}>
|
||||
<rect
|
||||
width={width}
|
||||
height={height}
|
||||
rx={10}
|
||||
ry={10}
|
||||
<path
|
||||
d={getPath(0, 0, width, height)}
|
||||
fill={fill}
|
||||
/>
|
||||
{striped && (
|
||||
<rect
|
||||
width={width}
|
||||
height={height}
|
||||
<path
|
||||
d={getPath(0, 0, width, height)}
|
||||
clipPath="url(#pillClip)"
|
||||
fill="url(#diagonalStripes)"
|
||||
/>
|
||||
|
|
@ -64,8 +58,6 @@ const PillBar = (props) => {
|
|||
);
|
||||
};
|
||||
|
||||
|
||||
|
||||
function CustomBarChart(props: Props) {
|
||||
const {
|
||||
data = { chart: [], namesMap: [] },
|
||||
|
|
@ -84,7 +76,6 @@ function CustomBarChart(props: Props) {
|
|||
return item;
|
||||
});
|
||||
|
||||
// we mix 1 original, then 1 comparison, etc
|
||||
const mergedNameMap: { data: any, isComp: boolean, index: number }[] = [];
|
||||
for (let i = 0; i < data.namesMap.length; i++) {
|
||||
mergedNameMap.push({ data: data.namesMap[i], isComp: false, index: i });
|
||||
|
|
@ -92,34 +83,55 @@ function CustomBarChart(props: Props) {
|
|||
mergedNameMap.push({ data: compData.namesMap[i], isComp: true, index: i });
|
||||
}
|
||||
}
|
||||
|
||||
// Filter out comparison items for legend
|
||||
const legendItems = mergedNameMap.filter(item => !item.isComp);
|
||||
|
||||
return (
|
||||
<ResponsiveContainer height={240} width="100%">
|
||||
<BarChart
|
||||
data={resultChart}
|
||||
margin={Styles.chartMargins}
|
||||
onClick={onClick}
|
||||
barSize={10}
|
||||
>
|
||||
<defs>
|
||||
<clipPath id="pillClip">
|
||||
<rect x="0" y="0" width="100%" height="100%" rx="10" ry="10" />
|
||||
<rect x="0" y="0" width="100%" height="100%" rx="4" ry="4" />
|
||||
</clipPath>
|
||||
<pattern
|
||||
id="diagonalStripes"
|
||||
patternUnits="userSpaceOnUse"
|
||||
width="8"
|
||||
height="8"
|
||||
width="4"
|
||||
height="4"
|
||||
patternTransform="rotate(45)"
|
||||
>
|
||||
<line x1="0" y="0" x2="0" y2="8" stroke="white" strokeWidth="6" />
|
||||
<line
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="0"
|
||||
y2="4"
|
||||
stroke="#FFFFFF"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
</pattern>
|
||||
</defs>
|
||||
{!hideLegend && (
|
||||
<Legend iconType={'circle'} wrapperStyle={{ top: inGrid ? undefined : -18 }} />
|
||||
<Legend
|
||||
iconType="rect"
|
||||
wrapperStyle={{ top: inGrid ? undefined : -18 }}
|
||||
payload={legendItems.map(item => ({
|
||||
value: item.data,
|
||||
type: 'rect',
|
||||
color: colors[item.index],
|
||||
id: item.data
|
||||
}))}
|
||||
/>
|
||||
)}
|
||||
<CartesianGrid
|
||||
strokeDasharray="3 3"
|
||||
strokeDasharray="1 3"
|
||||
vertical={false}
|
||||
stroke="#EEEEEE"
|
||||
stroke="rgba(0,0,0,.15)"
|
||||
/>
|
||||
<XAxis
|
||||
{...Styles.xaxis}
|
||||
|
|
@ -145,11 +157,22 @@ function CustomBarChart(props: Props) {
|
|||
fill={colors[item.index]}
|
||||
stroke={colors[item.index]}
|
||||
shape={(barProps: any) => (
|
||||
<PillBar {...barProps} fill={colors[item.index]} barKey={item.index} stroke={colors[item.index]} striped={item.isComp} />
|
||||
<PillBar
|
||||
{...barProps}
|
||||
fill={colors[item.index]}
|
||||
barKey={item.index}
|
||||
stroke={colors[item.index]}
|
||||
striped={item.isComp}
|
||||
/>
|
||||
)}
|
||||
legendType={'line'}
|
||||
legendType="rect"
|
||||
activeBar={
|
||||
<PillBar fill={colors[item.index]} stroke={colors[item.index]} barKey={item.index} striped={item.isComp} />
|
||||
<PillBar
|
||||
fill={colors[item.index]}
|
||||
stroke={colors[item.index]}
|
||||
barKey={item.index}
|
||||
striped={item.isComp}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
|
|
@ -158,4 +181,4 @@ function CustomBarChart(props: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
export default CustomBarChart;
|
||||
export default CustomBarChart;
|
||||
|
|
@ -16,7 +16,7 @@ function BigNumChart(props: Props) {
|
|||
values,
|
||||
} = props;
|
||||
return (
|
||||
<div className={'flex justify-around gap-2 w-full'} style={{ height: 240 }}>
|
||||
<div className={'flex flex-row flex-wrap gap-2 -mt-6'} 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 gap-2 py-8 items-center'}>
|
||||
<div className={'flex flex-col flex-auto justify-center items-center hover:bg-teal/5'}>
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -8,49 +8,44 @@ interface PayloadItem {
|
|||
name: string;
|
||||
value: number;
|
||||
prevValue?: number;
|
||||
color?: string;
|
||||
payload?: any;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
active: boolean;
|
||||
payload: PayloadItem[];
|
||||
label: string;
|
||||
activeKey?: string;
|
||||
hoveredSeries: string | null;
|
||||
}
|
||||
|
||||
function CustomTooltip(props: Props) {
|
||||
const { active, payload, label, activeKey } = props;
|
||||
if (!active || !payload?.length) return null;
|
||||
const { active, payload, label, hoveredSeries } = props;
|
||||
if (!active || !payload?.length || !hoveredSeries) return null;
|
||||
|
||||
const shownPayloads: PayloadItem[] = payload.filter((p) => !p.hide);
|
||||
const currentSeries: PayloadItem[] = [];
|
||||
const previousSeriesMap: Record<string, any> = {};
|
||||
// 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)`
|
||||
);
|
||||
|
||||
shownPayloads.forEach((item) => {
|
||||
if (item.name.startsWith('Previous ')) {
|
||||
const originalName = item.name.replace('Previous ', '');
|
||||
previousSeriesMap[originalName] = item.value;
|
||||
} else {
|
||||
currentSeries.push(item);
|
||||
}
|
||||
});
|
||||
if (!currentPayload) return null;
|
||||
|
||||
const transformedArray = currentSeries.map((item) => {
|
||||
const prevValue = previousSeriesMap[item.name] || null;
|
||||
return {
|
||||
...item,
|
||||
prevValue,
|
||||
};
|
||||
});
|
||||
// Create transformed array with comparison data
|
||||
const transformedArray = [{
|
||||
...currentPayload,
|
||||
prevValue: comparisonPayload ? comparisonPayload.value : null
|
||||
}];
|
||||
|
||||
const isHigher = (item: { value: number; prevValue: number }) =>
|
||||
item.prevValue !== null && item.prevValue < item.value;
|
||||
|
||||
const getPercentDelta = (val: number, prevVal: number) =>
|
||||
(((val - prevVal) / prevVal) * 100).toFixed(2);
|
||||
|
||||
const isHigher = (item: { value: number; prevValue: number }) => {
|
||||
return item.prevValue !== null && item.prevValue < item.value;
|
||||
};
|
||||
const getPercentDelta = (val, prevVal) => {
|
||||
return (((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'}>
|
||||
|
|
@ -64,20 +59,20 @@ function CustomTooltip(props: Props) {
|
|||
</div>
|
||||
<div
|
||||
style={{ borderLeft: `2px solid ${p.color}` }}
|
||||
className={'flex flex-col py-2 px-2 ml-2'}
|
||||
className={'flex flex-col px-2 ml-2 '}
|
||||
>
|
||||
<div className={'text-disabled-text text-sm'}>
|
||||
<div className={'text-neutral-600 text-sm'}>
|
||||
{label}, {formatTimeOrDate(p.payload.timestamp)}
|
||||
</div>
|
||||
<div className={'flex items-center gap-1'}>
|
||||
<div className={'font-medium'}>{p.value}</div>
|
||||
{p.prevValue ? (
|
||||
{p.prevValue !== null && (
|
||||
<CompareTag
|
||||
isHigher={isHigher(p)}
|
||||
absDelta={Math.abs(p.value - p.prevValue)}
|
||||
delta={getPercentDelta(p.value, p.prevValue)}
|
||||
/>
|
||||
) : null}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
|
|
@ -99,7 +94,7 @@ export function CompareTag({
|
|||
<div
|
||||
className={cn(
|
||||
'px-2 py-1 w-fit rounded flex items-center gap-1',
|
||||
isHigher ? 'bg-green2 text-xs' : 'bg-red2 text-xs'
|
||||
isHigher ? 'bg-green2/10 text-xs' : 'bg-red2/10 text-xs'
|
||||
)}
|
||||
>
|
||||
{!isHigher ? <ArrowDown size={12} /> : <ArrowUp size={12} />}
|
||||
|
|
@ -109,4 +104,4 @@ export function CompareTag({
|
|||
);
|
||||
}
|
||||
|
||||
export default CustomTooltip;
|
||||
export default CustomTooltip;
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
import React from 'react';
|
||||
import { Legend } from 'recharts';
|
||||
|
||||
interface CustomLegendProps {
|
||||
payload?: any[];
|
||||
}
|
||||
|
||||
function CustomLegend({ payload }: CustomLegendProps) {
|
||||
return (
|
||||
<div className="custom-legend" style={{ display: 'flex', justifyContent:'center', gap: '1rem', flexWrap: 'wrap' }}>
|
||||
{payload?.map((entry) => (
|
||||
<div key={entry.value} style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
||||
{entry.value.includes('(Comparison)') ? (
|
||||
<div
|
||||
style={{
|
||||
width: 20,
|
||||
height: 2,
|
||||
backgroundImage: 'linear-gradient(to right, black 50%, transparent 50%)',
|
||||
backgroundSize: '4px 2px',
|
||||
backgroundRepeat: 'repeat-x',
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
width: 20,
|
||||
height: 2,
|
||||
backgroundColor: entry.color,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<span className='text-sm'>{entry.value}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default CustomLegend;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import CustomTooltip from "../CustomChartTooltip";
|
||||
import { Styles } from '../../common';
|
||||
import {
|
||||
|
|
@ -37,10 +37,35 @@ function CustomMetricLineChart(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 };
|
||||
// return item;
|
||||
// });
|
||||
|
||||
const resultChart = data.chart.map((item, i) => {
|
||||
if (compData && compData.chart[i]) return { ...compData.chart[i], ...item }
|
||||
return 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;
|
||||
});
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<ResponsiveContainer height={240} width="100%">
|
||||
|
|
@ -48,65 +73,99 @@ function CustomMetricLineChart(props: Props) {
|
|||
data={resultChart}
|
||||
margin={Styles.chartMargins}
|
||||
onClick={onClick}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
{!hideLegend && (
|
||||
<Legend iconType={'circle'} wrapperStyle={{ top: inGrid ? undefined : -18 }} />
|
||||
<Legend wrapperStyle={{ top: inGrid ? undefined : -18 }}
|
||||
payload={
|
||||
(data.namesMap as string[]).map((key: string, index: number) => ({
|
||||
value: key,
|
||||
type: 'line',
|
||||
color: colors[index],
|
||||
id: key,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<CartesianGrid
|
||||
strokeDasharray="3 3"
|
||||
vertical={false}
|
||||
stroke="#EEEEEE"
|
||||
/>
|
||||
<CartesianGrid strokeDasharray="1 3" vertical={false} stroke="rgba(0,0,0,.15)" />
|
||||
<XAxis {...Styles.xaxis} dataKey="time" interval={'equidistantPreserveStart'} />
|
||||
<YAxis
|
||||
{...yaxis}
|
||||
allowDecimals={false}
|
||||
tickFormatter={(val) => Styles.tickFormatter(val)}
|
||||
label={{
|
||||
...Styles.axisLabelLeft,
|
||||
value: label || 'Number of Sessions',
|
||||
}}
|
||||
label={{ ...Styles.axisLabelLeft, value: label || 'Number of Sessions' }}
|
||||
/>
|
||||
<Tooltip {...Styles.tooltip} content={CustomTooltip} />
|
||||
<Tooltip {...Styles.tooltip} content={<CustomTooltip hoveredSeries={hoveredSeries} />} />
|
||||
|
||||
{Array.isArray(data.namesMap) &&
|
||||
data.namesMap.map((key, index) => key ? (
|
||||
<Line
|
||||
key={key}
|
||||
name={key}
|
||||
animationDuration={0}
|
||||
type="monotone"
|
||||
dataKey={key}
|
||||
stroke={colors[index]}
|
||||
fillOpacity={1}
|
||||
strokeWidth={2}
|
||||
strokeOpacity={key === 'Total' ? 0 : 0.6}
|
||||
legendType={key === 'Total' ? 'none' : 'line'}
|
||||
dot={false}
|
||||
activeDot={{ fill: colors[index]}}
|
||||
/>
|
||||
) : null)}
|
||||
{compData?.namesMap?.map((key, i) => data.namesMap[i] ? (
|
||||
<Line
|
||||
key={key}
|
||||
name={key}
|
||||
animationDuration={0}
|
||||
type="monotone"
|
||||
dataKey={key}
|
||||
stroke={colors[i]}
|
||||
fillOpacity={1}
|
||||
strokeWidth={2}
|
||||
strokeOpacity={0.6}
|
||||
legendType={'line'}
|
||||
dot={false}
|
||||
strokeDasharray={'4 3'}
|
||||
activeDot={{
|
||||
fill: colors[i],
|
||||
}}
|
||||
/>
|
||||
) : 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 ? 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
|
||||
)}
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(CustomMetricLineChart);
|
||||
export default observer(CustomMetricLineChart);
|
||||
|
|
@ -63,15 +63,14 @@ function CustomMetricPieChart(props: Props) {
|
|||
>
|
||||
<ResponsiveContainer height={240} width="100%">
|
||||
<PieChart>
|
||||
<Legend iconType={'circle'} wrapperStyle={{ top: inGrid ? undefined : -18 }} />
|
||||
<Legend iconType={'triangle'} wrapperStyle={{ top: inGrid ? undefined : -18 }} />
|
||||
<Pie
|
||||
isAnimationActive={false}
|
||||
data={values}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
innerRadius={40}
|
||||
outerRadius={70}
|
||||
// fill={colors[0]}
|
||||
innerRadius={20}
|
||||
outerRadius={60}
|
||||
activeIndex={1}
|
||||
onClick={onClickHandler}
|
||||
labelLine={({
|
||||
|
|
@ -149,7 +148,7 @@ function CustomMetricPieChart(props: Props) {
|
|||
{values && values.map((entry, index) => (
|
||||
<Cell
|
||||
key={`cell-${index}`}
|
||||
fill={Styles.colorsPie[index % Styles.colorsPie.length]}
|
||||
fill={Styles.safeColors[index % Styles.safeColors.length]}
|
||||
/>
|
||||
))}
|
||||
</Pie>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ const compareColors = ['#192EDB', '#6272FF', '#808DFF', '#B3BBFF', '#C9CFFF'];
|
|||
const compareColorsx = ["#222F99", "#2E3ECC", "#394EFF", "#6171FF", "#8895FF", "#B0B8FF", "#D7DCFF"].reverse();
|
||||
const customMetricColors = ['#394EFF', '#3EAAAF', '#565D97'];
|
||||
const colorsPie = colors.concat(["#DDDDDD"]);
|
||||
const safeColors = ['#394EFF', '#3EAAAF', '#9276da', '#ceba64', "#bc6f9d", '#966fbc', '#64ce86', '#e06da3', '#6dabe0'];
|
||||
|
||||
const countView = count => {
|
||||
const isMoreThanK = count >= 1000;
|
||||
|
|
@ -22,6 +23,7 @@ export default {
|
|||
colorsx,
|
||||
compareColors,
|
||||
compareColorsx,
|
||||
safeColors,
|
||||
lineColor: '#2A7B7F',
|
||||
lineColorCompare: '#394EFF',
|
||||
strokeColor: compareColors[0],
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@ import React from 'react';
|
|||
import BackButton from 'Shared/Breadcrumb/BackButton';
|
||||
import { withSiteId } from 'App/routes';
|
||||
import { withRouter, RouteComponentProps } from 'react-router-dom';
|
||||
import { PageTitle, confirm, Tooltip } from 'UI';
|
||||
import { PageTitle, confirm } from 'UI';
|
||||
import { Tooltip } from 'antd';
|
||||
import SelectDateRange from 'Shared/SelectDateRange';
|
||||
import { useStore } from 'App/mstore';
|
||||
import DashboardOptions from '../DashboardOptions';
|
||||
|
|
@ -53,7 +54,8 @@ function DashboardHeader(props: Props) {
|
|||
focusTitle={focusTitle}
|
||||
/>
|
||||
|
||||
<div className="flex items-center justify-between px-4 py-2 bg-white border-b border-b-gray-light">
|
||||
|
||||
<div className="flex items-center justify-between px-4 pt-4 bg-white">
|
||||
<div className="flex items-center gap-2" style={{ flex: 3 }}>
|
||||
<BackButton siteId={siteId} compact />
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
import { LockOutlined, TeamOutlined } from '@ant-design/icons';
|
||||
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import React from 'react';
|
||||
import { useHistory } from 'react-router';
|
||||
import {
|
||||
Empty,
|
||||
Switch,
|
||||
|
|
@ -10,17 +13,13 @@ import {
|
|||
Dropdown,
|
||||
Button,
|
||||
} from 'antd';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import React from 'react';
|
||||
import { useHistory } from 'react-router';
|
||||
|
||||
import { LockOutlined, TeamOutlined, MoreOutlined } from '@ant-design/icons';
|
||||
import { checkForRecent } from 'App/date';
|
||||
import { useStore } from 'App/mstore';
|
||||
import Dashboard from 'App/mstore/types/dashboard';
|
||||
import { dashboardSelected, withSiteId } from 'App/routes';
|
||||
import CreateDashboardButton from 'Components/Dashboard/components/CreateDashboardButton';
|
||||
import { Icon, confirm } from 'UI';
|
||||
import { EllipsisVertical } from 'lucide-react';
|
||||
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
|
||||
|
||||
import DashboardEditModal from '../DashboardEditModal';
|
||||
|
|
@ -106,6 +105,7 @@ function DashboardList() {
|
|||
}
|
||||
checkedChildren={'Team'}
|
||||
unCheckedChildren={'Private'}
|
||||
className='toggle-team-private'
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
|
@ -124,7 +124,7 @@ function DashboardList() {
|
|||
},
|
||||
|
||||
{
|
||||
title: 'Options',
|
||||
title: '',
|
||||
dataIndex: 'dashboardId',
|
||||
width: '5%',
|
||||
render: (id) => (
|
||||
|
|
@ -161,7 +161,7 @@ function DashboardList() {
|
|||
},
|
||||
}}
|
||||
>
|
||||
<Button id={'ignore-prop'} icon={<EllipsisVertical size={16} />} />
|
||||
<Button id={'ignore-prop'} icon={<MoreOutlined />} type='text' className='btn-dashboards-list-item-more-options' />
|
||||
</Dropdown>
|
||||
),
|
||||
},
|
||||
|
|
@ -223,6 +223,8 @@ function DashboardList() {
|
|||
showTotal: (total, range) =>
|
||||
`Showing ${range[0]}-${range[1]} of ${total} items`,
|
||||
size: 'small',
|
||||
simple: 'true',
|
||||
className: 'px-4 pr-8 mb-0',
|
||||
}}
|
||||
onRow={(record) => ({
|
||||
onClick: (e) => {
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ function DashboardSearch() {
|
|||
value={query}
|
||||
allowClear
|
||||
name="dashboardsSearch"
|
||||
className="w-full"
|
||||
className="w-full btn-search-dashboard"
|
||||
placeholder="Filter by dashboard title"
|
||||
onChange={write}
|
||||
onSearch={(value) => dashboardStore.updateKey('filter', { ...dashboardStore.filter, query: value })}
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ function AreaChartCard(props: Props) {
|
|||
margin={Styles.chartMargins}
|
||||
>
|
||||
{gradientDef}
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#EEEEEE"/>
|
||||
<CartesianGrid strokeDasharray="1 3" vertical={false} stroke="rgba(0,0,0,1.5)"/>
|
||||
<XAxis {...Styles.xaxis} dataKey="time" interval={3}/>
|
||||
<YAxis
|
||||
{...Styles.yaxis}
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ function DashboardView(props: Props) {
|
|||
const isSaas = /app\.openreplay\.com/.test(originStr);
|
||||
return (
|
||||
<Loader loading={loading}>
|
||||
<div style={{maxWidth: '1360px', margin: 'auto'}} className={'rounded border border-gray-light bg-gray-light-blue'}>
|
||||
<div style={{maxWidth: '1360px', margin: 'auto'}} className={'rounded-lg shadow-sm overflow-hidden bg-white border'}>
|
||||
{/* @ts-ignore */}
|
||||
<DashboardHeader renderReport={props.renderReport} siteId={siteId} dashboardId={dashboardId}/>
|
||||
{isSaas ? <AiQuery /> : null}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import WidgetWrapperNew from 'Components/Dashboard/components/WidgetWrapper/Widg
|
|||
import { observer } from 'mobx-react-lite';
|
||||
import AddCardSection from '../AddCardSection/AddCardSection';
|
||||
import cn from 'classnames';
|
||||
import { Button, Popover } from 'antd'
|
||||
import { Button, Popover, Tooltip } from 'antd'
|
||||
import { PlusOutlined } from '@ant-design/icons'
|
||||
import { Loader } from 'UI';
|
||||
|
||||
|
|
@ -39,7 +39,7 @@ function DashboardWidgetGrid(props: Props) {
|
|||
{list?.map((item: any, index: any) => (
|
||||
<div
|
||||
key={item.widgetId}
|
||||
className={cn('col-span-' + item.config.col, 'group relative px-6 py-2 hover:bg-active-blue w-full')}
|
||||
className={cn('col-span-' + item.config.col, 'group relative pl-6 pr-4 py-4 hover:bg-active-blue w-full rounded-xl')}
|
||||
>
|
||||
<WidgetWrapperNew
|
||||
index={index}
|
||||
|
|
@ -60,7 +60,9 @@ function DashboardWidgetGrid(props: Props) {
|
|||
)}
|
||||
>
|
||||
<Popover arrow={false} overlayInnerStyle={{ padding: 0, borderRadius: '0.75rem' }} content={<AddCardSection />} trigger={'click'}>
|
||||
<Tooltip title="Add Card">
|
||||
<Button icon={<PlusOutlined size={14} />} shape={'circle'} size={'small'} />
|
||||
</Tooltip>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ const FilterSeriesHeader = observer(
|
|||
onRemove: (seriesIndex: any) => void;
|
||||
canDelete: boolean | undefined;
|
||||
toggleExpand: () => void;
|
||||
onChange: () => void;
|
||||
}) => {
|
||||
const onUpdate = (name: any) => {
|
||||
props.series.update('name', name);
|
||||
|
|
@ -66,6 +67,7 @@ const FilterSeriesHeader = observer(
|
|||
seriesIndex={props.seriesIndex}
|
||||
name={props.series.name}
|
||||
onUpdate={onUpdate}
|
||||
onChange={props.onChange}
|
||||
/>
|
||||
{!props.expanded && (
|
||||
<FilterCountLabels
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { Icon } from 'UI';
|
||||
import { Input, Tooltip } from 'antd';
|
||||
|
||||
interface Props {
|
||||
name: string;
|
||||
onUpdate: (name: string) => void;
|
||||
onChange: () => void;
|
||||
seriesIndex?: number;
|
||||
}
|
||||
|
||||
|
|
@ -16,6 +16,7 @@ function SeriesName(props: Props) {
|
|||
|
||||
const write = ({ target: { value } }) => {
|
||||
setName(value);
|
||||
props.onChange();
|
||||
};
|
||||
|
||||
const onBlur = () => {
|
||||
|
|
@ -51,6 +52,7 @@ function SeriesName(props: Props) {
|
|||
onBlur={onBlur}
|
||||
onKeyDown={onKeyDown}
|
||||
className="bg-white text-lg border-transparent rounded-lg font-medium ps-2"
|
||||
maxLength={22}
|
||||
/>
|
||||
) : (
|
||||
<Tooltip title="Double click to rename.">
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ interface Props extends RouteComponentProps {
|
|||
function MetricTypeIcon({ type }: any) {
|
||||
return (
|
||||
<Tooltip title={<div className="capitalize">{TYPE_NAMES[type]}</div>}>
|
||||
<Avatar src={<Icon name={TYPE_ICONS[type]} size="16" color="tealx" />} size="small" className="bg-tealx-lightest mr-2" />
|
||||
<Avatar src={<Icon name={TYPE_ICONS[type]} size="16" color="tealx" />} size="default" className="bg-tealx-lightest mr-2 cursor-default avatar-card-list-item" />
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
|
@ -171,12 +171,12 @@ const MetricListItem: React.FC<Props> = ({
|
|||
case 'options':
|
||||
return (
|
||||
<>
|
||||
<div className='flex justify-end'>
|
||||
<div className='flex justify-end pr-4'>
|
||||
<Dropdown
|
||||
menu={{ items: menuItems, onClick: onMenuClick }}
|
||||
trigger={['click']}
|
||||
>
|
||||
<Button id={'ignore-prop'} icon={<EllipsisVertical size={16} />} />
|
||||
<Button id={'ignore-prop'} icon={<EllipsisVertical size={16} />} className='btn-cards-list-item-more-options' />
|
||||
</Dropdown>
|
||||
</div>
|
||||
{renderModal()}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,10 @@ import React, { useEffect } from 'react';
|
|||
import { PageTitle } from 'UI';
|
||||
import { Button, Popover } from 'antd';
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import React, { useState } from 'react';
|
||||
import { PageTitle, Icon } from 'UI';
|
||||
import { Segmented, Button, Popover, Space, Dropdown, Menu } from 'antd';
|
||||
import { PlusOutlined, DownOutlined } from '@ant-design/icons';
|
||||
import AddCardSection from '../AddCardSection/AddCardSection';
|
||||
import MetricsSearch from '../MetricsSearch';
|
||||
import { useStore } from 'App/mstore';
|
||||
|
|
@ -9,29 +13,63 @@ import { observer } from 'mobx-react-lite';
|
|||
|
||||
function MetricViewHeader() {
|
||||
const { metricStore } = useStore();
|
||||
const filter = metricStore.filter;
|
||||
const [showAddCardModal, setShowAddCardModal] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Set the default sort order to 'desc'
|
||||
metricStore.updateKey('sort', { by: 'desc' });
|
||||
}, [metricStore]);
|
||||
// Handler for dropdown menu selection
|
||||
const handleMenuClick = ({ key }) => {
|
||||
metricStore.updateKey('filter', { ...filter, type: key });
|
||||
};
|
||||
|
||||
// Dropdown menu options
|
||||
const menu = (
|
||||
<Menu onClick={handleMenuClick}>
|
||||
<Menu.Item key="all">All Types</Menu.Item>
|
||||
{DROPDOWN_OPTIONS.map((option) => (
|
||||
<Menu.Item key={option.value}>{option.label}</Menu.Item>
|
||||
))}
|
||||
</Menu>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between px-4 pb-2">
|
||||
<div className="flex items-baseline mr-3">
|
||||
<PageTitle title="Cards" className="" />
|
||||
<div className="flex items-center justify-between pr-4">
|
||||
<div className="flex items-center gap-2 ps-4">
|
||||
<PageTitle title="Cards" className="cursor-default" />
|
||||
<Space>
|
||||
<Dropdown overlay={menu} trigger={['click']} className=''>
|
||||
<Button type="text" size='small' className='mt-1'>
|
||||
{filter.type === 'all' ? 'All Types' : DROPDOWN_OPTIONS.find(opt => opt.value === filter.type)?.label || 'Select Type'}
|
||||
<DownOutlined />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</Space>
|
||||
</div>
|
||||
<div className="ml-auto flex items-center">
|
||||
<Popover arrow={false} overlayInnerStyle={{ padding: 0, borderRadius: '0.75rem' }} content={<AddCardSection fit inCards />} trigger={'click'}>
|
||||
<div className="ml-auto flex items-center gap-3">
|
||||
<Popover
|
||||
arrow={false}
|
||||
overlayInnerStyle={{ padding: 0, borderRadius: '0.75rem' }}
|
||||
content={<AddCardSection fit inCards />}
|
||||
trigger="click"
|
||||
>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
className='btn-create-card'
|
||||
>
|
||||
Create Card
|
||||
</Button>
|
||||
</Popover>
|
||||
<div className="ml-4 w-1/4" style={{ minWidth: 300 }}>
|
||||
|
||||
<Space>
|
||||
<MetricsSearch />
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ const ListView: React.FC<Props> = (props: Props) => {
|
|||
),
|
||||
dataIndex: 'name',
|
||||
key: 'title',
|
||||
className: 'cap-first',
|
||||
className: 'cap-first pl-4',
|
||||
sorter: true,
|
||||
width: '25%',
|
||||
render: (text: string, metric: Metric) => (
|
||||
|
|
@ -233,13 +233,6 @@ const ListView: React.FC<Props> = (props: Props) => {
|
|||
}
|
||||
: undefined
|
||||
}
|
||||
// footer={() => (
|
||||
// <div className="flex justify-end">
|
||||
// <Checkbox name="slack" checked={allSelected} onClick={toggleAll}>
|
||||
// Select All
|
||||
// </Checkbox>
|
||||
// </div>
|
||||
// )}
|
||||
pagination={{
|
||||
current: pagination.current,
|
||||
pageSize: pagination.pageSize,
|
||||
|
|
@ -248,7 +241,8 @@ const ListView: React.FC<Props> = (props: Props) => {
|
|||
className: 'px-4',
|
||||
showLessItems: true,
|
||||
showTotal: () => totalMessage,
|
||||
showQuickJumper: true
|
||||
size: 'small',
|
||||
simple: 'true',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { observer, useObserver } from 'mobx-react-lite';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { NoContent, Pagination, Loader } from 'UI';
|
||||
import { NoContent, Loader } from 'UI';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { sliceListPerPage } from 'App/utils';
|
||||
import GridView from './GridView';
|
||||
|
|
@ -16,6 +16,7 @@ function MetricsList({
|
|||
}) {
|
||||
const { metricStore, dashboardStore } = useStore();
|
||||
const metricsSearch = metricStore.filter.query;
|
||||
const listView = metricStore.listView;
|
||||
const [selectedMetrics, setSelectedMetrics] = useState<any>([]);
|
||||
|
||||
const dashboard = dashboardStore.selectedDashboard;
|
||||
|
|
@ -77,28 +78,51 @@ function MetricsList({
|
|||
</div>
|
||||
</div>
|
||||
}
|
||||
subtext="Utilize cards to visualize key user interactions or product performance metrics."
|
||||
subtext={
|
||||
metricsSearch !== ''
|
||||
? ''
|
||||
: 'Utilize cards to visualize key user interactions or product performance metrics.'
|
||||
}
|
||||
>
|
||||
<ListView
|
||||
disableSelection={!onSelectionChange}
|
||||
siteId={siteId}
|
||||
list={cards}
|
||||
selectedList={selectedMetrics}
|
||||
existingCardIds={existingCardIds}
|
||||
toggleSelection={toggleMetricSelection}
|
||||
allSelected={cards.length === selectedMetrics.length}
|
||||
toggleAll={({ target: { checked, name } }) =>
|
||||
setSelectedMetrics(
|
||||
checked
|
||||
? cards
|
||||
.map((i: any) => i.metricId)
|
||||
.slice(0, 30 - existingCardIds!.length)
|
||||
: []
|
||||
)
|
||||
}
|
||||
showOwn={showOwn}
|
||||
toggleOwn={toggleOwn}
|
||||
/>
|
||||
{listView ? (
|
||||
<ListView
|
||||
disableSelection={!onSelectionChange}
|
||||
siteId={siteId}
|
||||
list={cards}
|
||||
selectedList={selectedMetrics}
|
||||
existingCardIds={existingCardIds}
|
||||
toggleSelection={toggleMetricSelection}
|
||||
allSelected={cards.length === selectedMetrics.length}
|
||||
showOwn={showOwn}
|
||||
toggleOwn={toggleOwn}
|
||||
toggleAll={({ target: { checked, name } }) =>
|
||||
setSelectedMetrics(checked ? cards.map((i: any) => i.metricId).slice(0, 30 - existingCardIds!.length) : [])
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<GridView
|
||||
siteId={siteId}
|
||||
list={sliceListPerPage(cards, metricStore.page - 1, metricStore.pageSize)}
|
||||
selectedList={selectedMetrics}
|
||||
toggleSelection={toggleMetricSelection}
|
||||
/>
|
||||
<div className="w-full flex items-center justify-between py-4 px-6 border-t">
|
||||
<div className="">
|
||||
Showing{' '}
|
||||
<span className="font-medium">{Math.min(cards.length, metricStore.pageSize)}</span> out
|
||||
of <span className="font-medium">{cards.length}</span> cards
|
||||
</div>
|
||||
<Pagination
|
||||
page={metricStore.page}
|
||||
total={lenth}
|
||||
onPageChange={(page) => metricStore.updateKey('page', page)}
|
||||
limit={metricStore.pageSize}
|
||||
debounceRequest={100}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</NoContent>
|
||||
</Loader>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ function MetricsSearch() {
|
|||
value={query}
|
||||
allowClear
|
||||
name="metricsSearch"
|
||||
className="w-full"
|
||||
className="w-full input-search-card"
|
||||
placeholder="Filter by title or owner"
|
||||
onChange={write}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -11,7 +11,9 @@ function MetricsView({ siteId }: Props) {
|
|||
return (
|
||||
<div style={{ maxWidth: '1360px', margin: 'auto' }} className="bg-white rounded-lg shadow-sm pt-4 border">
|
||||
<MetricViewHeader siteId={siteId} />
|
||||
<div className='pt-3'>
|
||||
<MetricsList siteId={siteId} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ function WidgetChart(props: Props) {
|
|||
const period = dashboardStore.period;
|
||||
const drillDownPeriod = dashboardStore.drillDownPeriod;
|
||||
const drillDownFilter = dashboardStore.drillDownFilter;
|
||||
const colors = Styles.customMetricColors;
|
||||
const colors = Styles.safeColors;
|
||||
const [loading, setLoading] = useState(true);
|
||||
const params = { density: dashboardStore.selectedDensity };
|
||||
const metricParams = _metric.params;
|
||||
|
|
|
|||
|
|
@ -112,12 +112,13 @@ function WidgetDatatable(props: Props) {
|
|||
return (
|
||||
<div className={cn('relative -mx-4 px-2', showTable ? 'pt-6' : '')}>
|
||||
<div
|
||||
className={
|
||||
'absolute left-0 right-0 top-0 border-t border-t-gray-lighter'
|
||||
}
|
||||
className={cn(
|
||||
'absolute left-0 right-0 -top-3 border-t border-t-gray-lighter',
|
||||
{ 'hidden': !showTable }
|
||||
)}
|
||||
/>
|
||||
<div
|
||||
className={'absolute top-0 left-1/2 z-10'}
|
||||
className={'absolute -top-3 left-1/2 z-10'}
|
||||
style={{ transform: 'translate(-50%, -50%)' }}
|
||||
>
|
||||
<Button
|
||||
|
|
|
|||
|
|
@ -87,21 +87,18 @@ const FilterSection = observer(({ metric, excludeFilterKeys }: any) => {
|
|||
))}
|
||||
|
||||
{!isSingleSeries && canAddSeries && (
|
||||
<Card
|
||||
styles={{ body: { padding: '4px' } }}
|
||||
className="rounded-xl shadow-sm mb-2"
|
||||
>
|
||||
<div
|
||||
onClick={() => {
|
||||
if (!canAddSeries) return;
|
||||
metric.addSeries();
|
||||
}}
|
||||
className="w-full cursor-pointer flex items-center py-2 justify-center gap-2 font-medium hover:text-teal"
|
||||
>
|
||||
<PlusIcon size={16} />
|
||||
Add Series
|
||||
</div>
|
||||
</Card>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (!canAddSeries) return;
|
||||
metric.addSeries();
|
||||
}}
|
||||
size='small'
|
||||
type='text'
|
||||
className="w-full cursor-pointer flex items-center py-2 justify-center gap-2 font-medium hover:text-teal btn-add-series"
|
||||
>
|
||||
<PlusIcon size={16} />
|
||||
Add Series
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { Icon } from 'UI';
|
||||
import { Input, Tooltip } from 'antd';
|
||||
import cn from 'classnames';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { NoContent, Loader, Pagination, Button } from 'UI';
|
||||
import Select from 'Shared/Select';
|
||||
import { NoContent, Loader, Pagination } from 'UI';
|
||||
import {Button, Tag, Tooltip, Select, Dropdown} from 'antd';
|
||||
import {UndoOutlined, DownOutlined} from '@ant-design/icons'
|
||||
//import Select from 'Shared/Select';
|
||||
import cn from 'classnames';
|
||||
import { useStore } from 'App/mstore';
|
||||
import SessionItem from 'Shared/SessionItem';
|
||||
|
|
@ -11,7 +13,6 @@ import useIsMounted from 'App/hooks/useIsMounted';
|
|||
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
|
||||
import { numberWithCommas } from 'App/utils';
|
||||
import { HEATMAP } from 'App/constants/card';
|
||||
import { Tag } from 'antd';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
|
|
@ -34,6 +35,15 @@ function WidgetSessions(props: Props) {
|
|||
const filterText = filter.filters.length > 0 ? filter.filters[0].value : '';
|
||||
const metaList = customFieldStore.list.map((i: any) => i.key);
|
||||
|
||||
const seriesDropdownItems = seriesOptions.map((option) => ({
|
||||
key: option.value,
|
||||
label: (
|
||||
<div onClick={() => setActiveSeries(option.value)}>
|
||||
{option.label}
|
||||
</div>
|
||||
)
|
||||
}));
|
||||
|
||||
const writeOption = ({ value }: any) => setActiveSeries(value.value);
|
||||
useEffect(() => {
|
||||
if (!data) return;
|
||||
|
|
@ -119,27 +129,39 @@ function WidgetSessions(props: Props) {
|
|||
<div className={cn(className, 'bg-white p-3 pb-0 rounded-xl shadow-sm border mt-3')}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="flex items-baseline">
|
||||
<div className="flex items-baseline gap-2">
|
||||
<h2 className="text-xl">{metricStore.clickMapSearch ? 'Clicks' : 'Sessions'}</h2>
|
||||
<div className="ml-2 color-gray-medium">
|
||||
{metricStore.clickMapLabel ? `on "${metricStore.clickMapLabel}" ` : null}
|
||||
between <span className="font-medium color-gray-darkest">{startTime}</span> and{' '}
|
||||
<span className="font-medium color-gray-darkest">{endTime}</span>{' '}
|
||||
</div>
|
||||
{hasFilters && <Tooltip title='Clear Drilldown' placement='top'><Button type='text' size='small' onClick={clearFilters}><UndoOutlined /></Button></Tooltip>}
|
||||
</div>
|
||||
|
||||
{hasFilters && widget.metricType === 'table' &&
|
||||
<div className="py-2"><Tag closable onClose={clearFilters}>{filterText}</Tag></div>}
|
||||
{hasFilters && widget.metricType === 'table' && <div className="py-2"><Tag closable onClose={clearFilters}>{filterText}</Tag></div>}
|
||||
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
{hasFilters && <Button variant="text-primary" onClick={clearFilters}>Clear Filters</Button>}
|
||||
{widget.metricType !== 'table' && widget.metricType !== HEATMAP && (
|
||||
<div className="flex items-center ml-6">
|
||||
<span className="mr-2 color-gray-medium">Filter by Series</span>
|
||||
<Select options={seriesOptions} defaultValue={'all'} onChange={writeOption} plain />
|
||||
</div>
|
||||
)}
|
||||
{widget.metricType !== 'table' && widget.metricType !== HEATMAP && (
|
||||
<div className="flex items-center ml-6">
|
||||
<span className="mr-2 color-gray-medium">Filter by Series</span>
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: seriesDropdownItems,
|
||||
selectable: true,
|
||||
selectedKeys: [activeSeries]
|
||||
}}
|
||||
trigger={['click']}
|
||||
>
|
||||
<Button type="text" size='small'>
|
||||
{seriesOptions.find(option => option.value === activeSeries)?.label || 'Select Series'}
|
||||
<DownOutlined />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import { Button, Space, Tooltip } from 'antd';
|
|||
import CardViewMenu from 'Components/Dashboard/components/WidgetView/CardViewMenu';
|
||||
import { Link2 } from 'lucide-react'
|
||||
import copy from 'copy-to-clipboard';
|
||||
import MetricTypeSelector from "../MetricTypeSelector";
|
||||
|
||||
interface Props {
|
||||
onClick?: () => void;
|
||||
|
|
@ -22,6 +21,10 @@ function WidgetViewHeader({ onClick, onSave }: Props) {
|
|||
const { metricStore } = useStore();
|
||||
const widget = metricStore.instance;
|
||||
|
||||
const handleSave = () => {
|
||||
onSave();
|
||||
};
|
||||
|
||||
const copyUrl = () => {
|
||||
const url = window.location.href;
|
||||
copy(url)
|
||||
|
|
@ -36,19 +39,21 @@ function WidgetViewHeader({ onClick, onSave }: Props) {
|
|||
onClick={onClick}
|
||||
>
|
||||
<h1 className="mb-0 text-2xl mr-4 min-w-fit ">
|
||||
<WidgetName
|
||||
name={widget.name}
|
||||
onUpdate={(name) => metricStore.merge({ name })}
|
||||
canEdit={true}
|
||||
/>
|
||||
<WidgetName
|
||||
name={widget.name}
|
||||
onUpdate={(name) => {
|
||||
metricStore.merge({ name });
|
||||
}}
|
||||
canEdit={true}
|
||||
/>
|
||||
</h1>
|
||||
<Space>
|
||||
|
||||
|
||||
<Button
|
||||
type={
|
||||
metricStore.isSaving || (widget.exists() && !widget.hasChanged) ? 'text' : 'primary'
|
||||
}
|
||||
onClick={onSave}
|
||||
onClick={handleSave}
|
||||
loading={metricStore.isSaving}
|
||||
disabled={metricStore.isSaving || (widget.exists() && !widget.hasChanged)}
|
||||
className='font-medium btn-update-card'
|
||||
|
|
@ -56,15 +61,13 @@ function WidgetViewHeader({ onClick, onSave }: Props) {
|
|||
{widget.exists() ? 'Update' : 'Create'}
|
||||
</Button>
|
||||
|
||||
<MetricTypeSelector />
|
||||
|
||||
{/* <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>
|
||||
|
||||
<CardViewMenu />
|
||||
|
||||
|
||||
<CardViewMenu />
|
||||
</Space>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -142,7 +142,7 @@ function FunnelBarData({
|
|||
/>
|
||||
<span
|
||||
className={
|
||||
'mx-1 ' + (data.droppedCount > 0 ? 'color-red' : 'disabled')
|
||||
'mr-1 text-sm' + (data.droppedCount > 0 ? 'color-red' : 'disabled')
|
||||
}
|
||||
>
|
||||
{data.droppedCount} Skipped
|
||||
|
|
@ -183,7 +183,7 @@ export function UxTFunnelBar(props: Props) {
|
|||
backgroundColor: '#6272FF',
|
||||
}}
|
||||
>
|
||||
<div className="color-white absolute right-0 flex items-center font-medium mr-2 leading-3">
|
||||
<div className="color-white absolute right-0 flex items-center font-medium mr-1 leading-3 text-sm">
|
||||
{(
|
||||
(filter.completed / (filter.completed + filter.skipped)) *
|
||||
100
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { getNewIcon } from "../FilterModal/FilterModal";
|
|||
const ASSIST_ROUTE = assistRoute();
|
||||
|
||||
interface Props {
|
||||
filter?: any; // event/filter
|
||||
filter?: any;
|
||||
onFilterClick: (filter: any) => void;
|
||||
children?: any;
|
||||
excludeFilterKeys?: Array<string>;
|
||||
|
|
@ -69,7 +69,7 @@ function FilterSelection(props: Props) {
|
|||
}}
|
||||
onClick={() => setShowModal(true)}
|
||||
>
|
||||
<div className='text-xs text-neutral-500/90 hover:border-neutral-400 '>{getNewIcon(filter)}</div>
|
||||
<div className='text-xs text-neutral-500/90 hover:border-neutral-400'>{getNewIcon(filter)}</div>
|
||||
<div className={'text-neutral-500/90 flex gap-2 hover:border-neutral-400 '}>{`${filter.category} •`}</div>
|
||||
<div
|
||||
className="rounded-lg overflow-hidden whitespace-nowrap text-ellipsis mr-auto truncate "
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue