diff --git a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/CallsErrors4xx/CallsErrors4xx.tsx b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/CallsErrors4xx/CallsErrors4xx.tsx index f1e192587..e0f721078 100644 --- a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/CallsErrors4xx/CallsErrors4xx.tsx +++ b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/CallsErrors4xx/CallsErrors4xx.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { NoContent } from 'UI'; import { Styles } from '../../common'; import { - BarChart, Bar, CartesianGrid, Tooltip, + CartesianGrid, Tooltip, LineChart, Line, Legend, ResponsiveContainer, XAxis, YAxis } from 'recharts'; @@ -12,15 +12,14 @@ interface Props { metric?: any } function CallsErrors4xx(props: Props) { - const { data, metric } = props; - console.log('asd', metric.data.namesMap) + const { data, metric } = props; return ( - @@ -40,7 +39,7 @@ function CallsErrors4xx(props: Props) { { Array.isArray(metric.data.namesMap) && metric.data.namesMap.map((key, index) => ( ))} - + ); diff --git a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/CallsErrors5xx/CallsErrors5xx.tsx b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/CallsErrors5xx/CallsErrors5xx.tsx index 3dec42162..cc62baf45 100644 --- a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/CallsErrors5xx/CallsErrors5xx.tsx +++ b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/CallsErrors5xx/CallsErrors5xx.tsx @@ -19,7 +19,7 @@ function CallsErrors5xx(props: Props) { show={ metric.data.chart.length === 0 } > - @@ -39,7 +39,7 @@ function CallsErrors5xx(props: Props) { { Array.isArray(metric.data.namesMap) && metric.data.namesMap.map((key, index) => ( ))} - + ); diff --git a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/ErrorsPerDomain/ErrorsPerDomain.tsx b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/ErrorsPerDomain/ErrorsPerDomain.tsx index aaae19efa..fab8ced65 100644 --- a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/ErrorsPerDomain/ErrorsPerDomain.tsx +++ b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/ErrorsPerDomain/ErrorsPerDomain.tsx @@ -16,6 +16,7 @@ function ErrorsPerDomain(props: Props) {
{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 - @@ -201,13 +208,13 @@ function WidgetForm(props: Props) { )}
- - - 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