{metric.data.chart.map((item, i) =>
diff --git a/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx b/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx
index 24cbe0cc4..c3f8d10c2 100644
--- a/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx
+++ b/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx
@@ -19,6 +19,7 @@ function WidgetChart(props: Props) {
const { isWidget = false, metric } = props;
const { dashboardStore } = useStore();
const period = useObserver(() => dashboardStore.period);
+ const drillDownFilter = useObserver(() => dashboardStore.drillDownFilter);
const colors = Styles.customMetricColors;
const [loading, setLoading] = useState(false)
const isOverviewWidget = metric.metricType === 'predefined' && metric.viewType === 'overview';
@@ -34,6 +35,11 @@ function WidgetChart(props: Props) {
const periodTimestamps = metric.metricType === 'timeseries' ?
getStartAndEndTimestampsByDensity(timestamp, period.start, period.end, params.density) :
period.toTimestamps();
+
+ drillDownFilter.merge({
+ startTimestamp: periodTimestamps.startTimestamp,
+ endTimestamp: periodTimestamps.endTimestamp,
+ });
// const activeWidget = {
// widget: metric,
@@ -42,8 +48,6 @@ function WidgetChart(props: Props) {
// timestamp: payload.timestamp,
// index,
// }
-
- // props.setActiveWidget(activeWidget);
}
}
diff --git a/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx b/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx
index 6813a07aa..81d2ce430 100644
--- a/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx
+++ b/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx
@@ -20,7 +20,8 @@ interface Props {
function WidgetForm(props: Props) {
const [showDashboardSelectionModal, setShowDashboardSelectionModal] = useState(false);
const { history, match: { params: { siteId, dashboardId, metricId } } } = props;
- const { metricStore } = useStore();
+ const { metricStore, dashboardStore } = useStore();
+ const dashboards = dashboardStore.dashboards;
const isSaving = useObserver(() => metricStore.isSaving);
const metric: any = useObserver(() => metricStore.instance);
@@ -29,6 +30,7 @@ function WidgetForm(props: Props) {
const isTable = metric.metricType === 'table';
const isTimeSeries = metric.metricType === 'timeseries';
const _issueOptions = [{ text: 'All', value: 'all' }].concat(issueOptions);
+ const canAddToDashboard = metric.exists() && dashboards.length > 0;
const write = ({ target: { value, name } }) => metricStore.merge({ [ name ]: value });
const writeOption = (e, { value, name }) => {
@@ -193,7 +195,12 @@ function WidgetForm(props: Props) {
Delete
-
-
-
- setShowDashboardSelectionModal(false)}
- />
+ { canAddToDashboard && (
+ setShowDashboardSelectionModal(false)}
+ />
+ )}
));
}
diff --git a/frontend/app/components/Dashboard/components/WidgetSessions/WidgetSessions.tsx b/frontend/app/components/Dashboard/components/WidgetSessions/WidgetSessions.tsx
index ccfb0fbe7..e6c9cb808 100644
--- a/frontend/app/components/Dashboard/components/WidgetSessions/WidgetSessions.tsx
+++ b/frontend/app/components/Dashboard/components/WidgetSessions/WidgetSessions.tsx
@@ -1,43 +1,106 @@
-import React from 'react';
-import { NoContent } from 'UI';
+import React, { useEffect, useState } from 'react';
+import { NoContent, Dropdown, Icon, Loader } from 'UI';
import cn from 'classnames';
import { useStore } from 'App/mstore';
import SessionItem from 'Shared/SessionItem';
-import { useObserver } from 'mobx-react-lite';
+import { observer, useObserver } from 'mobx-react-lite';
import { DateTime } from 'luxon';
interface Props {
className?: string;
}
function WidgetSessions(props: Props) {
const { className = '' } = props;
- const { dashboardStore } = useStore();
- const filter = useObserver(() => dashboardStore.drillDownFilter);
- const widget = dashboardStore.currentWidget;
+ const [data, setData] = useState([]);
+ const [seriesOptions, setSeriesOptions] = useState([
+ { text: 'All', value: 'all' },
+ ]);
- // const range = period.toTimestamps()
+ const [activeSeries, setActiveSeries] = useState('all');
+
+ const writeOption = (e, { name, value }) => setActiveSeries(value);
+ useEffect(() => {
+ if (!data) return;
+ const seriesOptions = data.map(item => ({
+ text: item.seriesName,
+ value: item.seriesId,
+ }));
+ setSeriesOptions([
+ { text: 'All', value: 'all' },
+ ...seriesOptions,
+ ]);
+ }, [data]);
+
+ const filteredSessions = getListSessionsBySeries(data, activeSeries);
+ const { dashboardStore, metricStore } = useStore();
+ const filter = useObserver(() => dashboardStore.drillDownFilter);
+ const widget: any = metricStore.instance;
const startTime = DateTime.fromMillis(filter.startTimestamp).toFormat('LLL dd, yyyy HH:mm a');
const endTime = DateTime.fromMillis(filter.endTimestamp).toFormat('LLL dd, yyyy HH:mm a');
+ useEffect(() => {
+ widget.fetchSessions({ ...filter, filter: widget.toJsonDrilldown()}).then(res => {
+ console.log('res', res)
+ setData(res);
+ });
+ }, [filter.startTimestamp, filter.endTimestamp, widget.filter]);
+
return useObserver(() => (
-
-
Sessions
-
between {startTime} and {endTime}
+
+
+
Sessions
+
between {startTime} and {endTime}
+
+
+ { widget.metricType !== 'table' && (
+
+ Series
+ }
+ />
+
+ )}
-
- {widget.sessions.map((session: any) => (
-
- ))}
-
+
+
+ {filteredSessions.map((session: any) => (
+
+ ))}
+
+
));
}
-export default WidgetSessions;
\ No newline at end of file
+const getListSessionsBySeries = (data, seriesId) => {
+ const arr: any = []
+ data.forEach(element => {
+ if (seriesId === 'all') {
+ const sessionIds = arr.map(i => i.sessionId);
+ arr.push(...element.sessions.filter(i => !sessionIds.includes(i.sessionId)));
+ } else {
+ if (element.seriesId === seriesId) {
+ arr.push(...element.sessions)
+ }
+ }
+ });
+ return arr;
+}
+
+export default observer(WidgetSessions);
\ No newline at end of file
diff --git a/frontend/app/components/Errors/List/List.js b/frontend/app/components/Errors/List/List.js
index 2fa91c5e5..82ecce40c 100644
--- a/frontend/app/components/Errors/List/List.js
+++ b/frontend/app/components/Errors/List/List.js
@@ -219,7 +219,7 @@ export default class List extends React.PureComponent {
diff --git a/frontend/app/mstore/dashboardStore.ts b/frontend/app/mstore/dashboardStore.ts
index 73a41b379..dc8b4b999 100644
--- a/frontend/app/mstore/dashboardStore.ts
+++ b/frontend/app/mstore/dashboardStore.ts
@@ -5,7 +5,7 @@ import { dashboardService, metricService } from "App/services";
import { toast } from 'react-toastify';
import Period, { LAST_24_HOURS, LAST_7_DAYS } from 'Types/app/period';
import { getChartFormatter } from 'Types/dashboard/helper';
-import Filter from "./types/filter";
+import Filter, { IFilter } from "./types/filter";
export interface IDashboardSotre {
dashboards: IDashboard[]
@@ -15,7 +15,7 @@ export interface IDashboardSotre {
startTimestamp: number
endTimestamp: number
period: Period
- drillDownFilter: Filter
+ drillDownFilter: IFilter
siteId: any
currentWidget: Widget
@@ -29,6 +29,7 @@ export interface IDashboardSotre {
isSaving: boolean
isDeleting: boolean
fetchingDashboard: boolean
+ sessionsLoading: boolean
toggleAllSelectedWidgets: (isSelected: boolean) => void
removeSelectedWidgetByCategory(category: string): void
@@ -89,9 +90,11 @@ export default class DashboardStore implements IDashboardSotre {
isSaving: boolean = false
isDeleting: boolean = false
fetchingDashboard: boolean = false
+ sessionsLoading: boolean = false;
constructor() {
makeAutoObservable(this, {
+ drillDownFilter: observable.ref,
widgetCategories: observable.ref,
resetCurrentWidget: action,
addDashboard: action,
diff --git a/frontend/app/mstore/types/filter.ts b/frontend/app/mstore/types/filter.ts
index edd11f5fe..3c4e0aa1a 100644
--- a/frontend/app/mstore/types/filter.ts
+++ b/frontend/app/mstore/types/filter.ts
@@ -2,8 +2,27 @@ import { makeAutoObservable, runInAction, observable, action, reaction } from "m
import { FilterKey, FilterType } from 'Types/filter/filterType'
import { filtersMap } from 'Types/filter/newFilter'
import FilterItem from "./filterItem"
-export default class Filter {
+
+export interface IFilter {
+ filterId: string
+ name: string
+ filters: FilterItem[]
+ eventsOrder: string
+ startTimestamp: number
+ endTimestamp: number
+
+ merge: (filter: any) => void
+ addFilter: (filter: FilterItem) => void
+ updateFilter: (index:number, filter: any) => void
+ updateKey: (key: any, value: any) => void
+ removeFilter: (index: number) => void
+ fromJson: (json: any) => void
+ toJson: () => any
+ toJsonDrilldown: () => any
+}
+export default class Filter implements IFilter {
public static get ID_KEY():string { return "filterId" }
+ filterId: string = ''
name: string = ''
filters: FilterItem[] = []
eventsOrder: string = 'then'
@@ -14,10 +33,19 @@ export default class Filter {
makeAutoObservable(this, {
filters: observable,
eventsOrder: observable,
+ startTimestamp: observable,
+ endTimestamp: observable,
addFilter: action,
removeFilter: action,
updateKey: action,
+ merge: action,
+ })
+ }
+
+ merge(filter: any) {
+ runInAction(() => {
+ Object.assign(this, filter)
})
}
@@ -36,7 +64,7 @@ export default class Filter {
this.filters[index] = new FilterItem(filter)
}
- updateKey(key, value) {
+ updateKey(key: string, value) {
this[key] = value
}
diff --git a/frontend/app/mstore/types/session.ts b/frontend/app/mstore/types/session.ts
new file mode 100644
index 000000000..337e7009b
--- /dev/null
+++ b/frontend/app/mstore/types/session.ts
@@ -0,0 +1,79 @@
+import { runInAction, makeAutoObservable, observable } from 'mobx'
+import { List, Map } from 'immutable';
+import { DateTime, Duration } from 'luxon';
+
+const HASH_MOD = 1610612741;
+const HASH_P = 53;
+function hashString(s: string): number {
+ let mul = 1;
+ let hash = 0;
+ for (let i = 0; i < s.length; i++) {
+ hash = (hash + s.charCodeAt(i) * mul) % HASH_MOD;
+ mul = (mul*HASH_P) % HASH_MOD;
+ }
+ return hash;
+}
+
+export interface ISession {
+ sessionId: string
+ viewed: boolean
+ duration: number
+ metadata: any,
+ startedAt: number
+ userBrowser: string
+ userOs: string
+ userId: string
+ userDeviceType: string
+ userCountry: string
+ eventsCount: number
+ userNumericHash: number
+ userDisplayName: string
+}
+
+export default class Session implements ISession {
+ sessionId: string = "";
+ viewed: boolean = false
+ duration: number = 0
+ metadata: any = Map()
+ startedAt: number = 0
+ userBrowser: string = ""
+ userOs: string = ""
+ userId: string = ""
+ userDeviceType: string = ""
+ userCountry: string = ""
+ eventsCount: number = 0
+ userNumericHash: number = 0
+ userDisplayName: string = ""
+
+ constructor() {
+ makeAutoObservable(this, {
+ sessionId: observable,
+ })
+ }
+
+ fromJson(session: any) {
+ runInAction(() => {
+ Object.keys(session).forEach(key => {
+ this[key] = session[key]
+ })
+
+ const { startTs, timestamp } = session;
+ const startedAt = +startTs || +timestamp;
+
+ this.sessionId = session.sessionId
+ this.viewed = session.viewed
+ this.duration = Duration.fromMillis(session.duration < 1000 ? 1000 : session.duration);
+ this.metadata = Map(session.metadata)
+ this.startedAt = startedAt
+ this.userBrowser = session.userBrowser
+ this.userOs = session.userOs
+ this.userId = session.userId
+ this.userDeviceType = session.userDeviceType
+ this.eventsCount = session.eventsCount
+ this.userCountry = session.userCountry
+ this.userNumericHash = hashString(session.userId || session.userAnonymousId || session.userUuid || session.userID || session.userUUID || "")
+ this.userDisplayName = session.userId || session.userAnonymousId || session.userID || 'Anonymous User'
+ })
+ return this
+ }
+}
\ No newline at end of file
diff --git a/frontend/app/mstore/types/widget.ts b/frontend/app/mstore/types/widget.ts
index 2c74ce26b..4d20ca0f1 100644
--- a/frontend/app/mstore/types/widget.ts
+++ b/frontend/app/mstore/types/widget.ts
@@ -1,7 +1,9 @@
import { makeAutoObservable, runInAction, observable, action, reaction, computed } from "mobx"
import FilterSeries from "./filterSeries";
import { DateTime } from 'luxon';
-
+import { IFilter } from "./filter";
+import { metricService } from "App/services";
+import Session, { ISession } from "App/mstore/types/session";
export interface IWidget {
metricId: any
widgetId: any
@@ -20,6 +22,8 @@ export interface IWidget {
dashboardIds: any[]
config: any
+ sessionsLoading: boolean
+
position: number
data: any
isLoading: boolean
@@ -34,12 +38,14 @@ export interface IWidget {
removeSeries(index: number): void
addSeries(): void
fromJson(json: any): void
+ toJsonDrilldown(json: any): void
toJson(): any
validate(): void
update(data: any): void
exists(): boolean
toWidget(): any
setData(data: any): void
+ fetchSessions(filter: any): Promise
}
export default class Widget implements IWidget {
public static get ID_KEY():string { return "metricId" }
@@ -61,6 +67,8 @@ export default class Widget implements IWidget {
config: any = {}
params: any = { density: 70 }
+ sessionsLoading: boolean = false
+
position: number = 0
data: any = {
chart: [],
@@ -74,7 +82,9 @@ export default class Widget implements IWidget {
constructor() {
makeAutoObservable(this, {
+ sessionsLoading: observable,
data: observable.ref,
+ metricId: observable,
widgetId: observable,
name: observable,
metricType: observable,
@@ -146,6 +156,12 @@ export default class Widget implements IWidget {
}
}
+ toJsonDrilldown() {
+ return {
+ series: this.series.map((series: any) => series.toJson()),
+ }
+ }
+
toJson() {
return {
metricId: this.metricId,
@@ -179,4 +195,21 @@ export default class Widget implements IWidget {
Object.assign(this.data, data)
})
}
+
+ fetchSessions(filter: any): Promise {
+ this.sessionsLoading = true
+ return new Promise((resolve, reject) => {
+ console.log('fetching sessions', filter)
+ metricService.fetchSessions(this.metricId, filter).then(response => {
+ resolve(response.map(cat => {
+ return {
+ ...cat,
+ sessions: cat.sessions.map(s => new Session().fromJson(s))
+ }
+ }))
+ }).finally(() => {
+ this.sessionsLoading = false
+ })
+ })
+ }
}
\ No newline at end of file
diff --git a/frontend/app/services/MetricService.ts b/frontend/app/services/MetricService.ts
index a62520d52..0007bc8d1 100644
--- a/frontend/app/services/MetricService.ts
+++ b/frontend/app/services/MetricService.ts
@@ -1,5 +1,6 @@
import Widget, { IWidget } from "App/mstore/types/widget";
import APIClient from 'App/api_client';
+import { IFilter } from "App/mstore/types/filter";
export interface IMetricService {
initClient(client?: APIClient): void;
@@ -11,6 +12,7 @@ export interface IMetricService {
getTemplates(): Promise;
getMetricChartData(metric: IWidget, data: any, isWidget: boolean): Promise;
+ fetchSessions(metricId: string, filter: any): Promise
}
export default class MetricService implements IMetricService {
@@ -88,4 +90,15 @@ export default class MetricService implements IMetricService {
.then(response => response.json())
.then(response => response.data || {});
}
+
+ /**
+ * Fetch sessions from the server.
+ * @param filter
+ * @returns
+ */
+ fetchSessions(metricId: string, filter: any): Promise {
+ return this.client.post(`/metrics/${metricId}/sessions`, filter)
+ .then(response => response.json())
+ .then(response => response.data || []);
+ }
}
\ No newline at end of file