diff --git a/frontend/app/components/Charts/ColumnChart.tsx b/frontend/app/components/Charts/ColumnChart.tsx index 26e5c6f58..cce550bc4 100644 --- a/frontend/app/components/Charts/ColumnChart.tsx +++ b/frontend/app/components/Charts/ColumnChart.tsx @@ -25,6 +25,7 @@ export interface DataProps { interface ColumnChartProps extends DataProps { label?: string; + onSeriesFocus?: (name: string) => void; } function ColumnChart(props: ColumnChartProps) { @@ -80,6 +81,10 @@ function ColumnChart(props: ColumnChartProps) { const obs = new ResizeObserver(() => chart.resize()); obs.observe(chartRef.current); + chart.on('click', (event) => { + const focusedSeriesName = event.name; + props.onSeriesFocus?.(focusedSeriesName); + }) return () => { chart.dispose(); diff --git a/frontend/app/components/Charts/PieChart.tsx b/frontend/app/components/Charts/PieChart.tsx index a172bee26..9675aab97 100644 --- a/frontend/app/components/Charts/PieChart.tsx +++ b/frontend/app/components/Charts/PieChart.tsx @@ -18,7 +18,7 @@ interface PieChartProps { }; label?: string; inGrid?: boolean; - onClick?: (filters: any[]) => void; + onSeriesFocus?: (seriesName: string) => void; } function PieChart(props: PieChartProps) { @@ -40,10 +40,10 @@ function PieChart(props: PieChartProps) { return; } - const largestSlice = pieData.reduce((acc, curr) => - curr.value > acc.value ? curr : acc - ); - const largestVal = largestSlice.value || 1; // avoid divide-by-zero + // const largestSlice = pieData.reduce((acc, curr) => + // curr.value > acc.value ? curr : acc + // ); + // const largestVal = largestSlice.value || 1; // avoid divide-by-zero const option = { ...defaultOptions, @@ -75,14 +75,14 @@ function PieChart(props: PieChartProps) { name: d.name, value: d.value, label: { - show: d.value / largestVal >= 0.03, + show: false, //d.value / largestVal >= 0.03, position: 'outside', formatter: (params: any) => { return params.value; }, }, labelLine: { - show: d.value / largestVal >= 0.03, + show: false, // d.value / largestVal >= 0.03, length: 10, length2: 20, lineStyle: { color: '#3EAAAF' }, @@ -105,7 +105,8 @@ function PieChart(props: PieChartProps) { obs.observe(chartRef.current); chartInstance.on('click', function (params) { - onClick([{ name: params.name, value: params.value }]); + const focusedSeriesName = params.name + props.onSeriesFocus?.(focusedSeriesName); }); return () => { diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/BigNumChart.tsx b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/BigNumChart.tsx index 1f97c0892..01d4ca2e8 100644 --- a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/BigNumChart.tsx +++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/BigNumChart.tsx @@ -1,19 +1,21 @@ import React from 'react' import { CompareTag } from "./CustomChartTooltip"; +import cn from 'classnames' interface Props { colors: any; - onClick?: (event, index) => void; yaxis?: any; label?: string; hideLegend?: boolean; values: { value: number, compData?: number, series: string, valueLabel?: string }[]; + onSeriesFocus?: (name: string) => void; } function BigNumChart(props: Props) { const { colors, label = 'Number of Sessions', values, + onSeriesFocus, } = props; return (
@@ -26,19 +28,21 @@ function BigNumChart(props: Props) { label={label} compData={val.compData} valueLabel={val.valueLabel} + onSeriesFocus={onSeriesFocus} /> ))}
) } -function BigNum({ color, series, value, label, compData, valueLabel }: { +function BigNum({ color, series, value, label, compData, valueLabel, onSeriesFocus }: { color: string, series: string, value: number, label: string, compData?: number, valueLabel?: string, + onSeriesFocus?: (name: string) => void }) { const formattedNumber = (num: number) => { return Intl.NumberFormat().format(num); @@ -53,7 +57,13 @@ function BigNum({ color, series, value, label, compData, valueLabel }: { return value - compData; }, [value, compData]) return ( -
+
onSeriesFocus?.(series)} + className={cn( + 'flex flex-col flex-auto justify-center items-center rounded-lg transition-all', + 'hover:transition-all ease-in-out hover:ease-in-out hover:bg-teal/5 hover:cursor-pointer' + )} + >
{series}
diff --git a/frontend/app/components/Dashboard/components/AddCardSection/AddCardSection.tsx b/frontend/app/components/Dashboard/components/AddCardSection/AddCardSection.tsx index 4b3002499..2158dbfac 100644 --- a/frontend/app/components/Dashboard/components/AddCardSection/AddCardSection.tsx +++ b/frontend/app/components/Dashboard/components/AddCardSection/AddCardSection.tsx @@ -12,6 +12,8 @@ import { Combine, Users, Sparkles, + Globe, + MonitorSmartphone, } from 'lucide-react'; import { Icon } from 'UI'; import FilterSeries from 'App/mstore/types/filterSeries'; @@ -129,6 +131,18 @@ export const tabItems: Record = { type: FilterKey.USERID, description: 'Identify the users with the most interactions.', }, + { + icon: , + title: 'Top Countries', + type: FilterKey.LOCATION, + description: 'Track the geographical distribution of your audience.', + }, + { + icon: , + title: 'Top Devices', + type: FilterKey.USER_DEVICE, + description: 'Explore the devices used by your users.', + } // { TODO: 1.23+ maybe // icon: , // title: 'Speed Index by Country', diff --git a/frontend/app/components/Dashboard/components/MetricViewHeader/MetricViewHeader.tsx b/frontend/app/components/Dashboard/components/MetricViewHeader/MetricViewHeader.tsx index 53c9b833c..bc1191654 100644 --- a/frontend/app/components/Dashboard/components/MetricViewHeader/MetricViewHeader.tsx +++ b/frontend/app/components/Dashboard/components/MetricViewHeader/MetricViewHeader.tsx @@ -8,25 +8,40 @@ import { useStore } from 'App/mstore'; import { observer } from 'mobx-react-lite'; import { DROPDOWN_OPTIONS } from 'App/constants/card'; +const options = [ + { + key: 'all', + label: 'All Types', + }, + ...DROPDOWN_OPTIONS.map((option) => ({ + key: option.value, + label: option.label, + })), + { + key: 'monitors', + label: 'Monitors', + }, + { + key: 'web_analytics', + label: 'Web Analytics', + }, +] + function MetricViewHeader() { const { metricStore } = useStore(); const filter = metricStore.filter; useEffect(() => { - // Set the default sort order to 'desc' metricStore.updateKey('sort', { by: 'desc' }); }, [metricStore]); - // Handler for dropdown menu selection const handleMenuClick = ({ key }) => { metricStore.updateKey('filter', { ...filter, type: key }); }; - // Dropdown menu options const menu = ( - All Types - {DROPDOWN_OPTIONS.map((option) => ( - {option.label} + {options.map((option) => ( + {option.label} ))} ); @@ -39,10 +54,7 @@ function MetricViewHeader() { diff --git a/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx b/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx index 853d6ec1b..2699bf4c7 100644 --- a/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx +++ b/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx @@ -214,6 +214,10 @@ function WidgetChart(props: Props) { ]); useEffect(loadPage, [_metric.page]); + const onFocus = (seriesName: string)=> { + metricStore.setFocusedSeriesName(seriesName); + } + const renderChart = React.useCallback(() => { const { metricType, metricOf } = _metric; const viewType = _metric.viewType; @@ -351,7 +355,7 @@ function WidgetChart(props: Props) { compData={compData} params={params} colors={colors} - onClick={onChartClick} + onSeriesFocus={onFocus} label={ _metric.metricOf === 'sessionCount' ? 'Number of Sessions' @@ -366,7 +370,7 @@ function WidgetChart(props: Props) { ([]); const isMounted = useIsMounted(); const [loading, setLoading] = useState(false); - const filteredSessions = getListSessionsBySeries(data, activeSeries); + // all filtering done through series now + const filteredSessions = getListSessionsBySeries(data, 'all'); const { dashboardStore, metricStore, sessionStore, customFieldStore } = useStore(); + const focusedSeries = metricStore.focusedSeriesName; const filter = dashboardStore.drillDownFilter; const widget = metricStore.instance; const startTime = DateTime.fromMillis(filter.startTimestamp).toFormat('LLL dd, yyyy HH:mm'); @@ -44,15 +46,14 @@ function WidgetSessions(props: Props) { ) })); - const writeOption = ({ value }: any) => setActiveSeries(value.value); useEffect(() => { - if (!data) return; - const seriesOptions = data.map((item: any) => ({ - label: item.seriesName, + if (!widget.series) return; + const seriesOptions = widget.series.map((item: any) => ({ + label: item.name, value: item.seriesId })); setSeriesOptions([{ label: 'All', value: 'all' }, ...seriesOptions]); - }, [data]); + }, [widget.series]); const fetchSessions = (metricId: any, filter: any) => { if (!isMounted()) return; @@ -99,9 +100,10 @@ function WidgetSessions(props: Props) { }; debounceClickMapSearch(customFilter); } else { + const usedSeries = focusedSeries ? widget.series.filter((s) => s.name === focusedSeries) : widget.series; debounceRequest(widget.metricId, { ...filter, - series: widget.series.map((s) => s.toJson()), + series: usedSeries.map((s) => s.toJson()), page: metricStore.sessionsPage, limit: metricStore.sessionsPageSize }); @@ -116,9 +118,23 @@ function WidgetSessions(props: Props) { filter.filters, depsString, metricStore.clickMapSearch, - activeSeries + focusedSeries ]); useEffect(loadData, [metricStore.sessionsPage]); + useEffect(() => { + if (activeSeries === 'all') { + metricStore.setFocusedSeriesName(null); + } else { + metricStore.setFocusedSeriesName(seriesOptions.find((option) => option.value === activeSeries)?.label, false); + } + }, [activeSeries]) + useEffect(() => { + if (focusedSeries) { + setActiveSeries(seriesOptions.find((option) => option.label === focusedSeries)?.value || 'all'); + } else { + setActiveSeries('all'); + } + }, [focusedSeries]) const clearFilters = () => { metricStore.updateKey('sessionsPage', 1); diff --git a/frontend/app/mstore/metricStore.ts b/frontend/app/mstore/metricStore.ts index 3598a0956..db2a2e2d9 100644 --- a/frontend/app/mstore/metricStore.ts +++ b/frontend/app/mstore/metricStore.ts @@ -18,6 +18,37 @@ import { clickmapFilter } from 'App/types/filter/newFilter'; import { getRE } from 'App/utils'; import { FilterKey } from 'Types/filter/filterType'; +const handleFilter = (card: Widget, filterType?: string) => { + const metricType = card.metricType; + if (filterType === 'all' || !filterType || !metricType) { + return true; + } + if ([CATEGORIES.monitors, CATEGORIES.web_analytics].includes(filterType)) { + if (metricType !== 'table') return false; + const metricOf = card.metricOf; + if (filterType === CATEGORIES.monitors) { + return [ + FilterKey.ERRORS, + FilterKey.FETCH, + TIMESERIES + '_4xx_requests', + TIMESERIES + '_slow_network_requests' + ].includes(metricOf) + } + if (filterType === CATEGORIES.web_analytics) { + return [ + FilterKey.LOCATION, + FilterKey.USER_BROWSER, + FilterKey.REFERRER, + FilterKey.USERID, + FilterKey.LOCATION, + FilterKey.USER_DEVICE, + ].includes(metricOf) + } + } else { + return filterType === metricType; + } +} + const cardToCategory = (cardType: string) => { switch (cardType) { case TIMESERIES: @@ -70,6 +101,8 @@ export default class MetricStore { cardCategory: string | null = CATEGORIES.product_analytics; + focusedSeriesName: string | null = null; + constructor() { makeAutoObservable(this); } @@ -89,7 +122,7 @@ export default class MetricStore { (this.filter.showMine ? card.owner === JSON.parse(localStorage.getItem('user')!).account.email : true) && - (this.filter.type === 'all' || card.metricType === this.filter.type) && + handleFilter(card, this.filter.type) && (!dbIds.length || card.dashboards.map((i) => i.dashboardId).some((id) => dbIds.includes(id))) && // @ts-ignore @@ -105,6 +138,14 @@ export default class MetricStore { this.instance.update(metric || new Widget()); } + setFocusedSeriesName(name: string | null, resetOnSame = true) { + if (this.focusedSeriesName === name && resetOnSame) { + this.focusedSeriesName = null; + } else { + this.focusedSeriesName = name; + } + } + setCardCategory(category: string) { this.cardCategory = category; }