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