ui: more chart types, add table with filtering out series, start "compare to" thing
This commit is contained in:
parent
170e85b505
commit
19b5addc95
17 changed files with 1239 additions and 651 deletions
|
|
@ -0,0 +1,83 @@
|
|||
import React from 'react';
|
||||
import CustomTooltip from "./CustomChartTooltip";
|
||||
import { Styles } from '../common';
|
||||
import {
|
||||
ResponsiveContainer,
|
||||
XAxis,
|
||||
YAxis,
|
||||
CartesianGrid,
|
||||
Tooltip,
|
||||
AreaChart,
|
||||
Area,
|
||||
Legend,
|
||||
} from 'recharts';
|
||||
|
||||
interface Props {
|
||||
data: any;
|
||||
params: any;
|
||||
colors: any;
|
||||
onClick?: (event, index) => void;
|
||||
yaxis?: any;
|
||||
label?: string;
|
||||
hideLegend?: boolean;
|
||||
}
|
||||
|
||||
function CustomMetricLineChart(props: Props) {
|
||||
const {
|
||||
data = { chart: [], namesMap: [] },
|
||||
params,
|
||||
colors,
|
||||
onClick = () => null,
|
||||
yaxis = { ...Styles.yaxis },
|
||||
label = 'Number of Sessions',
|
||||
hideLegend = false,
|
||||
} = props;
|
||||
|
||||
|
||||
console.log(data.namesMap, data.chart)
|
||||
return (
|
||||
<ResponsiveContainer height={240} width="100%">
|
||||
<AreaChart
|
||||
data={data.chart}
|
||||
margin={Styles.chartMargins}
|
||||
onClick={onClick}
|
||||
>
|
||||
{!hideLegend && (
|
||||
<Legend iconType={'circle'} wrapperStyle={{ top: -26 }} />
|
||||
)}
|
||||
<CartesianGrid
|
||||
strokeDasharray="3 3"
|
||||
vertical={false}
|
||||
stroke="#EEEEEE"
|
||||
/>
|
||||
<XAxis {...Styles.xaxis} dataKey="time" interval={params.density / 7} />
|
||||
<YAxis
|
||||
{...yaxis}
|
||||
allowDecimals={false}
|
||||
tickFormatter={(val) => Styles.tickFormatter(val)}
|
||||
label={{
|
||||
...Styles.axisLabelLeft,
|
||||
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="monotone"
|
||||
dataKey={key}
|
||||
stroke={colors[index]}
|
||||
color={colors[index]}
|
||||
legendType={key === 'Total' ? 'none' : 'line'}
|
||||
dot={false}
|
||||
// strokeDasharray={'4 3'} FOR COPMARISON ONLY
|
||||
/>
|
||||
))}
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export default CustomMetricLineChart;
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
import React from 'react';
|
||||
import CustomTooltip from "./CustomChartTooltip";
|
||||
import { Styles } from '../common';
|
||||
import {
|
||||
ResponsiveContainer,
|
||||
XAxis,
|
||||
YAxis,
|
||||
CartesianGrid,
|
||||
Tooltip,
|
||||
BarChart,
|
||||
Bar,
|
||||
Legend,
|
||||
Rectangle,
|
||||
} from 'recharts';
|
||||
|
||||
interface Props {
|
||||
data: any;
|
||||
params: any;
|
||||
colors: any;
|
||||
onClick?: (event, index) => void;
|
||||
yaxis?: any;
|
||||
label?: string;
|
||||
hideLegend?: boolean;
|
||||
}
|
||||
const getPath = (x, y, width, height) => {
|
||||
const radius = Math.min(width / 2, height / 2);
|
||||
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}
|
||||
V${y + radius}
|
||||
A${radius},${radius} 0 0 1 ${x + radius},${y}
|
||||
Z
|
||||
`;
|
||||
};
|
||||
|
||||
const PillBar = (props) => {
|
||||
const { fill, x, y, width, height } = props;
|
||||
|
||||
return <path d={getPath(x, y, width, height)} stroke="none" fill={fill} />;
|
||||
};
|
||||
|
||||
|
||||
function CustomMetricLineChart(props: Props) {
|
||||
const {
|
||||
data = { chart: [], namesMap: [] },
|
||||
params,
|
||||
colors,
|
||||
onClick = () => null,
|
||||
yaxis = { ...Styles.yaxis },
|
||||
label = 'Number of Sessions',
|
||||
hideLegend = false,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<ResponsiveContainer height={240} width="100%">
|
||||
<BarChart
|
||||
data={data.chart}
|
||||
margin={Styles.chartMargins}
|
||||
onClick={onClick}
|
||||
>
|
||||
{!hideLegend && (
|
||||
<Legend iconType={'circle'} wrapperStyle={{ top: -26 }} />
|
||||
)}
|
||||
<CartesianGrid
|
||||
strokeDasharray="3 3"
|
||||
vertical={false}
|
||||
stroke="#EEEEEE"
|
||||
/>
|
||||
<XAxis {...Styles.xaxis} dataKey="time" interval={params.density / 7} />
|
||||
<YAxis
|
||||
{...yaxis}
|
||||
allowDecimals={false}
|
||||
tickFormatter={(val) => Styles.tickFormatter(val)}
|
||||
label={{
|
||||
...Styles.axisLabelLeft,
|
||||
value: label || 'Number of Sessions',
|
||||
}}
|
||||
/>
|
||||
<Tooltip {...Styles.tooltip} content={CustomTooltip} />
|
||||
{Array.isArray(data.namesMap) &&
|
||||
data.namesMap.map((key, index) => (
|
||||
<Bar
|
||||
key={key}
|
||||
name={key}
|
||||
type="monotone"
|
||||
dataKey={key}
|
||||
shape={<PillBar />}
|
||||
fill={colors[index]}
|
||||
legendType={key === 'Total' ? 'none' : 'line'}
|
||||
activeBar={<PillBar fill={colors[index]} stroke={colors[index]} />}
|
||||
// strokeDasharray={'4 3'} FOR COPMARISON ONLY
|
||||
/>
|
||||
))}
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export default CustomMetricLineChart;
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
import React from "react";
|
||||
import { formatTimeOrDate } from "App/date";
|
||||
|
||||
function CustomTooltip({ active, payload, label }) {
|
||||
if (!active) return;
|
||||
|
||||
const shownPayloads = payload.filter((p) => !p.hide);
|
||||
return (
|
||||
<div className={'flex flex-col gap-1 bg-white shadow border rounded p-2'}>
|
||||
{shownPayloads.map((p, index) => (
|
||||
<>
|
||||
<div className={'flex gap-2 items-center'}>
|
||||
<div
|
||||
style={{ borderRadius: 99, background: p.color }}
|
||||
className={'h-5 w-5 flex items-center justify-center'}
|
||||
>
|
||||
<div className={'invert text-sm'}>{index + 1}</div>
|
||||
</div>
|
||||
<div className={'font-semibold'}>{p.name}</div>
|
||||
</div>
|
||||
<div
|
||||
style={{ borderLeft: `2px solid ${p.color}` }}
|
||||
className={'flex flex-col py-2 px-2 ml-2'}
|
||||
>
|
||||
<div className={'text-disabled-text text-sm'}>
|
||||
{label}, {formatTimeOrDate(p.payload.timestamp)}
|
||||
</div>
|
||||
<div className={'font-semibold'}>{p.value}</div>
|
||||
</div>
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default CustomTooltip;
|
||||
|
|
@ -2,6 +2,7 @@ import React, { useState } from 'react';
|
|||
import { formatTimeOrDate } from 'App/date';
|
||||
import { Button, Table } from 'antd';
|
||||
import type { TableProps } from 'antd';
|
||||
import CustomTooltip from "../CustomChartTooltip";
|
||||
|
||||
import { Eye, EyeOff } from 'lucide-react';
|
||||
import { Styles } from '../../common';
|
||||
|
|
@ -15,6 +16,7 @@ import {
|
|||
Line,
|
||||
Legend,
|
||||
} from 'recharts';
|
||||
import cn from 'classnames';
|
||||
|
||||
interface Props {
|
||||
data: any;
|
||||
|
|
@ -26,18 +28,6 @@ interface Props {
|
|||
hideLegend?: boolean;
|
||||
}
|
||||
|
||||
const initTableProps = [{
|
||||
title: 'Series',
|
||||
dataIndex: 'seriesName',
|
||||
key: 'seriesName',
|
||||
},
|
||||
{
|
||||
title: 'Avg.',
|
||||
dataIndex: 'average',
|
||||
key: 'average',
|
||||
}
|
||||
]
|
||||
|
||||
function CustomMetricLineChart(props: Props) {
|
||||
const {
|
||||
data = { chart: [], namesMap: [] },
|
||||
|
|
@ -48,176 +38,55 @@ function CustomMetricLineChart(props: Props) {
|
|||
label = 'Number of Sessions',
|
||||
hideLegend = false,
|
||||
} = props;
|
||||
const [showTable, setShowTable] = useState(false);
|
||||
const hasMultipleSeries = data.namesMap.length > 1;
|
||||
const [tableData, setTableData] = useState([]);
|
||||
const [tableProps, setTableProps] = useState<TableProps['columns']>(initTableProps);
|
||||
// console.log(params.density / 7, data.chart)
|
||||
|
||||
const columnNames = new Set();
|
||||
/**
|
||||
* basically we have an array of
|
||||
* { time: some_date, series1: 1, series2: 2, series3: 3, timestamp: 123456 }
|
||||
* which we turn into a table where each series of filters = row;
|
||||
* and each unique time = column
|
||||
* + average for each row
|
||||
* [ { seriesName: 'series1', mon: 1, tue: 2, wed: 3, average: 2 }, ... ]
|
||||
* */
|
||||
React.useEffect(()=> {
|
||||
setTableProps(initTableProps)
|
||||
const series = Object.keys(data.chart[0])
|
||||
.filter((key) => key !== 'time' && key !== 'timestamp')
|
||||
columnNames.clear()
|
||||
data.chart.forEach((p: any) => {
|
||||
columnNames.add(p.time)
|
||||
}) // for example: mon, tue, wed, thu, fri, sat, sun
|
||||
const avg: any = {} // { seriesName: {itemsCount: 0, total: 0} }
|
||||
const items: Record<string, any>[] = []; // as many items (rows) as we have series in filter
|
||||
series.forEach(s => {
|
||||
items.push({ seriesName: s, average: 0 })
|
||||
avg[s] = { itemsCount: 0, total: 0 }
|
||||
})
|
||||
const tableCols: { title: string, dataIndex: string, key: string }[] = [];
|
||||
Array.from(columnNames).forEach((name: string) => {
|
||||
tableCols.push({
|
||||
title: name,
|
||||
dataIndex: name,
|
||||
key: name,
|
||||
})
|
||||
const values = data.chart.filter((p) => p.time === name)
|
||||
series.forEach((s) => {
|
||||
const toDateAvg = values.reduce((acc, curr) => acc + curr[s], 0) / values.length;
|
||||
avg[s].itemsCount += 1
|
||||
avg[s].total += toDateAvg
|
||||
const ind = items.findIndex((item) => item.seriesName === s)
|
||||
if (ind === -1) return
|
||||
items[ind][name] = (values.reduce((acc, curr) => acc + curr[s], 0) / values.length)
|
||||
.toFixed(2)
|
||||
})
|
||||
})
|
||||
Object.keys(avg).forEach((key) => {
|
||||
const ind = items.findIndex((item) => item.seriesName === key)
|
||||
if (ind === -1) return
|
||||
items[ind].average = (avg[key].total / avg[key].itemsCount).toFixed(2)
|
||||
})
|
||||
|
||||
setTableProps((prev) => [...prev, ...tableCols])
|
||||
setTableData(items)
|
||||
}, [data.chart.length])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ResponsiveContainer height={240} width="100%">
|
||||
<LineChart
|
||||
data={data.chart}
|
||||
margin={Styles.chartMargins}
|
||||
onClick={onClick}
|
||||
>
|
||||
{!hideLegend && (
|
||||
<Legend iconType={'circle'} wrapperStyle={{ top: -26 }} />
|
||||
)}
|
||||
<CartesianGrid
|
||||
strokeDasharray="3 3"
|
||||
vertical={false}
|
||||
stroke="#EEEEEE"
|
||||
/>
|
||||
<XAxis
|
||||
{...Styles.xaxis}
|
||||
dataKey="time"
|
||||
interval={params.density / 7}
|
||||
/>
|
||||
<YAxis
|
||||
{...yaxis}
|
||||
allowDecimals={false}
|
||||
tickFormatter={(val) => Styles.tickFormatter(val)}
|
||||
label={{
|
||||
...Styles.axisLabelLeft,
|
||||
value: label || 'Number of Sessions',
|
||||
}}
|
||||
/>
|
||||
<Tooltip {...Styles.tooltip} content={CustomTooltip} />
|
||||
{Array.isArray(data.namesMap) &&
|
||||
data.namesMap.map((key, index) => (
|
||||
<Line
|
||||
key={key}
|
||||
name={key}
|
||||
type="monotone"
|
||||
dataKey={key}
|
||||
stroke={colors[index]}
|
||||
fillOpacity={1}
|
||||
strokeWidth={2}
|
||||
strokeOpacity={key === 'Total' ? 0 : 0.6}
|
||||
legendType={key === 'Total' ? 'none' : 'line'}
|
||||
dot={false}
|
||||
// strokeDasharray={'4 3'} FOR COPMARISON ONLY
|
||||
activeDot={{
|
||||
fill: key === 'Total' ? 'transparent' : colors[index],
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
{hasMultipleSeries ? (
|
||||
<div className={'relative -mx-4 px-4'}>
|
||||
<div
|
||||
className={
|
||||
'absolute left-0 right-0 top-0 border-t border-t-gray-lighter'
|
||||
}
|
||||
/>
|
||||
<div className={'absolute top-0 left-1/2 z-10'} style={{ transform: 'translate(-50%, -50%)' }}>
|
||||
<Button
|
||||
icon={showTable ? <EyeOff size={16} /> : <Eye size={16} />}
|
||||
size={'small'}
|
||||
type={'default'}
|
||||
onClick={() => setShowTable(!showTable)}
|
||||
>
|
||||
{showTable ? 'Hide Table' : 'Show Table'}
|
||||
</Button>
|
||||
</div>
|
||||
{showTable ? (
|
||||
<Table
|
||||
columns={tableProps}
|
||||
dataSource={tableData}
|
||||
pagination={false}
|
||||
size={'small'}
|
||||
className={'py-6'}
|
||||
<ResponsiveContainer height={240} width="100%">
|
||||
<LineChart
|
||||
data={data.chart}
|
||||
margin={Styles.chartMargins}
|
||||
onClick={onClick}
|
||||
>
|
||||
{!hideLegend && (
|
||||
<Legend iconType={'circle'} wrapperStyle={{ top: -26 }} />
|
||||
)}
|
||||
<CartesianGrid
|
||||
strokeDasharray="3 3"
|
||||
vertical={false}
|
||||
stroke="#EEEEEE"
|
||||
/>
|
||||
<XAxis {...Styles.xaxis} dataKey="time" interval={params.density / 7} />
|
||||
<YAxis
|
||||
{...yaxis}
|
||||
allowDecimals={false}
|
||||
tickFormatter={(val) => Styles.tickFormatter(val)}
|
||||
label={{
|
||||
...Styles.axisLabelLeft,
|
||||
value: label || 'Number of Sessions',
|
||||
}}
|
||||
/>
|
||||
<Tooltip {...Styles.tooltip} content={CustomTooltip} />
|
||||
{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}
|
||||
// strokeDasharray={'4 3'} FOR COPMARISON ONLY
|
||||
activeDot={{
|
||||
fill: key === 'Total' ? 'transparent' : colors[index],
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CustomTooltip({ active, payload, label }) {
|
||||
if (!active) return;
|
||||
|
||||
const shownPayloads = payload.filter((p) => !p.hide);
|
||||
return (
|
||||
<div className={'flex flex-col gap-1 bg-white shadow border rounded p-2'}>
|
||||
{shownPayloads.map((p, index) => (
|
||||
<>
|
||||
<div className={'flex gap-2 items-center'}>
|
||||
<div
|
||||
style={{ borderRadius: 99, background: p.color }}
|
||||
className={'h-5 w-5 flex items-center justify-center'}
|
||||
>
|
||||
<div className={'invert text-sm'}>{index + 1}</div>
|
||||
</div>
|
||||
<div className={'font-semibold'}>{p.name}</div>
|
||||
</div>
|
||||
<div
|
||||
style={{ borderLeft: `2px solid ${p.color}` }}
|
||||
className={'flex flex-col py-2 px-2 ml-2'}
|
||||
>
|
||||
<div className={'text-disabled-text text-sm'}>
|
||||
{label}, {formatTimeOrDate(p.payload.timestamp)}
|
||||
</div>
|
||||
<div className={'font-semibold'}>{p.value}</div>
|
||||
</div>
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
) : null)}
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,126 +1,156 @@
|
|||
//@ts-nocheck
|
||||
import React from 'react'
|
||||
import React from 'react';
|
||||
import { ResponsiveContainer, Tooltip } from 'recharts';
|
||||
import { PieChart, Pie, Cell } 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';
|
||||
interface Props {
|
||||
metric: any,
|
||||
data: any;
|
||||
colors: any;
|
||||
onClick?: (filters) => void;
|
||||
metric: any;
|
||||
data: any;
|
||||
colors: any;
|
||||
onClick?: (filters) => void;
|
||||
}
|
||||
|
||||
function CustomMetricPieChart(props: Props) {
|
||||
const { metric, data = { values: [] }, onClick = () => null } = props;
|
||||
const { metric, data, onClick = () => null } = props;
|
||||
|
||||
const onClickHandler = (event) => {
|
||||
if (event && !event.payload.group) {
|
||||
const filters = Array<any>();
|
||||
let filter = { ...filtersMap[metric.metricOf] }
|
||||
filter.value = [event.payload.name]
|
||||
filter.type = filter.key
|
||||
delete filter.key
|
||||
delete filter.operatorOptions
|
||||
delete filter.category
|
||||
delete filter.icon
|
||||
delete filter.label
|
||||
delete filter.options
|
||||
const onClickHandler = (event) => {
|
||||
if (event && !event.payload.group) {
|
||||
const filters = Array<any>();
|
||||
let filter = { ...filtersMap[metric.metricOf] };
|
||||
filter.value = [event.payload.name];
|
||||
filter.type = filter.key;
|
||||
delete filter.key;
|
||||
delete filter.operatorOptions;
|
||||
delete filter.category;
|
||||
delete filter.icon;
|
||||
delete filter.label;
|
||||
delete filter.options;
|
||||
|
||||
filters.push(filter);
|
||||
onClick(filters);
|
||||
}
|
||||
filters.push(filter);
|
||||
onClick(filters);
|
||||
}
|
||||
return (
|
||||
<NoContent size="small" title="No data available" show={!data.values || data.values.length === 0} style={{ minHeight: '240px'}}>
|
||||
<ResponsiveContainer height={ 220 } width="100%">
|
||||
<PieChart>
|
||||
<Pie
|
||||
isAnimationActive={ false }
|
||||
data={data.values}
|
||||
dataKey="sessionCount"
|
||||
nameKey="name"
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
// innerRadius={40}
|
||||
outerRadius={70}
|
||||
// fill={colors[0]}
|
||||
activeIndex={1}
|
||||
onClick={onClickHandler}
|
||||
labelLine={({
|
||||
cx,
|
||||
cy,
|
||||
midAngle,
|
||||
innerRadius,
|
||||
outerRadius,
|
||||
value,
|
||||
}) => {
|
||||
const RADIAN = Math.PI / 180;
|
||||
let radius1 = 15 + innerRadius + (outerRadius - innerRadius);
|
||||
let radius2 = innerRadius + (outerRadius - innerRadius);
|
||||
let x2 = cx + radius1 * Math.cos(-midAngle * RADIAN);
|
||||
let y2 = cy + radius1 * Math.sin(-midAngle * RADIAN);
|
||||
let x1 = cx + radius2 * Math.cos(-midAngle * RADIAN);
|
||||
let y1 = cy + radius2 * Math.sin(-midAngle * RADIAN);
|
||||
};
|
||||
|
||||
const percentage = value * 100 / data.values.reduce((a, b) => a + b.sessionCount, 0);
|
||||
|
||||
if (percentage<3){
|
||||
return null;
|
||||
}
|
||||
|
||||
return(
|
||||
<line x1={x1} y1={y1} x2={x2} y2={y2} stroke="#3EAAAF" strokeWidth={1} />
|
||||
)
|
||||
}}
|
||||
label={({
|
||||
cx,
|
||||
cy,
|
||||
midAngle,
|
||||
innerRadius,
|
||||
outerRadius,
|
||||
value,
|
||||
index
|
||||
}) => {
|
||||
const RADIAN = Math.PI / 180;
|
||||
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 / data.values.reduce((a, b) => a + b.sessionCount, 0)) * 100;
|
||||
let name = data.values[index].name || 'Unidentified';
|
||||
name = name.length > 20 ? name.substring(0, 20) + '...' : name;
|
||||
if (percentage<3){
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<text
|
||||
x={x}
|
||||
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'
|
||||
>
|
||||
{name || 'Unidentified'} {numberWithCommas(value)}
|
||||
</text>
|
||||
);
|
||||
}}
|
||||
>
|
||||
{data && data.values && data.values.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={Styles.colorsPie[index % Styles.colorsPie.length]} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip {...Styles.tooltip} />
|
||||
</PieChart>
|
||||
|
||||
</ResponsiveContainer>
|
||||
<div className="text-sm color-gray-medium">Top 5 </div>
|
||||
</NoContent>
|
||||
)
|
||||
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 });
|
||||
return (
|
||||
<NoContent
|
||||
size="small"
|
||||
title="No data available"
|
||||
show={!data.chart || data.chart.length === 0}
|
||||
style={{ minHeight: '240px' }}
|
||||
>
|
||||
<ResponsiveContainer height={240} width="100%">
|
||||
<PieChart>
|
||||
<Legend iconType={'circle'} wrapperStyle={{ top: -26 }} />
|
||||
<Pie
|
||||
isAnimationActive={false}
|
||||
data={values}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
innerRadius={40}
|
||||
outerRadius={70}
|
||||
// fill={colors[0]}
|
||||
activeIndex={1}
|
||||
onClick={onClickHandler}
|
||||
labelLine={({
|
||||
cx,
|
||||
cy,
|
||||
midAngle,
|
||||
innerRadius,
|
||||
outerRadius,
|
||||
value,
|
||||
}) => {
|
||||
const RADIAN = Math.PI / 180;
|
||||
let radius1 = 15 + innerRadius + (outerRadius - innerRadius);
|
||||
let radius2 = innerRadius + (outerRadius - innerRadius);
|
||||
let x2 = cx + radius1 * Math.cos(-midAngle * RADIAN);
|
||||
let y2 = cy + radius1 * Math.sin(-midAngle * RADIAN);
|
||||
let x1 = cx + radius2 * Math.cos(-midAngle * RADIAN);
|
||||
let y1 = cy + radius2 * Math.sin(-midAngle * RADIAN);
|
||||
|
||||
const percentage =
|
||||
(value * 100) /
|
||||
highest.value;
|
||||
|
||||
if (percentage < 3) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<line
|
||||
x1={x1}
|
||||
y1={y1}
|
||||
x2={x2}
|
||||
y2={y2}
|
||||
stroke="#3EAAAF"
|
||||
strokeWidth={1}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
label={({
|
||||
cx,
|
||||
cy,
|
||||
midAngle,
|
||||
innerRadius,
|
||||
outerRadius,
|
||||
value,
|
||||
index,
|
||||
}) => {
|
||||
const RADIAN = Math.PI / 180;
|
||||
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;
|
||||
let name = values[index].name || 'Unidentified';
|
||||
name = name.length > 20 ? name.substring(0, 20) + '...' : name;
|
||||
if (percentage < 3) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<text
|
||||
x={x}
|
||||
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"
|
||||
>
|
||||
{numberWithCommas(value)}
|
||||
</text>
|
||||
);
|
||||
}}
|
||||
>
|
||||
{values && values.map((entry, index) => (
|
||||
<Cell
|
||||
key={`cell-${index}`}
|
||||
fill={Styles.colorsPie[index % Styles.colorsPie.length]}
|
||||
/>
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip {...Styles.tooltip} />
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</NoContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default CustomMetricPieChart;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,57 @@
|
|||
import React from 'react';
|
||||
|
||||
interface Props {
|
||||
data: { chart: any[], namesMap: string[] };
|
||||
params: any;
|
||||
colors: any;
|
||||
onClick?: (event, index) => void;
|
||||
yaxis?: any;
|
||||
label?: string;
|
||||
hideLegend?: boolean;
|
||||
}
|
||||
|
||||
function ProgressBarChart(props: Props) {
|
||||
const {
|
||||
data = { chart: [], namesMap: [] },
|
||||
colors,
|
||||
onClick = () => null,
|
||||
label = 'Number of Sessions',
|
||||
} = props;
|
||||
|
||||
const getTotalForSeries = (series: string) => {
|
||||
return data.chart.reduce((acc, curr) => acc + curr[series], 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 formattedNumber = (num: number) => {
|
||||
return Intl.NumberFormat().format(num);
|
||||
}
|
||||
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[i] }} />
|
||||
<span>{val.name}</span>
|
||||
</div>
|
||||
<div className={'flex items-center gap-2'} style={{ flex: 4 }}>
|
||||
<div style={{ height: 16, borderRadius: 16, backgroundColor: colors[i], width: `${(val.value/highest.value)*100}%` }} />
|
||||
<div>{formattedNumber(val.value)}</div>
|
||||
</div>
|
||||
<div style={{ flex: 1}}/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ProgressBarChart;
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { EventsList } from 'Shared/Filters/FilterList';
|
||||
import { EventsList, FilterList } from 'Shared/Filters/FilterList';
|
||||
import SeriesName from './SeriesName';
|
||||
import cn from 'classnames';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
|
@ -203,6 +203,7 @@ function FilterSeries(props: Props) {
|
|||
)}
|
||||
|
||||
{expanded ? (
|
||||
<>
|
||||
<EventsList
|
||||
filter={series.filter}
|
||||
onUpdateFilter={onUpdateFilter}
|
||||
|
|
@ -213,7 +214,20 @@ function FilterSeries(props: Props) {
|
|||
excludeFilterKeys={excludeFilterKeys}
|
||||
onAddFilter={onAddFilter}
|
||||
mergeUp={!hideHeader}
|
||||
mergeDown
|
||||
/>
|
||||
<FilterList
|
||||
filter={series.filter}
|
||||
onUpdateFilter={onUpdateFilter}
|
||||
onRemoveFilter={onRemoveFilter}
|
||||
onChangeEventsOrder={onChangeEventsOrder}
|
||||
supportsEmpty={supportsEmpty}
|
||||
onFilterMove={onFilterMove}
|
||||
excludeFilterKeys={excludeFilterKeys}
|
||||
onAddFilter={onAddFilter}
|
||||
mergeUp
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,26 +1,30 @@
|
|||
import React, {useState, useRef, useEffect} from 'react';
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import CustomMetricLineChart from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricLineChart';
|
||||
import CustomMetricPercentage from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricPercentage';
|
||||
import CustomMetricPieChart from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricPieChart';
|
||||
import {Styles} from 'App/components/Dashboard/Widgets/common';
|
||||
import {observer} from 'mobx-react-lite';
|
||||
import { Styles } from 'App/components/Dashboard/Widgets/common';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { Icon, Loader } from 'UI';
|
||||
import {useStore} from 'App/mstore';
|
||||
import { useStore } from 'App/mstore';
|
||||
import AreaChart from '../../Widgets/CustomMetricsWidgets/AreaChart';
|
||||
import BarChart from '../../Widgets/CustomMetricsWidgets/BarChart';
|
||||
import ProgressBarChart from '../../Widgets/CustomMetricsWidgets/ProgressBarChart';
|
||||
|
||||
import WidgetDatatable from '../WidgetDatatable/WidgetDatatable';
|
||||
import WidgetPredefinedChart from '../WidgetPredefinedChart';
|
||||
import CustomMetricOverviewChart from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricOverviewChart';
|
||||
import {getStartAndEndTimestampsByDensity} from 'Types/dashboard/helper';
|
||||
import {debounce} from 'App/utils';
|
||||
import { getStartAndEndTimestampsByDensity } from 'Types/dashboard/helper';
|
||||
import { debounce } from 'App/utils';
|
||||
import useIsMounted from 'App/hooks/useIsMounted';
|
||||
import {FilterKey} from 'Types/filter/filterType';
|
||||
import { FilterKey } from 'Types/filter/filterType';
|
||||
import {
|
||||
TIMESERIES,
|
||||
TABLE,
|
||||
HEATMAP,
|
||||
FUNNEL,
|
||||
ERRORS,
|
||||
INSIGHTS,
|
||||
USER_PATH,
|
||||
RETENTION
|
||||
TIMESERIES,
|
||||
TABLE,
|
||||
HEATMAP,
|
||||
FUNNEL,
|
||||
ERRORS,
|
||||
INSIGHTS,
|
||||
USER_PATH,
|
||||
RETENTION,
|
||||
} from 'App/constants/card';
|
||||
import FunnelWidget from 'App/components/Funnels/FunnelWidget';
|
||||
import CustomMetricTableSessions from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricTableSessions';
|
||||
|
|
@ -29,245 +33,367 @@ import ClickMapCard from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/
|
|||
import InsightsCard from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/InsightsCard';
|
||||
import SankeyChart from 'Shared/Insights/SankeyChart';
|
||||
import CohortCard from '../../Widgets/CustomMetricsWidgets/CohortCard';
|
||||
import SessionsBy from "Components/Dashboard/Widgets/CustomMetricsWidgets/SessionsBy";
|
||||
import { useInView } from "react-intersection-observer";
|
||||
import SessionsBy from 'Components/Dashboard/Widgets/CustomMetricsWidgets/SessionsBy';
|
||||
import { useInView } from 'react-intersection-observer';
|
||||
|
||||
interface Props {
|
||||
metric: any;
|
||||
isSaved?: boolean;
|
||||
isTemplate?: boolean;
|
||||
isPreview?: boolean;
|
||||
metric: any;
|
||||
isSaved?: boolean;
|
||||
isTemplate?: boolean;
|
||||
isPreview?: boolean;
|
||||
}
|
||||
|
||||
function WidgetChart(props: Props) {
|
||||
const { ref, inView } = useInView({
|
||||
triggerOnce: true,
|
||||
rootMargin: "200px 0px",
|
||||
});
|
||||
const {isSaved = false, metric, isTemplate} = props;
|
||||
const {dashboardStore, metricStore, sessionStore} = useStore();
|
||||
const _metric: any = metricStore.instance;
|
||||
const period = dashboardStore.period;
|
||||
const drillDownPeriod = dashboardStore.drillDownPeriod;
|
||||
const drillDownFilter = dashboardStore.drillDownFilter;
|
||||
const colors = Styles.customMetricColors;
|
||||
const [loading, setLoading] = useState(true);
|
||||
const params = {density: 70};
|
||||
const metricParams = {...params};
|
||||
const prevMetricRef = useRef<any>();
|
||||
const isMounted = useIsMounted();
|
||||
const [data, setData] = useState<any>(metric.data);
|
||||
const isTableWidget = metric.metricType === 'table' && metric.viewType === 'table';
|
||||
const isPieChart = metric.metricType === 'table' && metric.viewType === 'pieChart';
|
||||
const { ref, inView } = useInView({
|
||||
triggerOnce: true,
|
||||
rootMargin: '200px 0px',
|
||||
});
|
||||
const { isSaved = false, metric, isTemplate } = props;
|
||||
const { dashboardStore, metricStore, sessionStore } = useStore();
|
||||
const _metric: any = metricStore.instance;
|
||||
const period = dashboardStore.period;
|
||||
const drillDownPeriod = dashboardStore.drillDownPeriod;
|
||||
const drillDownFilter = dashboardStore.drillDownFilter;
|
||||
const colors = Styles.customMetricColors;
|
||||
const [loading, setLoading] = useState(true);
|
||||
const params = { density: 70 };
|
||||
const metricParams = { ...params };
|
||||
const prevMetricRef = useRef<any>();
|
||||
const isMounted = useIsMounted();
|
||||
const [data, setData] = useState<any>(metric.data);
|
||||
const [enabledRows, setEnabledRows] = useState([]);
|
||||
const isTableWidget =
|
||||
metric.metricType === 'table' && metric.viewType === 'table';
|
||||
const isPieChart =
|
||||
metric.metricType === 'table' && metric.viewType === 'pieChart';
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
dashboardStore.resetDrillDownFilter();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const onChartClick = (event: any) => {
|
||||
if (event) {
|
||||
if (isTableWidget || isPieChart) { // get the filter of clicked row
|
||||
const periodTimestamps = drillDownPeriod.toTimestamps();
|
||||
drillDownFilter.merge({
|
||||
filters: event,
|
||||
startTimestamp: periodTimestamps.startTimestamp,
|
||||
endTimestamp: periodTimestamps.endTimestamp
|
||||
});
|
||||
} else { // get the filter of clicked chart point
|
||||
const payload = event.activePayload[0].payload;
|
||||
const timestamp = payload.timestamp;
|
||||
const periodTimestamps = getStartAndEndTimestampsByDensity(timestamp, drillDownPeriod.start, drillDownPeriod.end, params.density);
|
||||
|
||||
drillDownFilter.merge({
|
||||
startTimestamp: periodTimestamps.startTimestamp,
|
||||
endTimestamp: periodTimestamps.endTimestamp
|
||||
});
|
||||
}
|
||||
}
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
dashboardStore.resetDrillDownFilter();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const depsString = JSON.stringify({
|
||||
..._metric.series, ..._metric.excludes, ..._metric.startPoint,
|
||||
hideExcess: _metric.hideExcess
|
||||
});
|
||||
const fetchMetricChartData = (metric: any, payload: any, isSaved: any, period: any) => {
|
||||
if (!isMounted()) return;
|
||||
setLoading(true);
|
||||
dashboardStore.fetchMetricChartData(metric, payload, isSaved, period).then((res: any) => {
|
||||
if (isMounted()) setData(res);
|
||||
}).finally(() => {
|
||||
setLoading(false);
|
||||
useEffect(() => {
|
||||
const series = data.chart[0] ? Object.keys(data.chart[0]).filter(
|
||||
(key) => key !== 'time' && key !== 'timestamp'
|
||||
) : []
|
||||
if (series.length) {
|
||||
setEnabledRows(series)
|
||||
}
|
||||
}, [data.chart.length])
|
||||
|
||||
const onChartClick = (event: any) => {
|
||||
if (event) {
|
||||
if (isTableWidget || isPieChart) {
|
||||
// get the filter of clicked row
|
||||
const periodTimestamps = drillDownPeriod.toTimestamps();
|
||||
drillDownFilter.merge({
|
||||
filters: event,
|
||||
startTimestamp: periodTimestamps.startTimestamp,
|
||||
endTimestamp: periodTimestamps.endTimestamp,
|
||||
});
|
||||
};
|
||||
} else {
|
||||
// get the filter of clicked chart point
|
||||
const payload = event.activePayload[0].payload;
|
||||
const timestamp = payload.timestamp;
|
||||
const periodTimestamps = getStartAndEndTimestampsByDensity(
|
||||
timestamp,
|
||||
drillDownPeriod.start,
|
||||
drillDownPeriod.end,
|
||||
params.density
|
||||
);
|
||||
|
||||
const debounceRequest: any = React.useCallback(debounce(fetchMetricChartData, 500), []);
|
||||
const loadPage = () => {
|
||||
if (!inView) return;
|
||||
if (prevMetricRef.current && prevMetricRef.current.name !== metric.name) {
|
||||
prevMetricRef.current = metric;
|
||||
return;
|
||||
}
|
||||
prevMetricRef.current = metric;
|
||||
const timestmaps = drillDownPeriod.toTimestamps();
|
||||
const payload = isSaved ? {...params} : {...metricParams, ...timestmaps, ...metric.toJson()};
|
||||
debounceRequest(metric, payload, isSaved, !isSaved ? drillDownPeriod : period);
|
||||
};
|
||||
useEffect(() => {
|
||||
_metric.updateKey('page', 1);
|
||||
loadPage();
|
||||
}, [
|
||||
drillDownPeriod,
|
||||
period,
|
||||
depsString,
|
||||
metric.metricType,
|
||||
metric.metricOf,
|
||||
metric.viewType,
|
||||
metric.metricValue,
|
||||
metric.startType,
|
||||
metric.metricFormat,
|
||||
inView,
|
||||
]);
|
||||
useEffect(loadPage, [_metric.page]);
|
||||
drillDownFilter.merge({
|
||||
startTimestamp: periodTimestamps.startTimestamp,
|
||||
endTimestamp: periodTimestamps.endTimestamp,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const depsString = JSON.stringify({
|
||||
..._metric.series,
|
||||
..._metric.excludes,
|
||||
..._metric.startPoint,
|
||||
hideExcess: _metric.hideExcess,
|
||||
});
|
||||
const fetchMetricChartData = (
|
||||
metric: any,
|
||||
payload: any,
|
||||
isSaved: any,
|
||||
period: any
|
||||
) => {
|
||||
if (!isMounted()) return;
|
||||
setLoading(true);
|
||||
dashboardStore
|
||||
.fetchMetricChartData(metric, payload, isSaved, period)
|
||||
.then((res: any) => {
|
||||
if (isMounted()) setData(res);
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
const renderChart = () => {
|
||||
const {metricType, viewType, metricOf} = metric;
|
||||
const metricWithData = {...metric, data};
|
||||
|
||||
if (metricType === FUNNEL) {
|
||||
return <FunnelWidget metric={metric} data={data} isWidget={isSaved || isTemplate}/>;
|
||||
}
|
||||
|
||||
if (metricType === 'predefined' || metricType === ERRORS) {
|
||||
const defaultMetric = metric.data.chart && metric.data.chart.length === 0 ? metricWithData : metric;
|
||||
return <WidgetPredefinedChart isTemplate={isTemplate} metric={defaultMetric} data={data}
|
||||
predefinedKey={metric.metricOf}/>;
|
||||
}
|
||||
|
||||
if (metricType === TIMESERIES) {
|
||||
if (viewType === 'lineChart') {
|
||||
return (
|
||||
<CustomMetricLineChart
|
||||
data={data}
|
||||
colors={colors}
|
||||
params={params}
|
||||
onClick={onChartClick}
|
||||
label={metric.metricOf === 'sessionCount' ? 'Number of Sessions' : 'Number of Users'}
|
||||
/>
|
||||
);
|
||||
} else if (viewType === 'progress') {
|
||||
return (
|
||||
<CustomMetricPercentage
|
||||
data={data[0]}
|
||||
colors={colors}
|
||||
params={params}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (metricType === TABLE) {
|
||||
if (metricOf === FilterKey.SESSIONS) {
|
||||
return (
|
||||
<CustomMetricTableSessions
|
||||
metric={metric}
|
||||
data={data}
|
||||
isTemplate={isTemplate}
|
||||
isEdit={!isSaved && !isTemplate}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (metricOf === FilterKey.ERRORS) {
|
||||
return (
|
||||
<CustomMetricTableErrors
|
||||
metric={metric}
|
||||
data={data}
|
||||
// isTemplate={isTemplate}
|
||||
isEdit={!isSaved && !isTemplate}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (viewType === TABLE) {
|
||||
return (
|
||||
<SessionsBy
|
||||
metric={metric}
|
||||
data={data[0]}
|
||||
onClick={onChartClick}
|
||||
isTemplate={isTemplate}
|
||||
/>
|
||||
);
|
||||
} else if (viewType === 'pieChart') {
|
||||
return (
|
||||
<CustomMetricPieChart
|
||||
metric={metric}
|
||||
data={data[0]}
|
||||
colors={colors}
|
||||
// params={params}
|
||||
onClick={onChartClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
if (metricType === HEATMAP) {
|
||||
if (!props.isPreview) {
|
||||
return metric.thumbnail ? (
|
||||
<div style={{height: '229px', overflow: 'hidden', marginBottom: '10px'}}>
|
||||
<img src={metric.thumbnail} alt='clickmap thumbnail'/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center relative justify-center" style={{ height: '229px'}}>
|
||||
<Icon name="info-circle" className="mr-2" size="14" />
|
||||
No data available for the selected period.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<ClickMapCard />
|
||||
);
|
||||
}
|
||||
|
||||
if (metricType === INSIGHTS) {
|
||||
return <InsightsCard data={data} />;
|
||||
}
|
||||
|
||||
if (metricType === USER_PATH && data && data.links) {
|
||||
// return <PathAnalysis data={data}/>;
|
||||
return <SankeyChart
|
||||
height={props.isPreview ? 500 : 240}
|
||||
data={data}
|
||||
onChartClick={(filters: any) => {
|
||||
dashboardStore.drillDownFilter.merge({filters, page: 1});
|
||||
}}/>;
|
||||
}
|
||||
|
||||
if (metricType === RETENTION) {
|
||||
if (viewType === 'trend') {
|
||||
return (
|
||||
<CustomMetricLineChart
|
||||
data={data}
|
||||
colors={colors}
|
||||
params={params}
|
||||
onClick={onChartClick}
|
||||
/>
|
||||
);
|
||||
} else if (viewType === 'cohort') {
|
||||
return (
|
||||
<CohortCard data={data[0]}/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return <div>Unknown metric type</div>;
|
||||
};
|
||||
return (
|
||||
<div ref={ref}>
|
||||
<Loader loading={loading} style={{height: `240px`}}>
|
||||
<div style={{minHeight: 240}}>{renderChart()}</div>
|
||||
</Loader>
|
||||
</div>
|
||||
const debounceRequest: any = React.useCallback(
|
||||
debounce(fetchMetricChartData, 500),
|
||||
[]
|
||||
);
|
||||
const loadPage = () => {
|
||||
if (!inView) return;
|
||||
if (prevMetricRef.current && prevMetricRef.current.name !== metric.name) {
|
||||
prevMetricRef.current = metric;
|
||||
return;
|
||||
}
|
||||
prevMetricRef.current = metric;
|
||||
const timestmaps = drillDownPeriod.toTimestamps();
|
||||
const payload = isSaved
|
||||
? { ...params }
|
||||
: { ...metricParams, ...timestmaps, ...metric.toJson() };
|
||||
debounceRequest(
|
||||
metric,
|
||||
payload,
|
||||
isSaved,
|
||||
!isSaved ? drillDownPeriod : period
|
||||
);
|
||||
};
|
||||
useEffect(() => {
|
||||
_metric.updateKey('page', 1);
|
||||
loadPage();
|
||||
}, [
|
||||
drillDownPeriod,
|
||||
period,
|
||||
depsString,
|
||||
metric.metricType,
|
||||
metric.metricOf,
|
||||
metric.metricValue,
|
||||
metric.startType,
|
||||
metric.metricFormat,
|
||||
inView,
|
||||
]);
|
||||
useEffect(loadPage, [_metric.page]);
|
||||
|
||||
const renderChart = () => {
|
||||
const { metricType, metricOf } = metric;
|
||||
const viewType = metric.viewType;
|
||||
const metricWithData = { ...metric, data };
|
||||
|
||||
if (metricType === FUNNEL) {
|
||||
return (
|
||||
<FunnelWidget
|
||||
metric={metric}
|
||||
data={data}
|
||||
isWidget={isSaved || isTemplate}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (metricType === 'predefined' || metricType === ERRORS) {
|
||||
const defaultMetric =
|
||||
metric.data.chart && metric.data.chart.length === 0
|
||||
? metricWithData
|
||||
: metric;
|
||||
return (
|
||||
<WidgetPredefinedChart
|
||||
isTemplate={isTemplate}
|
||||
metric={defaultMetric}
|
||||
data={data}
|
||||
predefinedKey={metric.metricOf}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (metricType === TIMESERIES) {
|
||||
const chartData = { ...data };
|
||||
chartData.namesMap = Array.isArray(chartData.namesMap) ? chartData.namesMap.map(n => enabledRows.includes(n) ? n : null) : chartData.namesMap
|
||||
if (viewType === 'lineChart') {
|
||||
return (
|
||||
<CustomMetricLineChart
|
||||
data={chartData}
|
||||
colors={colors}
|
||||
params={params}
|
||||
onClick={onChartClick}
|
||||
label={
|
||||
metric.metricOf === 'sessionCount'
|
||||
? 'Number of Sessions'
|
||||
: 'Number of Users'
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (viewType === 'areaChart') {
|
||||
return (
|
||||
<AreaChart
|
||||
data={chartData}
|
||||
params={params}
|
||||
colors={colors}
|
||||
onClick={onChartClick}
|
||||
label={
|
||||
metric.metricOf === 'sessionCount'
|
||||
? 'Number of Sessions'
|
||||
: 'Number of Users'
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (viewType === 'barChart') {
|
||||
return (
|
||||
<BarChart
|
||||
data={chartData}
|
||||
params={params}
|
||||
colors={colors}
|
||||
onClick={onChartClick}
|
||||
label={
|
||||
metric.metricOf === 'sessionCount'
|
||||
? 'Number of Sessions'
|
||||
: 'Number of Users'
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (viewType === 'progressChart') {
|
||||
return (
|
||||
<ProgressBarChart
|
||||
data={chartData}
|
||||
params={params}
|
||||
colors={colors}
|
||||
onClick={onChartClick}
|
||||
label={
|
||||
metric.metricOf === 'sessionCount'
|
||||
? 'Number of Sessions'
|
||||
: 'Number of Users'
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (viewType === 'pieChart') {
|
||||
return (
|
||||
<CustomMetricPieChart
|
||||
metric={metric}
|
||||
data={chartData}
|
||||
colors={colors}
|
||||
// params={params}
|
||||
onClick={onChartClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (viewType === 'progress') {
|
||||
return (
|
||||
<CustomMetricPercentage
|
||||
data={data[0]}
|
||||
colors={colors}
|
||||
params={params}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (viewType === 'table') {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (metricType === TABLE) {
|
||||
if (metricOf === FilterKey.SESSIONS) {
|
||||
return (
|
||||
<CustomMetricTableSessions
|
||||
metric={metric}
|
||||
data={data}
|
||||
isTemplate={isTemplate}
|
||||
isEdit={!isSaved && !isTemplate}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (metricOf === FilterKey.ERRORS) {
|
||||
return (
|
||||
<CustomMetricTableErrors
|
||||
metric={metric}
|
||||
data={data}
|
||||
// isTemplate={isTemplate}
|
||||
isEdit={!isSaved && !isTemplate}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (viewType === TABLE) {
|
||||
return (
|
||||
<SessionsBy
|
||||
metric={metric}
|
||||
data={data[0]}
|
||||
onClick={onChartClick}
|
||||
isTemplate={isTemplate}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
if (metricType === HEATMAP) {
|
||||
if (!props.isPreview) {
|
||||
return metric.thumbnail ? (
|
||||
<div
|
||||
style={{
|
||||
height: '229px',
|
||||
overflow: 'hidden',
|
||||
marginBottom: '10px',
|
||||
}}
|
||||
>
|
||||
<img src={metric.thumbnail} alt="clickmap thumbnail" />
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="flex items-center relative justify-center"
|
||||
style={{ height: '229px' }}
|
||||
>
|
||||
<Icon name="info-circle" className="mr-2" size="14" />
|
||||
No data available for the selected period.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return <ClickMapCard />;
|
||||
}
|
||||
|
||||
if (metricType === INSIGHTS) {
|
||||
return <InsightsCard data={data} />;
|
||||
}
|
||||
|
||||
if (metricType === USER_PATH && data && data.links) {
|
||||
// return <PathAnalysis data={data}/>;
|
||||
return (
|
||||
<SankeyChart
|
||||
height={props.isPreview ? 500 : 240}
|
||||
data={data}
|
||||
onChartClick={(filters: any) => {
|
||||
dashboardStore.drillDownFilter.merge({ filters, page: 1 });
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (metricType === RETENTION) {
|
||||
if (viewType === 'trend') {
|
||||
return (
|
||||
<CustomMetricLineChart
|
||||
data={data}
|
||||
colors={colors}
|
||||
params={params}
|
||||
onClick={onChartClick}
|
||||
/>
|
||||
);
|
||||
} else if (viewType === 'cohort') {
|
||||
return <CohortCard data={data[0]} />;
|
||||
}
|
||||
}
|
||||
|
||||
return <div>Unknown metric type</div>;
|
||||
};
|
||||
return (
|
||||
<div ref={ref}>
|
||||
<Loader loading={loading} style={{ height: `240px` }}>
|
||||
<div style={{ minHeight: 240 }}>
|
||||
{renderChart()}
|
||||
{metric.metricType === TIMESERIES ? (
|
||||
<WidgetDatatable data={data} enabledRows={enabledRows} setEnabledRows={setEnabledRows} />
|
||||
) : null}
|
||||
</div>
|
||||
</Loader>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(WidgetChart);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,161 @@
|
|||
import { Button, Table } from 'antd';
|
||||
import type { TableProps } from 'antd';
|
||||
|
||||
import { Eye, EyeOff } from 'lucide-react';
|
||||
import cn from 'classnames';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
const initTableProps = [
|
||||
{
|
||||
title: 'Series',
|
||||
dataIndex: 'seriesName',
|
||||
key: 'seriesName',
|
||||
sorter: (a, b) => a.seriesName.localeCompare(b.seriesName),
|
||||
},
|
||||
{
|
||||
title: 'Avg.',
|
||||
dataIndex: 'average',
|
||||
key: 'average',
|
||||
sorter: (a, b) => a.average - b.average,
|
||||
},
|
||||
];
|
||||
|
||||
interface Props {
|
||||
data: { chart: any[]; namesMap: string[] };
|
||||
enabledRows: string[];
|
||||
setEnabledRows: (rows: string[]) => void;
|
||||
}
|
||||
|
||||
function WidgetDatatable(props: Props) {
|
||||
const [tableProps, setTableProps] =
|
||||
useState<TableProps['columns']>(initTableProps);
|
||||
// console.log(params.density / 7, data.chart)
|
||||
const data = props.data;
|
||||
|
||||
const [showTable, setShowTable] = useState(false);
|
||||
const hasMultipleSeries = data.namesMap.length > 1;
|
||||
const [tableData, setTableData] = useState([]);
|
||||
|
||||
const columnNames = new Set();
|
||||
/**
|
||||
* basically we have an array of
|
||||
* { time: some_date, series1: 1, series2: 2, series3: 3, timestamp: 123456 }
|
||||
* which we turn into a table where each series of filters = row;
|
||||
* and each unique time = column
|
||||
* + average for each row
|
||||
* [ { seriesName: 'series1', mon: 1, tue: 2, wed: 3, average: 2 }, ... ]
|
||||
* */
|
||||
const series = Object.keys(data.chart[0]).filter(
|
||||
(key) => key !== 'time' && key !== 'timestamp'
|
||||
);
|
||||
React.useEffect(() => {
|
||||
setTableProps(initTableProps);
|
||||
columnNames.clear();
|
||||
data.chart.forEach((p: any) => {
|
||||
columnNames.add(p.time);
|
||||
}); // for example: mon, tue, wed, thu, fri, sat, sun
|
||||
const avg: any = {}; // { seriesName: {itemsCount: 0, total: 0} }
|
||||
const items: Record<string, any>[] = []; // as many items (rows) as we have series in filter
|
||||
series.forEach((s, i) => {
|
||||
items.push({ seriesName: s, average: 0, key: s });
|
||||
avg[s] = { itemsCount: 0, total: 0 };
|
||||
});
|
||||
const tableCols: {
|
||||
title: string;
|
||||
dataIndex: string;
|
||||
key: string;
|
||||
sorter: any;
|
||||
}[] = [];
|
||||
const uniqueColArr = Array.from(columnNames);
|
||||
uniqueColArr.forEach((name: string, i) => {
|
||||
tableCols.push({
|
||||
title: name,
|
||||
dataIndex: name,
|
||||
key: name,
|
||||
sorter: (a, b) => a[name] - b[name],
|
||||
});
|
||||
const values = data.chart.filter((p) => p.time === name);
|
||||
series.forEach((s) => {
|
||||
avg[s].itemsCount += 1;
|
||||
avg[s].total += values.reduce((acc, curr) => acc + curr[s], 0);
|
||||
const ind = items.findIndex((item) => item.seriesName === s);
|
||||
if (ind === -1) return;
|
||||
items[ind][name] = values.reduce((acc, curr) => acc + curr[s], 0);
|
||||
});
|
||||
});
|
||||
Object.keys(avg).forEach((key) => {
|
||||
const ind = items.findIndex((item) => item.seriesName === key);
|
||||
if (ind === -1) return;
|
||||
items[ind].average = (avg[key].total / avg[key].itemsCount).toFixed(2);
|
||||
});
|
||||
|
||||
setTableProps((prev) => [...prev, ...tableCols]);
|
||||
setTableData(items);
|
||||
}, [data.chart.length]);
|
||||
|
||||
const rowSelection: TableProps['rowSelection'] = {
|
||||
selectedRowKeys: props.enabledRows,
|
||||
onChange: (selectedRowKeys: React.Key[], selectedRows: any[]) => {
|
||||
props.setEnabledRows(selectedRowKeys as string[]);
|
||||
console.log(
|
||||
`selectedRowKeys: ${selectedRowKeys}`,
|
||||
'selectedRows: ',
|
||||
selectedRows
|
||||
);
|
||||
},
|
||||
getCheckboxProps: (record: any) => ({
|
||||
name: record.name,
|
||||
checked: false,
|
||||
}),
|
||||
type: 'checkbox',
|
||||
};
|
||||
return hasMultipleSeries ? (
|
||||
<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'
|
||||
}
|
||||
/>
|
||||
<div
|
||||
className={'absolute top-0 left-1/2 z-10'}
|
||||
style={{ transform: 'translate(-50%, -50%)' }}
|
||||
>
|
||||
<Button
|
||||
icon={showTable ? <EyeOff size={16} /> : <Eye size={16} />}
|
||||
size={'small'}
|
||||
type={'default'}
|
||||
onClick={() => setShowTable(!showTable)}
|
||||
>
|
||||
{showTable ? 'Hide Table' : 'Show Table'}
|
||||
</Button>
|
||||
</div>
|
||||
{showTable ? (
|
||||
<div className={'relative pb-2'}>
|
||||
<Table
|
||||
columns={tableProps}
|
||||
dataSource={tableData}
|
||||
pagination={false}
|
||||
rowSelection={rowSelection}
|
||||
size={'small'}
|
||||
/>
|
||||
{/* 1.23+ export menu floater */}
|
||||
{/*<div className={'absolute top-0 -right-1'}>*/}
|
||||
{/* <ItemMenu*/}
|
||||
{/* items={[*/}
|
||||
{/* { icon: 'pencil', text: 'Rename', onClick: () => null },*/}
|
||||
{/* ]}*/}
|
||||
{/* bold*/}
|
||||
{/* customTrigger={*/}
|
||||
{/* <div className={'flex items-center justify-center bg-gray-lighter cursor-pointer hover:bg-gray-light'} style={{ height: 38, width: 38, boxShadow: '-2px 0px 3px 0px rgba(0, 0, 0, 0.05)' }}>*/}
|
||||
{/* <EllipsisVertical size={16} />*/}
|
||||
{/* </div>*/}
|
||||
{/* }*/}
|
||||
{/* />*/}
|
||||
{/*</div>*/}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null;
|
||||
}
|
||||
|
||||
export default WidgetDatatable;
|
||||
|
|
@ -1,37 +1,39 @@
|
|||
import React from 'react';
|
||||
import SelectDateRange from 'Shared/SelectDateRange';
|
||||
import {useStore} from 'App/mstore';
|
||||
import {useObserver} from 'mobx-react-lite';
|
||||
import {Space} from "antd";
|
||||
import { useStore } from 'App/mstore';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { Space } from 'antd';
|
||||
|
||||
function WidgetDateRange({
|
||||
label = 'Time Range',
|
||||
}: any) {
|
||||
const {dashboardStore} = useStore();
|
||||
const period = useObserver(() => dashboardStore.drillDownPeriod);
|
||||
const drillDownFilter = useObserver(() => dashboardStore.drillDownFilter);
|
||||
function WidgetDateRange({ label = 'Time Range', comparison = false }: any) {
|
||||
const { dashboardStore } = useStore();
|
||||
const period = comparison ? dashboardStore.comparisonPeriod : dashboardStore.drillDownPeriod;
|
||||
const drillDownFilter = dashboardStore.drillDownFilter;
|
||||
|
||||
const onChangePeriod = (period: any) => {
|
||||
dashboardStore.setDrillDownPeriod(period);
|
||||
const periodTimestamps = period.toTimestamps();
|
||||
drillDownFilter.merge({
|
||||
startTimestamp: periodTimestamps.startTimestamp,
|
||||
endTimestamp: periodTimestamps.endTimestamp,
|
||||
})
|
||||
const onChangePeriod = (period: any) => {
|
||||
if (comparison) dashboardStore.setComparisonPeriod(period);
|
||||
else {
|
||||
dashboardStore.setDrillDownPeriod(period);
|
||||
const periodTimestamps = period.toTimestamps();
|
||||
drillDownFilter.merge({
|
||||
startTimestamp: periodTimestamps.startTimestamp,
|
||||
endTimestamp: periodTimestamps.endTimestamp,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Space>
|
||||
{label && <span className="mr-1 color-gray-medium">{label}</span>}
|
||||
<SelectDateRange
|
||||
period={period}
|
||||
onChange={onChangePeriod}
|
||||
right={true}
|
||||
isAnt={true}
|
||||
useButtonStyle={true}
|
||||
/>
|
||||
</Space>
|
||||
);
|
||||
return (
|
||||
<Space>
|
||||
{label && <span className="mr-1 color-gray-medium">{label}</span>}
|
||||
<SelectDateRange
|
||||
period={period}
|
||||
onChange={onChangePeriod}
|
||||
right={true}
|
||||
isAnt={true}
|
||||
useButtonStyle={true}
|
||||
comparison={comparison}
|
||||
/>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
|
||||
export default WidgetDateRange;
|
||||
export default observer(WidgetDateRange);
|
||||
|
|
|
|||
|
|
@ -1,17 +1,14 @@
|
|||
import React from 'react';
|
||||
import { FUNNEL, HEATMAP, TABLE, USER_PATH } from 'App/constants/card';
|
||||
import { Select, Space, Switch } from 'antd';
|
||||
import { FUNNEL, HEATMAP, TABLE, TIMESERIES, USER_PATH } from "App/constants/card";
|
||||
import { Select, Space, Switch, Dropdown, Button} from 'antd';
|
||||
import { DownOutlined } from '@ant-design/icons';
|
||||
import { useStore } from 'App/mstore';
|
||||
import ClickMapRagePicker from 'Components/Dashboard/components/ClickMapRagePicker/ClickMapRagePicker';
|
||||
import { FilterKey } from 'Types/filter/filterType';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { ChartLine, ChartArea, ChartColumn, ChartBar, ChartPie, Table } from 'lucide-react'
|
||||
|
||||
interface Props {
|
||||
|
||||
}
|
||||
|
||||
|
||||
function WidgetOptions(props: Props) {
|
||||
function WidgetOptions() {
|
||||
const { metricStore, dashboardStore } = useStore();
|
||||
const metric: any = metricStore.instance;
|
||||
|
||||
|
|
@ -19,6 +16,22 @@ function WidgetOptions(props: Props) {
|
|||
metric.update({ metricFormat: value });
|
||||
};
|
||||
|
||||
const chartTypes = {
|
||||
lineChart: 'Chart',
|
||||
barChart: 'Column',
|
||||
areaChart: 'Area',
|
||||
pieChart: 'Pie',
|
||||
progressChart: 'Bar',
|
||||
table: 'Table',
|
||||
}
|
||||
const chartIcons = {
|
||||
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} />,
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
{metric.metricType === USER_PATH && (
|
||||
|
|
@ -31,28 +44,50 @@ function WidgetOptions(props: Props) {
|
|||
>
|
||||
<Space>
|
||||
<Switch checked={metric.hideExcess} size="small" />
|
||||
<span className="mr-4 color-gray-medium">
|
||||
Hide Minor Paths
|
||||
</span>
|
||||
<span className="mr-4 color-gray-medium">Hide Minor Paths</span>
|
||||
</Space>
|
||||
</a>
|
||||
)}
|
||||
|
||||
{(metric.metricType === FUNNEL || metric.metricType === TABLE) && metric.metricOf != FilterKey.USERID && metric.metricOf != FilterKey.ERRORS && (
|
||||
<Select
|
||||
defaultValue={metric.metricFormat}
|
||||
onChange={handleChange}
|
||||
variant="borderless"
|
||||
options={[
|
||||
{ value: 'sessionCount', label: 'Sessions' },
|
||||
{ value: 'userCount', label: 'Users' }
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
|
||||
{metric.metricType === HEATMAP ? (
|
||||
<ClickMapRagePicker />
|
||||
{metric.metricType === TIMESERIES ? (
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: Object.entries(chartTypes).map(([key, name]) => ({
|
||||
key,
|
||||
label: <div className={'flex items-center gap-2'}>
|
||||
{chartIcons[key]}
|
||||
<div>{name}</div>
|
||||
</div>,
|
||||
})),
|
||||
onClick: ({ key }: any) => {
|
||||
metric.updateKey('viewType', key);
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Button>
|
||||
<Space>
|
||||
{chartIcons[metric.viewType]}
|
||||
<div>{chartTypes[metric.viewType]}</div>
|
||||
<DownOutlined />
|
||||
</Space>
|
||||
</Button>
|
||||
</Dropdown>
|
||||
) : null}
|
||||
{(metric.metricType === FUNNEL || metric.metricType === TABLE) &&
|
||||
metric.metricOf != FilterKey.USERID &&
|
||||
metric.metricOf != FilterKey.ERRORS && (
|
||||
<Select
|
||||
defaultValue={metric.metricFormat}
|
||||
onChange={handleChange}
|
||||
variant="borderless"
|
||||
options={[
|
||||
{ value: 'sessionCount', label: 'Sessions' },
|
||||
{ value: 'userCount', label: 'Users' },
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
|
||||
{metric.metricType === HEATMAP ? <ClickMapRagePicker /> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
import { Button, Space, Switch } from 'antd';
|
||||
import cn from 'classnames';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import React from 'react';
|
||||
|
||||
import { HEATMAP, USER_PATH } from 'App/constants/card';
|
||||
import WidgetDateRange from "Components/Dashboard/components/WidgetDateRange/WidgetDateRange";
|
||||
import { useStore } from 'App/mstore';
|
||||
import ClickMapRagePicker from 'Components/Dashboard/components/ClickMapRagePicker';
|
||||
import { TIMESERIES } from "../../../../constants/card";
|
||||
|
||||
import WidgetWrapper from '../WidgetWrapper';
|
||||
import WidgetOptions from 'Components/Dashboard/components/WidgetOptions';
|
||||
|
|
@ -21,14 +19,16 @@ function WidgetPreview(props: Props) {
|
|||
const { metricStore, dashboardStore } = useStore();
|
||||
const metric: any = metricStore.instance;
|
||||
|
||||
// compare logic
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={cn(className, 'bg-white rounded-xl border shadow-sm mt-0')}
|
||||
>
|
||||
<div className="flex items-center justify-between px-4 pt-2">
|
||||
<h2 className="text-xl">{props.name}</h2>
|
||||
<div className="flex items-center">
|
||||
<div className="flex items-center gap-2 px-4 pt-2">
|
||||
<WidgetDateRange label="" />
|
||||
{metric.metricType === TIMESERIES ? <WidgetDateRange comparison label="" /> : null}
|
||||
<div className="flex items-center ml-auto">
|
||||
<WidgetOptions />
|
||||
{/*{metric.metricType === USER_PATH && (*/}
|
||||
{/* <a*/}
|
||||
|
|
@ -47,7 +47,6 @@ function WidgetPreview(props: Props) {
|
|||
{/* </a>*/}
|
||||
{/*)}*/}
|
||||
|
||||
|
||||
{/*{isTimeSeries && (*/}
|
||||
{/* <>*/}
|
||||
{/* <span className="mr-4 color-gray-medium">Visualization</span>*/}
|
||||
|
|
@ -104,8 +103,6 @@ function WidgetPreview(props: Props) {
|
|||
{/*</>*/}
|
||||
{/*)}*/}
|
||||
|
||||
|
||||
|
||||
{/* add to dashboard */}
|
||||
{/*{metric.exists() && (*/}
|
||||
{/* <AddToDashboardButton metricId={metric.metricId}/>*/}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import WidgetName from "Components/Dashboard/components/WidgetName";
|
|||
import {useStore} from "App/mstore";
|
||||
import {useObserver} from "mobx-react-lite";
|
||||
import AddToDashboardButton from "Components/Dashboard/components/AddToDashboardButton";
|
||||
import WidgetDateRange from "Components/Dashboard/components/WidgetDateRange/WidgetDateRange";
|
||||
import {Button, Space} from "antd";
|
||||
import CardViewMenu from "Components/Dashboard/components/WidgetView/CardViewMenu";
|
||||
|
||||
|
|
@ -30,7 +29,6 @@ function WidgetViewHeader({onClick, onSave, undoChanges}: Props) {
|
|||
/>
|
||||
</h1>
|
||||
<Space>
|
||||
<WidgetDateRange label=""/>
|
||||
<AddToDashboardButton metricId={widget.metricId}/>
|
||||
<Button
|
||||
type="primary"
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
import { DownOutlined } from '@ant-design/icons';
|
||||
import { DownOutlined, CloseOutlined } from '@ant-design/icons';
|
||||
import Period from 'Types/app/period';
|
||||
import { Dropdown, Button } from 'antd';
|
||||
import { Dropdown } from 'antd';
|
||||
import cn from 'classnames';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import React from 'react';
|
||||
import { components } from 'react-select';
|
||||
import { CUSTOM_RANGE, DATE_RANGE_OPTIONS } from 'App/dateRange';
|
||||
|
||||
import { CUSTOM_RANGE, DATE_RANGE_OPTIONS, DATE_RANGE_COMPARISON_OPTIONS } from 'App/dateRange';
|
||||
import { Calendar } from 'lucide-react'
|
||||
import DateRangePopup from 'Shared/DateRangeDropdown/DateRangePopup';
|
||||
import OutsideClickDetectingDiv from 'Shared/OutsideClickDetectingDiv';
|
||||
import Select from 'Shared/Select';
|
||||
|
||||
interface Props {
|
||||
period: any;
|
||||
period: any | null;
|
||||
onChange: (data: any) => void;
|
||||
disableCustom?: boolean;
|
||||
right?: boolean;
|
||||
|
|
@ -20,17 +20,18 @@ interface Props {
|
|||
isAnt?: boolean;
|
||||
small?: boolean;
|
||||
useButtonStyle?: boolean; // New prop to control button style
|
||||
|
||||
comparison?: boolean;
|
||||
[x: string]: any;
|
||||
}
|
||||
|
||||
function SelectDateRange(props: Props) {
|
||||
const [isCustom, setIsCustom] = React.useState(false);
|
||||
const { right = false, period, disableCustom = false, timezone, useButtonStyle = false } = props;
|
||||
let selectedValue = DATE_RANGE_OPTIONS.find(
|
||||
(obj: any) => obj.value === period.rangeName
|
||||
);
|
||||
const options = DATE_RANGE_OPTIONS.filter((obj: any) =>
|
||||
const dateRangeOptions = props.comparison ? DATE_RANGE_COMPARISON_OPTIONS : DATE_RANGE_OPTIONS;
|
||||
let selectedValue = period?.rangeName ? dateRangeOptions.find(
|
||||
(obj: any) => obj.value === period?.rangeName
|
||||
) : null;
|
||||
const options = dateRangeOptions.filter((obj: any) =>
|
||||
disableCustom ? obj.value !== CUSTOM_RANGE : true
|
||||
);
|
||||
|
||||
|
|
@ -54,7 +55,7 @@ function SelectDateRange(props: Props) {
|
|||
setIsCustom(false);
|
||||
};
|
||||
|
||||
const isCustomRange = period.rangeName === CUSTOM_RANGE;
|
||||
const isCustomRange = period ? period.rangeName === CUSTOM_RANGE : false;
|
||||
const isUSLocale = navigator.language === 'en-US' || navigator.language.startsWith('en-US');
|
||||
const customRange = isCustomRange ? period.rangeFormatted(isUSLocale ? "MMM dd yyyy, hh:mm a" : "MMM dd yyyy, HH:mm") : '';
|
||||
|
||||
|
|
@ -70,28 +71,54 @@ function SelectDateRange(props: Props) {
|
|||
},
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<div className={'relative'}>
|
||||
<Dropdown menu={menuProps} >
|
||||
{useButtonStyle ? (
|
||||
<Button type='text'>
|
||||
<span>{isCustomRange ? customRange : selectedValue?.label}</span>
|
||||
<DownOutlined />
|
||||
</Button>
|
||||
) : (
|
||||
<div className={'cursor-pointer flex items-center gap-2'}>
|
||||
<span>{isCustomRange ? customRange : selectedValue?.label}</span>
|
||||
<DownOutlined />
|
||||
{props.comparison ? (
|
||||
<div className={'flex items-center gap-0'}>
|
||||
<Dropdown menu={menuProps} className={'px-2 py-1'}>
|
||||
<div className={"cursor-pointer flex items-center gap-2 border-l border-t border-b border-gray-light rounded-l !border-r-0"}>
|
||||
<span>
|
||||
{isCustomRange
|
||||
? customRange
|
||||
: `Compare to ${selectedValue ? selectedValue?.label : ''}`}
|
||||
</span>
|
||||
<DownOutlined />
|
||||
</div>
|
||||
</Dropdown>
|
||||
<div
|
||||
className={"flex items-center justify-center border border-gray-light p-2 hover:border-main rounded-r"}
|
||||
style={{ height: 30 }}
|
||||
onClick={() => props.onChange(null)}
|
||||
>
|
||||
<CloseOutlined />
|
||||
</div>
|
||||
)}
|
||||
</Dropdown>
|
||||
</div>
|
||||
) : (
|
||||
<Dropdown menu={menuProps} className={'px-2 py-1'}>
|
||||
{useButtonStyle ? (
|
||||
<div className={'flex items-center gap-2 border border-gray-light rounded cursor-pointer'}>
|
||||
<Calendar size={16} />
|
||||
<span>
|
||||
{isCustomRange ? customRange : selectedValue?.label}
|
||||
</span>
|
||||
<DownOutlined />
|
||||
</div>
|
||||
) : (
|
||||
<div className={'cursor-pointer flex items-center gap-2'}>
|
||||
<span>
|
||||
{isCustomRange ? customRange : selectedValue?.label}
|
||||
</span>
|
||||
<DownOutlined />
|
||||
</div>
|
||||
)}
|
||||
</Dropdown>
|
||||
)}
|
||||
{isCustom && (
|
||||
<OutsideClickDetectingDiv
|
||||
onClickOutside={(e: any) => {
|
||||
if (
|
||||
e.target.className.includes('react-calendar')
|
||||
|| e.target.parentElement.parentElement.classList.contains(
|
||||
e.target.className.includes('react-calendar') ||
|
||||
e.target.parentElement.parentElement.classList.contains(
|
||||
'rc-time-picker-panel-select'
|
||||
) ||
|
||||
e.target.parentElement.parentElement.classList[0]?.includes(
|
||||
|
|
@ -117,7 +144,7 @@ function SelectDateRange(props: Props) {
|
|||
onApply={onApplyDateRange}
|
||||
onCancel={() => setIsCustom(false)}
|
||||
selectedDateRange={period.range}
|
||||
className='h-fit'
|
||||
className="h-fit"
|
||||
/>
|
||||
</div>
|
||||
</OutsideClickDetectingDiv>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import React from "react";
|
||||
import { Icon, Popover, Tooltip } from "UI";
|
||||
import { Dropdown, Menu, Button} from "antd";
|
||||
import {EllipsisVertical} from 'lucide-react';
|
||||
import styles from "./itemMenu.module.css";
|
||||
import cn from "classnames";
|
||||
import React from 'react';
|
||||
import { Icon, Popover, Tooltip } from 'UI';
|
||||
import { Dropdown, Menu, Button } from 'antd';
|
||||
import { EllipsisVertical } from 'lucide-react';
|
||||
import styles from './itemMenu.module.css';
|
||||
import cn from 'classnames';
|
||||
|
||||
interface Item {
|
||||
icon?: string;
|
||||
|
|
@ -21,6 +21,7 @@ interface Props {
|
|||
label?: React.ReactNode;
|
||||
sm?: boolean;
|
||||
onToggle?: (args: any) => void;
|
||||
customTrigger?: React.ReactElement;
|
||||
}
|
||||
|
||||
export default class ItemMenu extends React.PureComponent<Props> {
|
||||
|
|
@ -30,13 +31,13 @@ export default class ItemMenu extends React.PureComponent<Props> {
|
|||
displayed: false,
|
||||
};
|
||||
|
||||
handleEsc = (e: KeyboardEvent) => e.key === "Escape" && this.closeMenu();
|
||||
handleEsc = (e: KeyboardEvent) => e.key === 'Escape' && this.closeMenu();
|
||||
|
||||
componentDidMount() {
|
||||
document.addEventListener("keydown", this.handleEsc, false);
|
||||
document.addEventListener('keydown', this.handleEsc, false);
|
||||
}
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener("keydown", this.handleEsc, false);
|
||||
document.removeEventListener('keydown', this.handleEsc, false);
|
||||
}
|
||||
|
||||
onClick = (callback: Function) => (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
|
|
@ -58,13 +59,17 @@ export default class ItemMenu extends React.PureComponent<Props> {
|
|||
render() {
|
||||
const { items, label, bold, sm } = this.props;
|
||||
const { displayed } = this.state;
|
||||
const parentStyles = label ? "hover:bg-gray-light" : "";
|
||||
const parentStyles = label ? 'hover:bg-gray-light' : '';
|
||||
|
||||
return (
|
||||
<Popover
|
||||
placement="bottom-end" // Set the placement to bottom-end for right alignment
|
||||
render={() => (
|
||||
<div className={cn(styles.menu, 'rounded-lg', { [styles.menuDim]: !bold })}>
|
||||
<div
|
||||
className={cn(styles.menu, 'rounded-lg', {
|
||||
[styles.menuDim]: !bold,
|
||||
})}
|
||||
>
|
||||
{items
|
||||
.filter(({ hidden }) => !hidden)
|
||||
.map(
|
||||
|
|
@ -73,15 +78,22 @@ export default class ItemMenu extends React.PureComponent<Props> {
|
|||
text,
|
||||
icon,
|
||||
disabled = false,
|
||||
tooltipTitle = "",
|
||||
tooltipTitle = '',
|
||||
}) => (
|
||||
<Tooltip key={text} disabled={!disabled} title={tooltipTitle} delay={0}>
|
||||
<Tooltip
|
||||
key={text}
|
||||
disabled={!disabled}
|
||||
title={tooltipTitle}
|
||||
delay={0}
|
||||
>
|
||||
<div
|
||||
onClick={!disabled ? this.onClick(onClick) : () => {}}
|
||||
className={`${disabled ? "cursor-not-allowed" : ""}`}
|
||||
className={`${disabled ? 'cursor-not-allowed' : ''}`}
|
||||
role="menuitem"
|
||||
>
|
||||
<div className={cn(styles.menuItem, { disabled: disabled })}>
|
||||
<div
|
||||
className={cn(styles.menuItem, { disabled: disabled })}
|
||||
>
|
||||
{icon && (
|
||||
<div className={styles.iconWrapper}>
|
||||
<Icon name={icon} size="13" color="gray-dark" />
|
||||
|
|
@ -96,32 +108,38 @@ export default class ItemMenu extends React.PureComponent<Props> {
|
|||
</div>
|
||||
)}
|
||||
>
|
||||
|
||||
{this.props.customTrigger ? (
|
||||
this.props.customTrigger
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
type="text"
|
||||
className={cn("select-none", !this.props.flat ? parentStyles : "", {
|
||||
"": !this.props.flat && displayed && label,
|
||||
})}
|
||||
>
|
||||
{label && (
|
||||
<span className={cn("font-medium")}>
|
||||
{label}
|
||||
</span>
|
||||
className={cn(
|
||||
'select-none',
|
||||
!this.props.flat ? parentStyles : '',
|
||||
{
|
||||
'': !this.props.flat && displayed && label,
|
||||
}
|
||||
)}
|
||||
>
|
||||
{label && <span className={cn('font-medium')}>{label}</span>}
|
||||
{!this.props.flat && (
|
||||
<div
|
||||
ref={(ref) => {
|
||||
this.menuBtnRef = ref;
|
||||
}}
|
||||
className={cn("rounded-full flex items-center justify-center")}
|
||||
className={cn(
|
||||
'rounded-full flex items-center justify-center'
|
||||
)}
|
||||
role="button"
|
||||
>
|
||||
<EllipsisVertical size={16} />
|
||||
</div>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
</>
|
||||
)}
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,19 @@ const DATE_RANGE_LABELS = {
|
|||
[CUSTOM_RANGE]: "Custom Range",
|
||||
};
|
||||
|
||||
const COMPARISON_DATE_RANGE_LABELS = {
|
||||
// LAST_30_MINUTES: '30 Minutes',
|
||||
// TODAY: 'Today',
|
||||
LAST_24_HOURS: "Previous Day",
|
||||
// YESTERDAY: 'Yesterday',
|
||||
LAST_7_DAYS: "Previous Week",
|
||||
LAST_30_DAYS: "Previous Month",
|
||||
//THIS_MONTH: 'This Month',
|
||||
//LAST_MONTH: 'Previous Month',
|
||||
//THIS_YEAR: 'This Year',
|
||||
[CUSTOM_RANGE]: "Custom Range",
|
||||
}
|
||||
|
||||
const DATE_RANGE_VALUES = {};
|
||||
Object.keys(DATE_RANGE_LABELS).forEach((key) => {
|
||||
DATE_RANGE_VALUES[key] = key;
|
||||
|
|
@ -31,6 +44,12 @@ export const DATE_RANGE_OPTIONS = Object.keys(DATE_RANGE_LABELS).map((key) => {
|
|||
value: key,
|
||||
};
|
||||
});
|
||||
export const DATE_RANGE_COMPARISON_OPTIONS = Object.keys(COMPARISON_DATE_RANGE_LABELS).map((key) => {
|
||||
return {
|
||||
label: COMPARISON_DATE_RANGE_LABELS[key],
|
||||
value: key,
|
||||
};
|
||||
});
|
||||
|
||||
export function getDateRangeLabel(value) {
|
||||
return DATE_RANGE_LABELS[value];
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ export default class DashboardStore {
|
|||
period: Record<string, any> = Period({ rangeName: LAST_24_HOURS });
|
||||
drillDownFilter: Filter = new Filter();
|
||||
drillDownPeriod: Record<string, any> = Period({ rangeName: LAST_7_DAYS });
|
||||
comparisonPeriod: Record<string, any> | null = null
|
||||
startTimestamp: number = 0;
|
||||
endTimestamp: number = 0;
|
||||
pendingRequests: number = 0;
|
||||
|
|
@ -408,6 +409,17 @@ export default class DashboardStore {
|
|||
});
|
||||
}
|
||||
|
||||
setComparisonPeriod(period: any) {
|
||||
if (!period) {
|
||||
return this.comparisonPeriod = null
|
||||
}
|
||||
this.comparisonPeriod = Period({
|
||||
start: period.start,
|
||||
end: period.end,
|
||||
rangeName: period.rangeName,
|
||||
});
|
||||
}
|
||||
|
||||
toggleAlertModal(val: boolean) {
|
||||
this.showAlertModal = val;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue