diff --git a/packages/aesirx-bi-app/package.json b/packages/aesirx-bi-app/package.json index 65e9324d..f414cd69 100644 --- a/packages/aesirx-bi-app/package.json +++ b/packages/aesirx-bi-app/package.json @@ -1,6 +1,6 @@ { "name": "aesirx-bi-app", - "version": "2.7.2", + "version": "2.8.0", "license": "GPL-3.0-only", "author": "AesirX", "main": "dist/index.js", diff --git a/packages/aesirx-bi-app/src/containers/Dashboard/Dashboard.jsx b/packages/aesirx-bi-app/src/containers/Dashboard/Dashboard.jsx index 24e682c5..d1a5936d 100644 --- a/packages/aesirx-bi-app/src/containers/Dashboard/Dashboard.jsx +++ b/packages/aesirx-bi-app/src/containers/Dashboard/Dashboard.jsx @@ -35,6 +35,7 @@ const Dashboard = observer( this.dashboardListViewModel = this.viewModel ? this.viewModel.getDashboardListViewModel() : null; + this.realtimeInterval = null; } componentDidUpdate = (prevProps) => { @@ -61,7 +62,7 @@ const Dashboard = observer( ?.reduce((acc, curr) => ({ ...acc, ...curr }), {}), }); try { - setInterval(async () => { + this.realtimeInterval = setInterval(async () => { this.dashboardListViewModel.getLiveVisitorsTotal( { ...this.context.biListViewModel.activeDomain @@ -82,12 +83,19 @@ const Dashboard = observer( }, true ); - }, 30000); + }, 15000); } catch (e) { console.log(e); } }; + componentWillUnmount() { + if (this.realtimeInterval) { + clearInterval(this.realtimeInterval); + this.realtimeInterval = null; + } + } + handleDateRangeChange = (startDate, endDate) => { this.dashboardListViewModel.handleFilterDateRange(startDate ?? endDate, endDate ?? startDate); }; @@ -228,14 +236,14 @@ const Dashboard = observer( {this.props.integration ? ( this.props.handleChangeLink(e, `/flow-list`)} + onClick={(e) => this.props.handleChangeLink(e, `/visitors/realtime`)} className={'text-secondary-50 text-nowrap fw-medium'} > {t('txt_view_more')} ) : ( {t('txt_view_more')} diff --git a/packages/aesirx-bi-app/src/containers/Dashboard/DashboardViewModels/DashboardListViewModel.js b/packages/aesirx-bi-app/src/containers/Dashboard/DashboardViewModels/DashboardListViewModel.js index 8e5c24d5..19d1f6a0 100644 --- a/packages/aesirx-bi-app/src/containers/Dashboard/DashboardViewModels/DashboardListViewModel.js +++ b/packages/aesirx-bi-app/src/containers/Dashboard/DashboardViewModels/DashboardListViewModel.js @@ -115,6 +115,7 @@ class DashboardListViewModel { this.dashboardStore.getVisitors( { ...this.dataFilter, + 'filter_not[visibility_change]': 'true', page_size: '1000', }, dateRangeFilter, @@ -248,6 +249,7 @@ class DashboardListViewModel { ...this.dataFilterPages, ...dataFilter, ...this.sortByAttribute, + 'filter_not[visibility_change]': 'true', }; const dateRangeFilter = { ...this.globalStoreViewModel.dateFilter, ...dateFilter }; diff --git a/packages/aesirx-bi-app/src/containers/RealTimePage/Component/Overview.jsx b/packages/aesirx-bi-app/src/containers/RealTimePage/Component/Overview.jsx new file mode 100644 index 00000000..839e5e2e --- /dev/null +++ b/packages/aesirx-bi-app/src/containers/RealTimePage/Component/Overview.jsx @@ -0,0 +1,48 @@ +import React, { Component } from 'react'; +import { withTranslation } from 'react-i18next'; +import { observer } from 'mobx-react'; +import { withRouter } from 'react-router-dom'; +import BarChartComponent from 'components/BarChartComponent'; + +const OverviewComponent = observer( + class OverviewComponent extends Component { + constructor(props) { + super(props); + const { listViewModel } = props; + this.listViewModel = listViewModel ? listViewModel : null; + this.state = { loading: false }; + } + + render() { + const { t, status, bars, barColors, data, filterData } = this.props; + return ( +
+ +
+ ); + } + } +); +export default withTranslation()(withRouter(OverviewComponent)); diff --git a/packages/aesirx-bi-app/src/containers/RealTimePage/Component/RealTimeTable.jsx b/packages/aesirx-bi-app/src/containers/RealTimePage/Component/RealTimeTable.jsx new file mode 100644 index 00000000..ca0b97b9 --- /dev/null +++ b/packages/aesirx-bi-app/src/containers/RealTimePage/Component/RealTimeTable.jsx @@ -0,0 +1,107 @@ +import Table from '../../../components/Table'; +import React from 'react'; +import { withTranslation } from 'react-i18next'; +import { Tooltip } from 'react-tooltip'; +import { PAGE_STATUS, RingLoaderComponent } from 'aesirx-uikit'; +import ComponentNoData from 'components/ComponentNoData'; +import { env } from 'aesirx-lib'; +import ComponentSVG from 'components/ComponentSVG'; +const RealTimeTable = (props) => { + const { + data, + t, + isPagination = true, + simplePagination = false, + pagination, + selectPage, + selectPageSize, + status, + limit, + isPaginationAPI = isPagination ? true : false, + sortAPI, + handleSort, + sortBy, + } = props; + const columnsTable = React.useMemo( + () => + data?.header.map((item, index) => { + let tooltip = ''; + switch (item?.accessor) { + default: + tooltip = ''; + } + return { + ...item, + className: `px-3 py-16 fs-sm fw-semibold border-bottom ${ + index + 1 === data?.header.length ? 'rounded-top-end-3' : '' + } ${index === 0 ? 'rounded-top-start-3' : ''}`, + width: item.width ? item.width : index === 0 ? 'auto' : 170, + allowSort: item?.allowSort || false, + Header: ( + + {t(item.Header)} + {tooltip && ( + <> +
+ +
+ + + )} +
+ ), + }; + }), + [data?.header] + ); + const dataTable = React.useMemo(() => data?.data, [data?.data]); + return ( +
+ {status === PAGE_STATUS.LOADING ? ( + + ) : data ? ( + + ) : ( +
+
+ +
+
+ )} + + ); +}; +export default withTranslation()(RealTimeTable); diff --git a/packages/aesirx-bi-app/src/containers/RealTimePage/RealTime.jsx b/packages/aesirx-bi-app/src/containers/RealTimePage/RealTime.jsx new file mode 100644 index 00000000..65e2fd8e --- /dev/null +++ b/packages/aesirx-bi-app/src/containers/RealTimePage/RealTime.jsx @@ -0,0 +1,280 @@ +import React, { Component } from 'react'; +import { withTranslation } from 'react-i18next'; +import { withRealTimeViewModel } from './RealTimeViewModels/RealTimeViewModelContextProvider'; +import { observer } from 'mobx-react'; +import { BiViewModelContext } from '../../store/BiStore/BiViewModelContextProvider'; +import { withRouter } from 'react-router-dom'; +import PAGE_STATUS from '../../constants/PageStatus'; +import { RingLoaderComponent, Image } from 'aesirx-uikit'; +import RealTimeTable from './Component/RealTimeTable'; +import ComponentNoData from '../../components/ComponentNoData'; +import { BI_SUMMARY_FIELD_KEY, BI_DEVICES_FIELD_KEY, env, Helper } from 'aesirx-lib'; +import 'flag-icons/sass/flag-icons.scss'; +import queryString from 'query-string'; +import { Col, Row, Spinner } from 'react-bootstrap'; +import { AesirXSelect } from 'aesirx-uikit'; +const RealTime = observer( + class RealTime extends Component { + static contextType = BiViewModelContext; + + constructor(props) { + super(props); + const { viewModel } = props; + this.viewModel = viewModel ? viewModel : null; + + this.realTimeListViewModel = this.viewModel + ? this.viewModel.getRealTimeListViewModel() + : null; + this.params = queryString.parse(props.location.search); + this.realtimeInterval = null; + } + + loadRealTimeData = async (isReload) => { + if ( + (this.realTimeListViewModel?.realtimeTableData?.pagination?.page === 1 && + this.realTimeListViewModel?.realtimeTableData?.pagination?.page_size === 20) || + !isReload + ) { + this.realTimeListViewModel.initialize( + { + ...this.context.biListViewModel.activeDomain + ?.map((value, index) => ({ + [`filter[domain][${index + 1}]`]: value, + })) + ?.reduce((acc, curr) => ({ ...acc, ...curr }), {}), + ...(this.params?.pagination && { page: this.params?.pagination }), + }, + {}, + { + ...(this.params['sort[]'] + ? { 'sort[]': this.params['sort[]'] } + : { 'sort[]': 'start' }), + ...(this.params['sort_direction[]'] + ? { + 'sort_direction[]': this.params['sort_direction[]'], + } + : { 'sort_direction[]': 'desc' }), + }, + isReload + ); + } + Promise.all([ + this.realTimeListViewModel.getLiveVisitorsTotal( + this.context.biListViewModel.activeDomain + ?.map((value, index) => ({ + [`filter[domain][${index + 1}]`]: value, + })) + ?.reduce((acc, curr) => ({ ...acc, ...curr }), {}), + isReload + ), + this.realTimeListViewModel.getLiveVisitorsDevice( + this.context.biListViewModel.activeDomain + ?.map((value, index) => ({ + [`filter[domain][${index + 1}]`]: value, + })) + ?.reduce((acc, curr) => ({ ...acc, ...curr }), {}), + isReload + ), + ]); + }; + componentDidMount = () => { + this.loadRealTimeData(false); + try { + this.realtimeInterval = setInterval(async () => { + this.loadRealTimeData(true); + }, 15000); + } catch (e) { + console.log(e); + } + }; + + componentWillUnmount() { + if (this.realtimeInterval) { + clearInterval(this.realtimeInterval); + this.realtimeInterval = null; + } + } + + render() { + const { t } = this.props; + const { statusTable } = this.realTimeListViewModel; + const realTimeSyncArr = [ + { label: 'Last 1 minutes', value: 1 }, + { label: 'Last 5 minutes', value: 5 }, + { label: 'Last 15 minutes', value: 15 }, + { label: 'Last 30 minutes', value: 30 }, + ]; + return ( + <> +
+
+
+

Real-Time Visitors

+
+
+
+
Time window:
+
+ {this.realTimeListViewModel?.formSelectTimeStatus === PAGE_STATUS.LOADING ? ( +
+ +
+ ) : ( + <> + )} + { + await this.realTimeListViewModel.updateConsentsTemplate({ + domain: this.context.biListViewModel.activeDomain[0], + realtime_sync: data?.value, + }); + this.context.biListViewModel.dataStream.realtime_sync = data?.value; + await this.loadRealTimeData(false); + }} + value={ + this.context.biListViewModel.dataStream?.realtime_sync + ? realTimeSyncArr?.find( + (e) => e.value === this.context.biListViewModel.dataStream?.realtime_sync + ) + : { + label: 'Last 5 minutes', + value: 5, + } + } + /> +
+
+ +
+
+
+
+
{t('txt_real_time_active_users')}
+
+ {this.realTimeListViewModel?.statusLiveVisitorsTotal === + PAGE_STATUS.LOADING ? ( + + ) : ( + Helper.numberWithCommas(this.realTimeListViewModel.liveVisitorsTotalData) + )} +
+
+
+
+ + {this.realTimeListViewModel.liveVisitorsDeviceData?.map((device, index) => { + let imgIcon = `${env.PUBLIC_URL}/assets/images/device_mobile.png`; + switch (device[BI_DEVICES_FIELD_KEY?.DEVICE]) { + case 'desktop': + imgIcon = `${env.PUBLIC_URL}/assets/images/device_desktop.png`; + break; + case 'iPad': + imgIcon = `${env.PUBLIC_URL}/assets/images/device_tablet.png`; + break; + case 'tablet': + imgIcon = `${env.PUBLIC_URL}/assets/images/device_tablet.png`; + break; + } + return ( + +
+
+ +
+ {device[BI_DEVICES_FIELD_KEY?.DEVICE] + ? device[BI_DEVICES_FIELD_KEY?.DEVICE] + : 'Unknown'} +
+
+
+ {this.realTimeListViewModel?.statusLiveVisitorsList === + PAGE_STATUS.LOADING ? ( + + ) : ( + <> + { +
+ {( + (device[BI_SUMMARY_FIELD_KEY?.NUMBER_OF_VISITORS] / + this.realTimeListViewModel.liveVisitorsDeviceData.reduce( + (a, b) => +a + +b[BI_SUMMARY_FIELD_KEY.NUMBER_OF_VISITORS], + 0 + )) * + 100 + )?.toFixed(2)} + % +
+ } +
+ {device[BI_SUMMARY_FIELD_KEY?.NUMBER_OF_VISITORS]} +
+ + )} +
+
+ + ); + })} + +
+ {statusTable === PAGE_STATUS.LOADING ? ( + + ) : this.realTimeListViewModel?.realtimeTableData?.list ? ( + { + await this.realTimeListViewModel.handleFilterRealTime( + { page: value }, + this.props.integration + ); + }} + selectPageSize={async (value) => { + await this.realTimeListViewModel.handleFilterRealTime( + { + page: 1, + page_size: value, + }, + this.props.integration + ); + }} + status={statusTable} + {...this.props} + /> + ) : ( +
+
+ +
+
+ )} +
+ + + ); + } + } +); +export default withTranslation()(withRouter(withRealTimeViewModel(RealTime))); diff --git a/packages/aesirx-bi-app/src/containers/RealTimePage/RealTimeModel/RealTimeModel.js b/packages/aesirx-bi-app/src/containers/RealTimePage/RealTimeModel/RealTimeModel.js new file mode 100644 index 00000000..8aed4847 --- /dev/null +++ b/packages/aesirx-bi-app/src/containers/RealTimePage/RealTimeModel/RealTimeModel.js @@ -0,0 +1,187 @@ +/* + * @copyright Copyright (C) 2022 AesirX. All rights reserved. + * @license GNU General Public License version 3, see LICENSE. + */ +import React from 'react'; +import { BI_FLOW_LIST_FIELD_KEY } from 'aesirx-lib'; +import { Link } from 'react-router-dom'; +import { timeAgo } from 'utils'; + +class RealTimeModel { + data = []; + globalViewModel = null; + constructor(entity, globalViewModel) { + if (entity) { + this.data = entity ?? []; + this.globalViewModel = globalViewModel; + } + } + toRaw = () => { + return this.data; + }; + handleChangeLink = (e, link) => { + e.preventDefault(); + if (link) { + this.globalViewModel.setIntegrationLink(link); + } + }; + toRealTimeTable = (integration, utm_currency = '') => { + const headerTable = [ + 'ID', + 'Last activity', + 'Current/last page', + 'Source', + 'Device', + 'Browser', + 'Country', + 'Session metric value', + ]; + const accessor = [ + BI_FLOW_LIST_FIELD_KEY.FLOW_UUID, + BI_FLOW_LIST_FIELD_KEY.END, + BI_FLOW_LIST_FIELD_KEY.URL, + 'utm_campaign_label', + BI_FLOW_LIST_FIELD_KEY.DEVICE, + BI_FLOW_LIST_FIELD_KEY.BROWSER_NAME, + BI_FLOW_LIST_FIELD_KEY.GEO, + BI_FLOW_LIST_FIELD_KEY.EVENTS, + ]; + if (this.data?.length) { + const header = accessor.map((key, index) => { + return { + Header: headerTable[index], + accessor: key, + width: + key === BI_FLOW_LIST_FIELD_KEY.URL + ? 350 + : key === BI_FLOW_LIST_FIELD_KEY.EVENTS || key === 'utm_campaign_label' + ? 150 + : key === BI_FLOW_LIST_FIELD_KEY.END + ? 120 + : 80, + allowSort: true, + Cell: ({ cell, column, row }) => { + if (column.id === BI_FLOW_LIST_FIELD_KEY.GEO) { + return ( +
+ {cell?.value === '' ? ( + <> + ) : ( + + o[BI_FLOW_LIST_FIELD_KEY.GEO]?.country?.code === + row?.values[BI_FLOW_LIST_FIELD_KEY.GEO]?.country?.code + ) + ?.[BI_FLOW_LIST_FIELD_KEY.GEO]?.country?.code?.toLowerCase()}`} + > + )} + {cell?.value === '' ? 'Unknown' : ''} +
+ ); + } else if (column.id === BI_FLOW_LIST_FIELD_KEY.FLOW_UUID) { + return ( + + {cell?.value} + + ); + } else if (column.id === BI_FLOW_LIST_FIELD_KEY.END) { + return
{cell?.value ? timeAgo(cell?.value) : ''}
; + } else if (column.id === BI_FLOW_LIST_FIELD_KEY.DEVICE) { + return
{cell?.value}
; + } else if (column.id === 'utm_campaign_label') { + const referer = row?.original?.events[0]?.referer ?? ''; + const utm_campaign_label = row?.original?.events[0]?.utm_campaign_label ?? ''; + const domain = referer ? new URL(referer).hostname : ''; + let refererHostname = 'Direct'; + switch (domain) { + case '': + refererHostname = 'Direct'; + break; + case 'google.com': + refererHostname = 'Google'; + break; + case 'facebook.com': + refererHostname = 'Facebook'; + break; + case 'linkedin.com': + refererHostname = 'Linkedin'; + break; + case 'yandex.ru': + refererHostname = 'Yandex'; + break; + case 'duckduckgo.com': + refererHostname = 'Duckduckgo'; + break; + case 'reddit.com': + refererHostname = 'Reddit'; + break; + case 'twitter.com': + refererHostname = 'Twitter'; + break; + case 'github.com': + refererHostname = 'Github'; + break; + } + const source = utm_campaign_label ? utm_campaign_label : refererHostname; + return
{source}
; + } else if (column.id === BI_FLOW_LIST_FIELD_KEY.EVENTS) { + const total_utm_value = + cell?.value?.reduce((sum, item) => { + return sum + (item.utm_value || 0); + }, 0) ?? 0; + const total_tag_value = + cell?.value?.reduce((sum, item) => { + return sum + (item.tag_metric_value || 0); + }, 0) ?? 0; + return ( +
+ {total_utm_value + total_tag_value} {utm_currency ? utm_currency : ''} +
+ ); + } else { + return
{cell?.value}
; + } + }, + }; + }); + const data = this.data?.map((item) => { + return { + ...item, + ...accessor + .map((i) => { + return { + [i]: item[i], + }; + }) + .reduce((accumulator, currentValue) => ({ ...currentValue, ...accumulator }), {}), + }; + }); + return { + header, + data: data, + }; + } else { + return { + header: [], + data: [], + }; + } + }; + + getFilterName = () => { + return [{ label: 'Action', value: 'action' }]; + }; +} + +export default RealTimeModel; diff --git a/packages/aesirx-bi-app/src/containers/RealTimePage/RealTimeStore/RealTimeStore.js b/packages/aesirx-bi-app/src/containers/RealTimePage/RealTimeStore/RealTimeStore.js new file mode 100644 index 00000000..9f9b5315 --- /dev/null +++ b/packages/aesirx-bi-app/src/containers/RealTimePage/RealTimeStore/RealTimeStore.js @@ -0,0 +1,109 @@ +import { runInAction } from 'mobx'; +import { AesirxBiApiService } from 'aesirx-lib'; +export class RealTimeStore { + getRealTime = async (dataFilter, dateFilter, callbackOnSuccess, callbackOnError) => { + try { + const biService = new AesirxBiApiService(); + const responseDataFromLibrary = await biService.getLiveVisitorsList(dataFilter, dateFilter); + if (responseDataFromLibrary && responseDataFromLibrary?.name !== 'AxiosError') { + runInAction(() => { + callbackOnSuccess(responseDataFromLibrary); + }); + } else { + callbackOnError({ + message: + responseDataFromLibrary?.response?.data?.error || + 'Something went wrong from Server response', + }); + } + } catch (error) { + console.log('errorrrr', error); + runInAction(() => { + if (error.response?.data.message) { + callbackOnError({ + message: error.response?.data?.message, + }); + } else { + callbackOnError({ + message: error?.response?.data?._messages + ? error.response?.data?._messages[0]?.message + : 'Something went wrong from Server response', + }); + } + }); + } + }; + + getLiveVisitorsTotal = async (dataFilter, callbackOnSuccess, callbackOnError) => { + try { + const biService = new AesirxBiApiService(); + const responseDataFromLibrary = await biService.getLiveVisitorsTotal(dataFilter); + if (responseDataFromLibrary) { + runInAction(() => { + callbackOnSuccess(responseDataFromLibrary); + }); + } else { + callbackOnError({ + message: 'Something went wrong from Server response', + }); + } + } catch (error) { + console.log('errorrrr', error); + runInAction(() => { + if (error.response?.data.message) { + callbackOnError({ + message: error.response?.data?.message, + }); + } else { + callbackOnError({ + message: error?.response?.data?._messages + ? error.response?.data?._messages[0]?.message + : 'Something went wrong from Server response', + }); + } + }); + } + }; + getLiveVisitorsDevice = async (dataFilter, callbackOnSuccess, callbackOnError) => { + try { + const biService = new AesirxBiApiService(); + const responseDataFromLibrary = await biService.getLiveVisitorsDevice(dataFilter); + if (responseDataFromLibrary) { + runInAction(() => { + callbackOnSuccess(responseDataFromLibrary); + }); + } else { + callbackOnError({ + message: 'Something went wrong from Server response', + }); + } + } catch (error) { + console.log('errorrrr', error); + runInAction(() => { + if (error.response?.data.message) { + callbackOnError({ + message: error.response?.data?.message, + }); + } else { + callbackOnError({ + message: error?.response?.data?._messages + ? error.response?.data?._messages[0]?.message + : 'Something went wrong from Server response', + }); + } + }); + } + }; + updateConsentsTemplate = async (updateFieldData) => { + try { + let resultOnSave; + const updateOrganizationApiService = new AesirxBiApiService(); + resultOnSave = await updateOrganizationApiService.updateConsentsTemplate(updateFieldData); + return { error: false, response: resultOnSave }; + } catch (error) { + return { error: true, response: error?.response?.data }; + } + }; +} + +export default RealTimeStore; diff --git a/packages/aesirx-bi-app/src/containers/RealTimePage/RealTimeViewModels/RealTimeListViewModel.js b/packages/aesirx-bi-app/src/containers/RealTimePage/RealTimeViewModels/RealTimeListViewModel.js new file mode 100644 index 00000000..5331230d --- /dev/null +++ b/packages/aesirx-bi-app/src/containers/RealTimePage/RealTimeViewModels/RealTimeListViewModel.js @@ -0,0 +1,211 @@ +/* + * @copyright Copyright (C) 2022 AesirX. All rights reserved. + * @license GNU General Public License version 3, see LICENSE. + */ + +import { history, notify } from 'aesirx-uikit'; +import PAGE_STATUS from '../../../constants/PageStatus'; +import { makeAutoObservable, runInAction } from 'mobx'; +import moment from 'moment'; +import RealTimeModel from '../RealTimeModel/RealTimeModel'; +import queryString from 'query-string'; +import { BI_LIVE_VISITORS_TOTAL_FIELD_KEY } from 'aesirx-lib'; + +class RealTimeListViewModel { + realTimeStore = null; + status = PAGE_STATUS.READY; + statusTable = PAGE_STATUS.READY; + statusChart = PAGE_STATUS.READY; + statusLiveVisitorsTotal = PAGE_STATUS.READY; + statusLiveVisitorsDevice = PAGE_STATUS.READY; + formSelectTimeStatus = PAGE_STATUS.READY; + globalStoreViewModel = null; + realtimeTableData = null; + sortBy = { 'sort[]': '', 'sort_direction[]': '' }; + dataFilterRealTime = {}; + liveVisitorsTotalData = null; + liveVisitorsDeviceData = null; + constructor(realTimeStore, globalStoreViewModel) { + makeAutoObservable(this); + this.realTimeStore = realTimeStore; + this.globalStoreViewModel = globalStoreViewModel; + } + + initialize = (dataFilter, dateFilter, dataFilterTable, isReload = false) => { + if (!dateFilter) { + const dataFilterObjects = [this.dataFilter, this.dataFilterEvents, this.dataFilterConversion]; + dataFilterObjects?.forEach((dataFilterObj) => { + for (const key in dataFilterObj) { + if (key.startsWith('filter[domain]')) { + delete dataFilterObj[key]; + } + } + }); + } + this.getRealTime({ ...dataFilter, ...dataFilterTable }, dateFilter, {}, isReload); + }; + + getRealTime = ( + dataFilter, + dateFilter, + sortBy = { 'sort[]': 'start', 'sort_direction[]': 'desc' }, + isReload = false + ) => { + this.statusTable = !isReload ? PAGE_STATUS.LOADING : PAGE_STATUS.READY; + this.sortBy = sortBy; + this.dataFilter = { + page_size: '20', + ...this.dataFilter, + ...dataFilter, + ...this.sortBy, + }; + const dateRangeFilter = { ...this.globalStoreViewModel.dateFilter, ...dateFilter }; + this.globalStoreViewModel.dataFilter = { + ...(sortBy['sort[]'] && { 'sort[]': sortBy['sort[]'] }), + ...(sortBy['sort_direction[]'] && { 'sort_direction[]': sortBy['sort_direction[]'] }), + }; + this.realTimeStore.getRealTime( + this.dataFilter, + dateRangeFilter, + this.callbackOnRealTimeSuccessHandler, + this.callbackOnErrorHandler + ); + }; + + getLiveVisitorsTotal = async (dataFilter, isReload = false) => { + console.log('isReload', isReload); + this.statusLiveVisitorsTotal = !isReload ? PAGE_STATUS.LOADING : PAGE_STATUS.READY; + this.dataFilterLiveVisitors = { + ...dataFilter, + }; + await this.realTimeStore.getLiveVisitorsTotal( + this.dataFilterLiveVisitors, + this.callbackOnLiveVisitorsTotalSuccessHandler, + this.callbackOnErrorHandler + ); + }; + getLiveVisitorsDevice = async (dataFilter, isReload = false) => { + this.statusLiveVisitorsDevice = !isReload ? PAGE_STATUS.LOADING : PAGE_STATUS.READY; + this.dataFilterLiveVisitors = { + page_size: '8', + ...dataFilter, + }; + await this.realTimeStore.getLiveVisitorsDevice( + this.dataFilterLiveVisitors, + this.callbackOnLiveVisitorsDeviceSuccessHandler, + this.callbackOnErrorHandler + ); + }; + + handleFilter = (dataFilter) => { + this.status = PAGE_STATUS.LOADING; + this.dataFilter = { ...this.dataFilter, ...dataFilter }; + const dateRangeFilter = { ...this.globalStoreViewModel.dateFilter }; + this.realTimeStore.getRealTime( + this.dataFilter, + dateRangeFilter, + this.callbackOnRealTimeSuccessHandler, + this.callbackOnErrorHandler + ); + }; + + handleFilterDateRange = (startDate, endDate) => { + this.status = PAGE_STATUS.LOADING; + let dateRangeFilter = { + date_start: moment(startDate).format('YYYY-MM-DD'), + date_end: moment(endDate).endOf('day').format('YYYY-MM-DD'), + }; + this.initialize(this.dataFilter, dateRangeFilter); + }; + + handleFilterRealTime = async (dataFilter, intergration) => { + const location = history.location; + this.statusTable = PAGE_STATUS.LOADING; + + this.dataFilterRealTime = { ...this.dataFilter, ...dataFilter }; + this.globalStoreViewModel.dataFilter = { pagination: this.dataFilterRealTime?.page }; + + const dateRangeFilter = { ...this.globalStoreViewModel.dateFilter }; + await this.realTimeStore.getRealTime( + this.dataFilterRealTime, + dateRangeFilter, + this.callbackOnRealTimeSuccessHandler, + this.callbackOnErrorHandler + ); + if (dataFilter?.page) { + const search = { + ...queryString.parse(location.search), + ...{ pagination: dataFilter?.page }, + }; + !intergration && + window.history.replaceState('', '', `/visitors/realtime?${queryString.stringify(search)}`); + } + }; + + callbackOnErrorHandler = (error) => { + this.status = PAGE_STATUS.READY; + this.statusTable = PAGE_STATUS.READY; + notify(error.message, 'error'); + }; + + callbackOnRealTimeSuccessHandler = (data) => { + if (data) { + if (data?.message !== 'canceled' && data?.message !== 'isCancle') { + this.statusTable = PAGE_STATUS.READY; + const transformData = new RealTimeModel(data.list, this.globalStoreViewModel); + this.realtimeTableData = { + list: transformData, + pagination: data.pagination, + }; + } + } else { + this.statusTable = PAGE_STATUS.ERROR; + this.data = []; + } + }; + + callbackOnLiveVisitorsTotalSuccessHandler = (data) => { + if (data) { + if (data?.message !== 'canceled' && data?.message !== 'isCancle') { + this.liveVisitorsTotalData = data?.list[BI_LIVE_VISITORS_TOTAL_FIELD_KEY.TOTAL]; + this.statusLiveVisitorsTotal = PAGE_STATUS.READY; + } + } else { + this.statusLiveVisitorsTotal = PAGE_STATUS.ERROR; + this.data = []; + } + }; + + callbackOnLiveVisitorsDeviceSuccessHandler = (data) => { + if (data) { + if (data?.message !== 'canceled' && data?.message !== 'isCancle') { + this.liveVisitorsDeviceData = data?.list; + this.statusLiveVisitorsDevice = PAGE_STATUS.READY; + } + } else { + this.statusLiveVisitorsDevice = PAGE_STATUS.ERROR; + this.data = []; + } + }; + + updateConsentsTemplate = async (formData) => { + this.formSelectTimeStatus = PAGE_STATUS.LOADING; + const data = await this.realTimeStore.updateConsentsTemplate(formData); + runInAction(async () => { + if (!data?.error) { + this.onSuccessConsentTemplateHandler(data?.response, 'Updated successfully'); + } else { + this.callbackOnErrorHandler(data?.response); + } + }); + return data; + }; + onSuccessConsentTemplateHandler = (result, message) => { + if (result && message) { + notify(message, 'success'); + this.formSelectTimeStatus = PAGE_STATUS.READY; + } + }; +} + +export default RealTimeListViewModel; diff --git a/packages/aesirx-bi-app/src/containers/RealTimePage/RealTimeViewModels/RealTimeViewModel.js b/packages/aesirx-bi-app/src/containers/RealTimePage/RealTimeViewModels/RealTimeViewModel.js new file mode 100644 index 00000000..0adc41c6 --- /dev/null +++ b/packages/aesirx-bi-app/src/containers/RealTimePage/RealTimeViewModels/RealTimeViewModel.js @@ -0,0 +1,19 @@ +/* + * @copyright Copyright (C) 2022 AesirX. All rights reserved. + * @license GNU General Public License version 3, see LICENSE. + */ + +import RealTimeListViewModel from './RealTimeListViewModel'; + +class RealTimeViewModel { + realTimeListViewModel = null; + + constructor(realTimeStore, globalStore) { + if (realTimeStore) { + this.realTimeListViewModel = new RealTimeListViewModel(realTimeStore, globalStore); + } + } + getRealTimeListViewModel = () => this.realTimeListViewModel; +} + +export default RealTimeViewModel; diff --git a/packages/aesirx-bi-app/src/containers/RealTimePage/RealTimeViewModels/RealTimeViewModelContextProvider.js b/packages/aesirx-bi-app/src/containers/RealTimePage/RealTimeViewModels/RealTimeViewModelContextProvider.js new file mode 100644 index 00000000..aaa3acf0 --- /dev/null +++ b/packages/aesirx-bi-app/src/containers/RealTimePage/RealTimeViewModels/RealTimeViewModelContextProvider.js @@ -0,0 +1,23 @@ +/* + * @copyright Copyright (C) 2022 AesirX. All rights reserved. + * @license GNU General Public License version 3, see LICENSE. + */ + +import React from 'react'; +export const RealTimeViewModelContext = React.createContext(); + +export const RealTimeViewModelContextProvider = ({ children, viewModel }) => { + return ( + + {children} + + ); +}; + +/* Hook to use store in any functional component */ +export const useRealTimeViewModel = () => React.useContext(RealTimeViewModelContext); + +/* HOC to inject store to any functional or class component */ +export const withRealTimeViewModel = (Component) => (props) => { + return ; +}; diff --git a/packages/aesirx-bi-app/src/containers/RealTimePage/index.jsx b/packages/aesirx-bi-app/src/containers/RealTimePage/index.jsx new file mode 100644 index 00000000..bf5afc3f --- /dev/null +++ b/packages/aesirx-bi-app/src/containers/RealTimePage/index.jsx @@ -0,0 +1,90 @@ +import React, { Component, lazy } from 'react'; +import { withTranslation } from 'react-i18next'; +import { withBiViewModel } from '../../store/BiStore/BiViewModelContextProvider'; +import { observer } from 'mobx-react'; +import { Route, withRouter } from 'react-router-dom'; +import RealTimeStore from './RealTimeStore/RealTimeStore'; +import RealTimeViewModel from './RealTimeViewModels/RealTimeViewModel'; +import { RealTimeViewModelContextProvider } from './RealTimeViewModels/RealTimeViewModelContextProvider'; +import { history } from 'aesirx-uikit'; +import ExportButton from 'components/ExportButton'; + +const RealTime = lazy(() => import('./RealTime')); + +const RenderComponent = ({ link, ...props }) => { + switch (link) { + case 'visitors-realtime': + return ; + } +}; + +const RealTimeContainer = observer( + class RealTimeContainer extends Component { + realTimeStore = null; + realTimeViewModel = null; + constructor(props) { + super(props); + const { viewModel } = props; + this.viewModel = viewModel ? viewModel : null; + this.biListViewModel = this.viewModel ? this.viewModel.getBiListViewModel() : null; + + this.realTimeStore = new RealTimeStore(); + this.realTimeViewModel = new RealTimeViewModel(this.realTimeStore, this.biListViewModel); + } + + componentDidMount = () => { + if (!this.props.integration && history.location.pathname === '/') { + history.push(`${this.biListViewModel.activeDomain[0]}`); + } + }; + render() { + const { integration = false } = this.props; + const { integrationLink, activeDomain } = this.biListViewModel; + return ( + + + (this.componentRef = el)} + integration={integration} + integrationLink={integrationLink} + activeDomain={activeDomain} + /> + + ); + } + } +); +const ComponentToPrint = observer( + class extends Component { + constructor(props) { + super(props); + } + + render() { + return ( +
+ {this.props.integration ? ( + + ) : ( + <> + + + + + )} +
+ ); + } + } +); +export default withTranslation()(withRouter(withBiViewModel(RealTimeContainer))); diff --git a/packages/aesirx-bi-app/src/containers/VisitorsPage/VisitorsViewModels/VisitorsListViewModel.js b/packages/aesirx-bi-app/src/containers/VisitorsPage/VisitorsViewModels/VisitorsListViewModel.js index a5585a1e..a029adea 100644 --- a/packages/aesirx-bi-app/src/containers/VisitorsPage/VisitorsViewModels/VisitorsListViewModel.js +++ b/packages/aesirx-bi-app/src/containers/VisitorsPage/VisitorsViewModels/VisitorsListViewModel.js @@ -131,6 +131,7 @@ class VisitorsListViewModel { { ...this.dataFilter, page_size: '1000', + 'filter_not[visibility_change]': 'true', }, dateRangeFilter, this.callbackOnVisitorSuccessHandler, @@ -145,6 +146,7 @@ class VisitorsListViewModel { this.visitorsStore.getVisits( { ...this.dataFilter, + 'filter_not[visibility_change]': 'true', page_size: '1000', }, dateRangeFilter, diff --git a/packages/aesirx-bi-app/src/routes/menu.js b/packages/aesirx-bi-app/src/routes/menu.js index 3be8aa08..26b239a3 100644 --- a/packages/aesirx-bi-app/src/routes/menu.js +++ b/packages/aesirx-bi-app/src/routes/menu.js @@ -120,6 +120,12 @@ const mainMenu = [ link: `/visitors/platforms`, page: 'visitors-platforms', }, + { + text: 'Real-Time', + mini_text: 'Real-Time', + link: `/visitors/realtime`, + page: 'visitors-realtime', + }, ], }, { diff --git a/packages/aesirx-bi-app/src/routes/routes.js b/packages/aesirx-bi-app/src/routes/routes.js index 72d49028..bc0c38eb 100644 --- a/packages/aesirx-bi-app/src/routes/routes.js +++ b/packages/aesirx-bi-app/src/routes/routes.js @@ -34,6 +34,7 @@ const UtmLinkPage = lazy(() => import('../containers/UTMLinkPage')); const EditUtmLinkProvider = lazy(() => import('../containers/UTMLinkPage/edit')); const TagEventPage = lazy(() => import('../containers/TagEventPage')); const EditTagEventProvider = lazy(() => import('../containers/TagEventPage/edit')); +const RealTimePage = lazy(() => import('../containers/RealTimePage')); const authRoutes = [ { path: '/login', @@ -134,6 +135,12 @@ const mainRoutes = [ exact: true, main: () => , }, + { + path: '/visitors/realtime', + page: 'visitors-realtime', + exact: true, + main: () => , + }, { path: '/flow-list', page: 'flow-list', diff --git a/packages/aesirx-bi-app/src/store/BiStore/BiListViewModel.js b/packages/aesirx-bi-app/src/store/BiStore/BiListViewModel.js index 554ab91d..72bf5643 100644 --- a/packages/aesirx-bi-app/src/store/BiStore/BiListViewModel.js +++ b/packages/aesirx-bi-app/src/store/BiStore/BiListViewModel.js @@ -111,10 +111,6 @@ class BiListViewModel { ...queryString.parse(location.search), ...{ page: 'aesirx-bi-' + link }, }; - console.log( - 'unescape(queryString.stringify(search))', - unescape(queryString.stringify(search)) - ); history.push({ ...location, ...{ search: unescape(queryString.stringify(search)) }, @@ -147,7 +143,8 @@ class BiListViewModel { !location?.pathname?.startsWith('/user-handling/edit') && location?.pathname !== '/utm-links/link' && !location?.pathname?.startsWith('/utm-links/edit') && - location?.pathname !== '/utm-links/add' + location?.pathname !== '/utm-links/add' && + location?.pathname !== '/visitors/realtime' ) { history.push({ ...location, diff --git a/packages/aesirx-bi-app/src/utils/index.js b/packages/aesirx-bi-app/src/utils/index.js index c97a01da..c8b18914 100644 --- a/packages/aesirx-bi-app/src/utils/index.js +++ b/packages/aesirx-bi-app/src/utils/index.js @@ -22,4 +22,26 @@ const downloadExcel = async (data, nameFile) => { link.download = `${nameFile}.xlsx`; link.click(); }; -export { downloadExcel }; +const timeAgo = (isoString) => { + const now = Date.now(); + const past = new Date(isoString).getTime(); + const diff = Math.floor((now - past) / 1000); // seconds + + if (diff < 60) { + return `${diff}s ago`; + } + + const minutes = Math.floor(diff / 60); + if (minutes < 60) { + return `${minutes} minute${minutes > 1 ? 's' : ''} ago`; + } + + const hours = Math.floor(minutes / 60); + if (hours < 24) { + return `${hours} hour${hours > 1 ? 's' : ''} ago`; + } + + const days = Math.floor(hours / 24); + return `${days} day${days > 1 ? 's' : ''} ago`; +}; +export { downloadExcel, timeAgo }; diff --git a/packages/aesirx-lib b/packages/aesirx-lib index 028a98a0..138750ef 160000 --- a/packages/aesirx-lib +++ b/packages/aesirx-lib @@ -1 +1 @@ -Subproject commit 028a98a096769f6ec7bedaa53b6f2d670b186d93 +Subproject commit 138750efd158bc79f08d48c433416759aaec03e7