diff --git a/frontend/app/components/Dashboard/components/WidgetSessions/WidgetSessions.tsx b/frontend/app/components/Dashboard/components/WidgetSessions/WidgetSessions.tsx index 5ce1d865a..6eea04830 100644 --- a/frontend/app/components/Dashboard/components/WidgetSessions/WidgetSessions.tsx +++ b/frontend/app/components/Dashboard/components/WidgetSessions/WidgetSessions.tsx @@ -1,395 +1,394 @@ -import React, {useEffect, useState} from 'react'; -import {NoContent, Loader, Pagination} from 'UI'; -import {Button, Tag, Tooltip, Dropdown, message} from 'antd'; -import {UndoOutlined, DownOutlined} from '@ant-design/icons'; +import React, { useEffect, useState } from 'react'; +import { NoContent, Loader, Pagination } from 'UI'; +import { Button, Tag, Tooltip, Dropdown, message } from 'antd'; +import { UndoOutlined, DownOutlined } from '@ant-design/icons'; import cn from 'classnames'; -import {useStore} from 'App/mstore'; +import { useStore } from 'App/mstore'; import SessionItem from 'Shared/SessionItem'; -import {observer} from 'mobx-react-lite'; -import {DateTime} from 'luxon'; -import {debounce, numberWithCommas} from 'App/utils'; +import { observer } from 'mobx-react-lite'; +import { DateTime } from 'luxon'; +import { debounce, numberWithCommas } from 'App/utils'; import useIsMounted from 'App/hooks/useIsMounted'; -import AnimatedSVG, {ICONS} from 'Shared/AnimatedSVG/AnimatedSVG'; -import {HEATMAP, USER_PATH, FUNNEL} from 'App/constants/card'; -import {useTranslation} from 'react-i18next'; +import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG'; +import { HEATMAP, USER_PATH, FUNNEL } from 'App/constants/card'; +import { useTranslation } from 'react-i18next'; interface Props { - className?: string; + className?: string; } function WidgetSessions(props: Props) { - const {t} = useTranslation(); - const listRef = React.useRef(null); - const {className = ''} = props; - const [activeSeries, setActiveSeries] = useState('all'); - const [data, setData] = useState([]); - const isMounted = useIsMounted(); - const [loading, setLoading] = useState(false); - // 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', - ); - const endTime = DateTime.fromMillis(filter.endTimestamp).toFormat( - 'LLL dd, yyyy HH:mm', - ); - const [seriesOptions, setSeriesOptions] = useState([ - {label: t('All'), value: 'all'}, - ]); - const hasFilters = - filter.filters.length > 0 || - filter.startTimestamp !== dashboardStore.drillDownPeriod.start || - filter.endTimestamp !== dashboardStore.drillDownPeriod.end; - const filterText = filter.filters.length > 0 ? filter.filters[0].value : ''; - const metaList = customFieldStore.list.map((i: any) => i.key); + const { t } = useTranslation(); + const listRef = React.useRef(null); + const { className = '' } = props; + const [activeSeries, setActiveSeries] = useState('all'); + const [data, setData] = useState([]); + const isMounted = useIsMounted(); + const [loading, setLoading] = useState(false); + // 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', + ); + const endTime = DateTime.fromMillis(filter.endTimestamp).toFormat( + 'LLL dd, yyyy HH:mm', + ); + const [seriesOptions, setSeriesOptions] = useState([ + { label: t('All'), value: 'all' }, + ]); + const hasFilters = + filter.filters.length > 0 || + filter.startTimestamp !== dashboardStore.drillDownPeriod.start || + filter.endTimestamp !== dashboardStore.drillDownPeriod.end; + const filterText = filter.filters.length > 0 ? filter.filters[0].value : ''; + const metaList = customFieldStore.list.map((i: any) => i.key); - const seriesDropdownItems = seriesOptions.map((option) => ({ - key: option.value, - label: ( -
setActiveSeries(option.value)}>{option.label}
- ), + const seriesDropdownItems = seriesOptions.map((option) => ({ + key: option.value, + label: ( +
setActiveSeries(option.value)}>{option.label}
+ ), + })); + + useEffect(() => { + if (!widget.series) return; + const seriesOptions = widget.series.map((item: any) => ({ + label: item.name, + value: item.seriesId ?? item.name, })); + setSeriesOptions([{ label: t('All'), value: 'all' }, ...seriesOptions]); + }, [widget.series.length]); - useEffect(() => { - if (!widget.series) return; - const seriesOptions = widget.series.map((item: any) => ({ - label: item.name, - value: item.seriesId ?? item.name, - })); - setSeriesOptions([{label: t('All'), value: 'all'}, ...seriesOptions]); - }, [widget.series.length]); + const fetchSessions = (metricId: any, filter: any) => { + if (!isMounted()) return; - const fetchSessions = (metricId: any, filter: any) => { - if (!isMounted()) return; + if (widget.metricType === FUNNEL) { + if (filter.series[0].filter.filters.length === 0) { + setLoading(false); + return setData([]); + } + } - if (widget.metricType === FUNNEL) { - if (filter.series[0].filter.filters.length === 0) { - setLoading(false); - return setData([]); - } + setLoading(true); + const filterCopy = { ...filter }; + delete filterCopy.eventsOrderSupport; + + try { + // Handle filters properly with null checks + if (filterCopy.filters && filterCopy.filters.length > 0) { + // Ensure the nested path exists before pushing + if (filterCopy.series?.[0]?.filter) { + if (!filterCopy.series[0].filter.filters) { + filterCopy.series[0].filter.filters = []; + } + filterCopy.series[0].filter.filters.push(...filterCopy.filters); } - - - setLoading(true); - const filterCopy = {...filter}; - delete filterCopy.eventsOrderSupport; - - try { - // Handle filters properly with null checks - if (filterCopy.filters && filterCopy.filters.length > 0) { - // Ensure the nested path exists before pushing - if (filterCopy.series?.[0]?.filter) { - if (!filterCopy.series[0].filter.filters) { - filterCopy.series[0].filter.filters = []; - } - filterCopy.series[0].filter.filters.push(...filterCopy.filters); - } - filterCopy.filters = []; - } - } catch (e) { - // do nothing + filterCopy.filters = []; + } + } catch (e) { + // do nothing + } + widget + .fetchSessions(metricId, filterCopy) + .then((res: any) => { + setData(res); + if (metricStore.drillDown) { + setTimeout(() => { + message.info(t('Sessions Refreshed!')); + listRef.current?.scrollIntoView({ behavior: 'smooth' }); + metricStore.setDrillDown(false); + }, 0); } - widget - .fetchSessions(metricId, filterCopy) - .then((res: any) => { - setData(res); - if (metricStore.drillDown) { - setTimeout(() => { - message.info(t('Sessions Refreshed!')); - listRef.current?.scrollIntoView({behavior: 'smooth'}); - metricStore.setDrillDown(false); - }, 0); - } - }) - .finally(() => { - setLoading(false); - }); - }; - const fetchClickmapSessions = (customFilters: Record) => { - sessionStore.getSessions(customFilters).then((data) => { - setData([{...data, seriesId: 1, seriesName: 'Clicks'}]); - }); - }; - const debounceRequest: any = React.useCallback( - debounce(fetchSessions, 1000), - [], - ); - const debounceClickMapSearch = React.useCallback( - debounce(fetchClickmapSessions, 1000), - [], - ); + }) + .finally(() => { + setLoading(false); + }); + }; + const fetchClickmapSessions = (customFilters: Record) => { + sessionStore.getSessions(customFilters).then((data) => { + setData([{ ...data, seriesId: 1, seriesName: 'Clicks' }]); + }); + }; + const debounceRequest: any = React.useCallback( + debounce(fetchSessions, 1000), + [], + ); + const debounceClickMapSearch = React.useCallback( + debounce(fetchClickmapSessions, 1000), + [], + ); - const depsString = JSON.stringify(widget.series); + const depsString = JSON.stringify(widget.series); - const loadData = () => { - if (widget.metricType === HEATMAP && metricStore.clickMapSearch) { - const clickFilter = { - value: [metricStore.clickMapSearch], - type: 'CLICK', - operator: 'onSelector', - isEvent: true, - // @ts-ignore - filters: [], - }; - const timeRange = { - rangeValue: dashboardStore.drillDownPeriod.rangeValue, - startDate: dashboardStore.drillDownPeriod.start, - endDate: dashboardStore.drillDownPeriod.end, - }; - const customFilter = { - ...filter, - ...timeRange, - filters: [...sessionStore.userFilter.filters, clickFilter], - }; - debounceClickMapSearch(customFilter); - } else { - const hasStartPoint = - !!widget.startPoint && widget.metricType === USER_PATH; - const onlyFocused = focusedSeries - ? widget.series.filter((s) => s.name === focusedSeries) - : widget.series; - const activeSeries = metricStore.disabledSeries.length - ? onlyFocused.filter( - (s) => !metricStore.disabledSeries.includes(s.name), - ) - : onlyFocused; - const seriesJson = activeSeries.map((s) => s.toJson()); - if (hasStartPoint) { - seriesJson[0].filter.filters.push(widget.startPoint.toJson()); - } - if (widget.metricType === USER_PATH) { - if ( - seriesJson[0].filter.filters[0].value[0] === '' && - widget.data.nodes?.length - ) { - seriesJson[0].filter.filters[0].value = widget.data.nodes[0].name; - } else if ( - seriesJson[0].filter.filters[0].value[0] === '' && - !widget.data.nodes?.length - ) { - // no point requesting if we don't have starting point picked by api - return; - } - } - debounceRequest(widget.metricId, { - ...filter, - series: seriesJson, - page: metricStore.sessionsPage, - limit: metricStore.sessionsPageSize, - }); + const loadData = () => { + if (widget.metricType === HEATMAP && metricStore.clickMapSearch) { + const clickFilter = { + value: [metricStore.clickMapSearch], + type: 'CLICK', + operator: 'onSelector', + isEvent: true, + // @ts-ignore + filters: [], + }; + const timeRange = { + rangeValue: dashboardStore.drillDownPeriod.rangeValue, + startDate: dashboardStore.drillDownPeriod.start, + endDate: dashboardStore.drillDownPeriod.end, + }; + const customFilter = { + ...filter, + ...timeRange, + filters: [...sessionStore.userFilter.filters, clickFilter], + }; + debounceClickMapSearch(customFilter); + } else { + const hasStartPoint = + !!widget.startPoint && widget.metricType === USER_PATH; + const onlyFocused = focusedSeries + ? widget.series.filter((s) => s.name === focusedSeries) + : widget.series; + const activeSeries = metricStore.disabledSeries.length + ? onlyFocused.filter( + (s) => !metricStore.disabledSeries.includes(s.name), + ) + : onlyFocused; + const seriesJson = activeSeries.map((s) => s.toJson()); + if (hasStartPoint) { + seriesJson[0].filter.filters.push(widget.startPoint.toJson()); + } + if (widget.metricType === USER_PATH) { + if ( + seriesJson[0].filter.filters[0].value[0] === '' && + widget.data.nodes?.length + ) { + seriesJson[0].filter.filters[0].value = widget.data.nodes[0].name; + } else if ( + seriesJson[0].filter.filters[0].value[0] === '' && + !widget.data.nodes?.length + ) { + // no point requesting if we don't have starting point picked by api + return; } - }; - useEffect(() => { - metricStore.updateKey('sessionsPage', 1); - loadData(); - }, [ - filter.startTimestamp, - filter.endTimestamp, - filter.filters, - depsString, - metricStore.clickMapSearch, - focusedSeries, - widget.startPoint, - widget.data.nodes, - metricStore.disabledSeries.length, - ]); - 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]); + } + debounceRequest(widget.metricId, { + ...filter, + series: seriesJson, + page: metricStore.sessionsPage, + limit: metricStore.sessionsPageSize, + }); + } + }; + useEffect(() => { + metricStore.updateKey('sessionsPage', 1); + loadData(); + }, [ + filter.startTimestamp, + filter.endTimestamp, + filter.filters, + depsString, + metricStore.clickMapSearch, + focusedSeries, + widget.startPoint, + widget.data?.nodes, + metricStore.disabledSeries.length, + ]); + 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); - dashboardStore.resetDrillDownFilter(); - }; + const clearFilters = () => { + metricStore.updateKey('sessionsPage', 1); + dashboardStore.resetDrillDownFilter(); + }; - return ( -
-
-
-
-

- {metricStore.clickMapSearch ? t('Clicks') : t('Sessions')} -

-
- {metricStore.clickMapLabel - ? `on "${metricStore.clickMapLabel}" ` - : null} - {t('between')}{' '} - + return ( +
+
+
+
+

+ {metricStore.clickMapSearch ? t('Clicks') : t('Sessions')} +

+
+ {metricStore.clickMapLabel + ? `on "${metricStore.clickMapLabel}" ` + : null} + {t('between')}{' '} + {startTime} {' '} - {t('and')}{' '} - + {t('and')}{' '} + {endTime} {' '} -
- {hasFilters && ( - - - - )} -
+
+ {hasFilters && ( + + + + )} +
- {hasFilters && widget.metricType === 'table' && ( -
- - {filterText} - -
- )} -
+ {hasFilters && widget.metricType === 'table' && ( +
+ + {filterText} + +
+ )} +
-
- {widget.metricType !== 'table' && widget.metricType !== HEATMAP && ( -
+
+ {widget.metricType !== 'table' && widget.metricType !== HEATMAP && ( +
{t('Filter by Series')} - - - -
- )} -
+ + +
+ )} +
+
-
- - - -
-
- {t('No relevant sessions found for the selected time period')} -
-
- } - show={filteredSessions.sessions.length === 0} - > - {filteredSessions.sessions.map((session: any) => ( - - -
- - ))} +
+ + + +
+
+ {t('No relevant sessions found for the selected time period')} +
+
+ } + show={filteredSessions.sessions.length === 0} + > + {filteredSessions.sessions.map((session: any) => ( + + +
+ + ))} -
-
- {t('Showing')}{' '} - +
+
+ {t('Showing')}{' '} + {(metricStore.sessionsPage - 1) * - metricStore.sessionsPageSize + - 1} + metricStore.sessionsPageSize + + 1} {' '} - {t('to')}{' '} - + {t('to')}{' '} + {(metricStore.sessionsPage - 1) * - metricStore.sessionsPageSize + - filteredSessions.sessions.length} + metricStore.sessionsPageSize + + filteredSessions.sessions.length} {' '} - {t('of')}{' '} - + {t('of')}{' '} + {numberWithCommas(filteredSessions.total)} {' '} - {t('sessions.')} -
- - metricStore.updateKey('sessionsPage', page) - } - limit={metricStore.sessionsPageSize} - debounceRequest={500} - /> -
- - + {t('sessions.')} +
+ + metricStore.updateKey('sessionsPage', page) + } + limit={metricStore.sessionsPageSize} + debounceRequest={500} + />
-
- ); +
+
+
+
+ ); } const getListSessionsBySeries = (data: any, seriesId: any) => { - const arr = data.reduce( - (arr: any, element: any) => { - if (seriesId === 'all') { - const sessionIds = arr.sessions.map((i: any) => i.sessionId); - const sessions = element.sessions.filter( - (i: any) => !sessionIds.includes(i.sessionId), - ); - arr.sessions.push(...sessions); - } else if (element.seriesId === seriesId) { - const sessionIds = arr.sessions.map((i: any) => i.sessionId); - const sessions = element.sessions.filter( - (i: any) => !sessionIds.includes(i.sessionId), - ); - const duplicates = element.sessions.length - sessions.length; - arr.sessions.push(...sessions); - arr.total = element.total - duplicates; - } - return arr; - }, - {sessions: []}, - ); - arr.total = - seriesId === 'all' - ? Math.max(...data.map((i: any) => i.total)) - : data.find((i: any) => i.seriesId === seriesId).total; - return arr; + const arr = data.reduce( + (arr: any, element: any) => { + if (seriesId === 'all') { + const sessionIds = arr.sessions.map((i: any) => i.sessionId); + const sessions = element.sessions.filter( + (i: any) => !sessionIds.includes(i.sessionId), + ); + arr.sessions.push(...sessions); + } else if (element.seriesId === seriesId) { + const sessionIds = arr.sessions.map((i: any) => i.sessionId); + const sessions = element.sessions.filter( + (i: any) => !sessionIds.includes(i.sessionId), + ); + const duplicates = element.sessions.length - sessions.length; + arr.sessions.push(...sessions); + arr.total = element.total - duplicates; + } + return arr; + }, + { sessions: [] }, + ); + arr.total = + seriesId === 'all' + ? Math.max(...data.map((i: any) => i.total)) + : data.find((i: any) => i.seriesId === seriesId).total; + return arr; }; export default observer(WidgetSessions); diff --git a/frontend/app/mstore/types/widget.ts b/frontend/app/mstore/types/widget.ts index 5ce757500..2f0ae0842 100644 --- a/frontend/app/mstore/types/widget.ts +++ b/frontend/app/mstore/types/widget.ts @@ -1,4 +1,4 @@ -import { makeAutoObservable, runInAction } from 'mobx'; +import { makeAutoObservable, runInAction, observable } from 'mobx'; import FilterSeries from './filterSeries'; import { DateTime } from 'luxon'; import Session from 'App/mstore/types/session'; @@ -433,13 +433,15 @@ export default class Widget { } if (!isComparison) { - runInAction(() => { - Object.assign(this.data, _data); - }); + this.setDataValue(_data); } return _data; } + setDataValue = (data: any) => { + this.data = observable({ ...data }); + }; + fetchSessions(metricId: any, filter: any): Promise { return new Promise((resolve) => { metricService.fetchSessions(metricId, filter).then((response: any[]) => {