diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/BigNumChart.tsx b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/BigNumChart.tsx index b0e84e483..8c62c50b3 100644 --- a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/BigNumChart.tsx +++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/BigNumChart.tsx @@ -2,35 +2,19 @@ import React from 'react' import { CompareTag } from "./CustomChartTooltip"; interface Props { - data: { chart: any[], namesMap: string[] }; - compData: { chart: any[], namesMap: string[] } | null; colors: any; onClick?: (event, index) => void; yaxis?: any; label?: string; hideLegend?: boolean; + values: { value: number, compData?: number, series: string, valueLabel?: string }[]; } function BigNumChart(props: Props) { const { - data = { chart: [], namesMap: [] }, - compData = { chart: [], namesMap: [] }, colors, - onClick = () => null, label = 'Number of Sessions', + values, } = props; - - const values: { value: number, compData?: number, series: string }[] = []; - for (let i = 0; i < data.namesMap.length; i++) { - if (!data.namesMap[i]) { - continue; - } - - values.push({ - value: data.chart.reduce((acc, curr) => acc + curr[data.namesMap[i]], 0), - compData: compData ? compData.chart.reduce((acc, curr) => acc + curr[compData.namesMap[i]], 0) : undefined, - series: data.namesMap[i], - }); - } return (
{values.map((val, i) => ( @@ -41,18 +25,20 @@ function BigNumChart(props: Props) { value={val.value} label={label} compData={val.compData} + valueLabel={val.valueLabel} /> ))}
) } -function BigNum({ color, series, value, label, compData }: { +function BigNum({ color, series, value, label, compData, valueLabel }: { color: string, series: string, value: number, label: string, compData?: number, + valueLabel?: string, }) { const formattedNumber = (num: number) => { return Intl.NumberFormat().format(num); @@ -69,7 +55,7 @@ function BigNum({ color, series, value, label, compData }: {
{series}
- {formattedNumber(value)} + {formattedNumber(value)}{valueLabel ? `${valueLabel}` : null}
{label} diff --git a/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx b/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx index 4bc3c345d..a5b962bec 100644 --- a/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx +++ b/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx @@ -6,6 +6,7 @@ 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 FunnelTable from "../../../Funnels/FunnelWidget/FunnelTable"; import AreaChart from '../../Widgets/CustomMetricsWidgets/AreaChart'; import BarChart from '../../Widgets/CustomMetricsWidgets/BarChart'; import ProgressBarChart from '../../Widgets/CustomMetricsWidgets/ProgressBarChart'; @@ -215,6 +216,42 @@ function WidgetChart(props: Props) { const metricWithData = { ...metric, data }; if (metricType === FUNNEL) { + console.log(data, compData); + if (viewType === 'table') { + return ( + + ) + } + if (viewType === 'metric') { + const values: { + value: number; + compData?: number; + series: string; + valueLabel?: string; + }[] = [ + { + value: data.funnel.totalConversionsPercentage, + compData: compData + ? compData.funnel.totalConversionsPercentage + : undefined, + series: 'Dynamic', + valueLabel: '%' + }, + ]; + + return ( + + ); + } + return ( acc + curr[data.namesMap[i]], 0), + compData: compData ? compData.chart.reduce((acc, curr) => acc + curr[compData.namesMap[i]], 0) : undefined, + series: data.namesMap[i], + }); + } return ( ) : null}
diff --git a/frontend/app/components/Dashboard/components/WidgetDatatable/WidgetDatatable.tsx b/frontend/app/components/Dashboard/components/WidgetDatatable/WidgetDatatable.tsx index e4e1371fb..c86709c4b 100644 --- a/frontend/app/components/Dashboard/components/WidgetDatatable/WidgetDatatable.tsx +++ b/frontend/app/components/Dashboard/components/WidgetDatatable/WidgetDatatable.tsx @@ -4,6 +4,7 @@ import type { TableProps } from 'antd'; import { Eye, EyeOff } from 'lucide-react'; import cn from 'classnames'; import React, { useState } from 'react'; +import { TableExporter } from "../../../Funnels/FunnelWidget/FunnelTable"; const initTableProps = [ { @@ -27,6 +28,7 @@ interface Props { enabledRows: string[]; setEnabledRows: (rows: string[]) => void; defaultOpen?: boolean; + metric: { name: string }; } function WidgetDatatable(props: Props) { @@ -137,20 +139,11 @@ function WidgetDatatable(props: Props) { size={'small'} scroll={{ x: 'max-content' }} /> - {/* 1.23+ export menu floater */} - {/*
*/} - {/* null },*/} - {/* ]}*/} - {/* bold*/} - {/* customTrigger={*/} - {/*
*/} - {/* */} - {/*
*/} - {/* }*/} - {/* />*/} - {/*
*/} + ) : null} diff --git a/frontend/app/components/Dashboard/components/WidgetOptions.tsx b/frontend/app/components/Dashboard/components/WidgetOptions.tsx index bf34b95a4..20463cad3 100644 --- a/frontend/app/components/Dashboard/components/WidgetOptions.tsx +++ b/frontend/app/components/Dashboard/components/WidgetOptions.tsx @@ -54,7 +54,7 @@ function WidgetOptions() { )} {metric.metricType === TIMESERIES ? ( - + ) : null} {(metric.metricType === FUNNEL || metric.metricType === TABLE) && metric.metricOf != FilterKey.USERID && @@ -80,7 +80,7 @@ const SeriesTypeOptions = observer(({ metric }: { metric: any }) => { const items = { sessionCount: 'Total Sessions', userCount: 'Unique Users', - } + }; const chartIcons = { sessionCount: , userCount: , @@ -112,7 +112,7 @@ const SeriesTypeOptions = observer(({ metric }: { metric: any }) => { ); -}) +}); const WidgetViewTypeOptions = observer(({ metric }: { metric: any }) => { const chartTypes = { @@ -139,9 +139,17 @@ const WidgetViewTypeOptions = observer(({ metric }: { metric: any }) => { chart: , }; const allowedTypes = { - [TIMESERIES]: ['lineChart', 'barChart', 'areaChart', 'pieChart', 'progressChart', 'table', 'metric',], - [FUNNEL]: ['chart', 'columnChart', ] // + table + metric - } + [TIMESERIES]: [ + 'lineChart', + 'barChart', + 'areaChart', + 'pieChart', + 'progressChart', + 'table', + 'metric', + ], + [FUNNEL]: ['chart', 'columnChart', 'metric', 'table'], + }; return ( { ); -}) +}); export default observer(WidgetOptions); diff --git a/frontend/app/components/Funnels/FunnelWidget/FunnelBar.tsx b/frontend/app/components/Funnels/FunnelWidget/FunnelBar.tsx index 813591b45..1b04d76f9 100644 --- a/frontend/app/components/Funnels/FunnelWidget/FunnelBar.tsx +++ b/frontend/app/components/Funnels/FunnelWidget/FunnelBar.tsx @@ -23,7 +23,7 @@ function FunnelBar(props: Props) { focusedFilter && index ? focusedFilter === index - 1 : false; return (
- +
+
{filter.label} {filter.operator} {filter.value.map((value: any, index: number) => ( diff --git a/frontend/app/components/Funnels/FunnelWidget/FunnelTable.tsx b/frontend/app/components/Funnels/FunnelWidget/FunnelTable.tsx new file mode 100644 index 000000000..c5c5bb9a2 --- /dev/null +++ b/frontend/app/components/Funnels/FunnelWidget/FunnelTable.tsx @@ -0,0 +1,131 @@ +import React from 'react'; +import { Table } from 'antd'; +import type { TableProps } from 'antd'; +import Widget from 'App/mstore/types/widget'; +import Funnel from 'App/mstore/types/funnel'; +import { ItemMenu } from 'UI'; +import { EllipsisVertical } from 'lucide-react'; +import { exportAntCsv } from '../../../utils'; + +interface Props { + metric?: Widget; + data: { funnel: Funnel }; + compData: { funnel: Funnel }; +} + +function FunnelTable(props: Props) { + const tableData = [ + { + conversion: props.data.funnel.totalConversionsPercentage, + }, + ]; + const tableProps: TableProps['columns'] = [ + { + title: 'Conversion %', + dataIndex: 'conversion', + key: 'conversion', + fixed: 'left', + width: 140, + render: (text: string, _, index) => ( +
+
Overall {index > 0 ? '(previous)' : ''}
+
{text}%
+
+ ), + }, + ]; + + React.useEffect(() => { + const funnel = props.data.funnel; + funnel.stages.forEach((st, ind) => { + const title = `${st.label} ${st.operator} ${st.value.join(' or ')}`; + const wrappedTitle = + title.length > 40 ? title.slice(0, 40) + '...' : title; + tableProps.push({ + title: wrappedTitle, + dataIndex: 'st_' + ind, + key: 'st_' + ind, + ellipsis: true, + width: 120, + }); + tableData[0]['st_' + ind] = st.count; + }); + if (props.compData) { + tableData.push({ + conversion: props.compData.funnel.totalConversionsPercentage, + }) + const compFunnel = props.compData.funnel; + compFunnel.stages.forEach((st, ind) => { + tableData[1]['st_' + ind] = st.count; + }); + } + }, [props.data]); + + return ( +
+
+ ( + index > 0 ? 'opacity-70' : '' + )} + /> + + + + ); +} + +export function TableExporter({ + tableData, + tableColumns, + filename, + top, + right, +}: { + tableData: any; + tableColumns: any; + filename: string; + top?: string; + right?: string; +}) { + const onClick = () => exportAntCsv(tableColumns, tableData, filename); + return ( +
+ + +
+ } + /> + + ); +} + +export default FunnelTable; diff --git a/frontend/app/components/Funnels/FunnelWidget/FunnelWidget.tsx b/frontend/app/components/Funnels/FunnelWidget/FunnelWidget.tsx index a44335318..3ca5d1d78 100644 --- a/frontend/app/components/Funnels/FunnelWidget/FunnelWidget.tsx +++ b/frontend/app/components/Funnels/FunnelWidget/FunnelWidget.tsx @@ -82,7 +82,7 @@ function FunnelWidget(props: Props) { } show={!stages || stages.length === 0} > -
+
{!isWidget && shownStages.map((stage: any, index: any) => ( { } const base64 = base64Url.replace("-", "+").replace("_", "/"); return JSON.parse(atob(base64)); -}; \ No newline at end of file +}; + +function saveAsFile(blob: Blob, filename: string) { + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + + a.href = url; + a.download = filename; + a.click(); + window.URL.revokeObjectURL(url); +} + +export function exportAntCsv(tableColumns, tableData, filename = 'table.csv') { + console.log(tableColumns, tableData) + const headers = tableColumns.map(col => col.title).join(','); + const rows = tableData.map(row => { + return tableColumns + .map(col => { + const value = col.dataIndex ? row[col.dataIndex] : ''; + return typeof value === 'string' ? `"${value.replace(/"/g, '""')}"` : value; + }) + .join(','); + }); + + const csvContent = [headers, ...rows].join('\n'); + console.log(csvContent) + // const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); + + // saveAsFile(blob, filename); +} \ No newline at end of file