ui: more chart types, add table with filtering out series, start "compare to" thing

This commit is contained in:
nick-delirium 2024-11-26 17:39:15 +01:00
parent 170e85b505
commit 19b5addc95
No known key found for this signature in database
GPG key ID: 93ABD695DF5FDBA0
17 changed files with 1239 additions and 651 deletions

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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>
);
}

View file

@ -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;

View file

@ -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;

View file

@ -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>
);

View file

@ -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);

View file

@ -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;

View file

@ -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);

View file

@ -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>
);
}

View file

@ -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}/>*/}

View file

@ -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"

View file

@ -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>

View file

@ -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>
);
}
}
}

View file

@ -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];

View file

@ -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;
}