feat(ui) - dashboard - widget drilldown
This commit is contained in:
parent
471d6068c1
commit
f9df0d2b91
12 changed files with 274 additions and 44 deletions
|
|
@ -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 (
|
||||
<NoContent
|
||||
size="small"
|
||||
show={ metric.data.chart.length === 0 }
|
||||
>
|
||||
<ResponsiveContainer height={ 240 } width="100%">
|
||||
<BarChart
|
||||
<LineChart
|
||||
data={metric.data.chart}
|
||||
margin={Styles.chartMargins}
|
||||
>
|
||||
|
|
@ -40,7 +39,7 @@ function CallsErrors4xx(props: Props) {
|
|||
{ Array.isArray(metric.data.namesMap) && metric.data.namesMap.map((key, index) => (
|
||||
<Line key={key} name={key} type="monotone" dataKey={key} stroke={Styles.colors[index]} fillOpacity={ 1 } strokeWidth={ 2 } strokeOpacity={ 0.8 } fill="url(#colorCount)" dot={false} />
|
||||
))}
|
||||
</BarChart>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</NoContent>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ function CallsErrors5xx(props: Props) {
|
|||
show={ metric.data.chart.length === 0 }
|
||||
>
|
||||
<ResponsiveContainer height={ 240 } width="100%">
|
||||
<BarChart
|
||||
<LineChart
|
||||
data={metric.data.chart}
|
||||
margin={Styles.chartMargins}
|
||||
>
|
||||
|
|
@ -39,7 +39,7 @@ function CallsErrors5xx(props: Props) {
|
|||
{ Array.isArray(metric.data.namesMap) && metric.data.namesMap.map((key, index) => (
|
||||
<Line key={key} name={key} type="monotone" dataKey={key} stroke={Styles.colors[index]} fillOpacity={ 1 } strokeWidth={ 2 } strokeOpacity={ 0.8 } fill="url(#colorCount)" dot={false} />
|
||||
))}
|
||||
</BarChart>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</NoContent>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ function ErrorsPerDomain(props: Props) {
|
|||
<NoContent
|
||||
size="small"
|
||||
show={ metric.data.chart.length === 0 }
|
||||
style={{ height: '240px'}}
|
||||
>
|
||||
<div className="w-full" style={{ height: '240px' }}>
|
||||
{metric.data.chart.map((item, i) =>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
|||
<Icon name="trash" size="14" className="mr-2" color="teal"/>
|
||||
Delete
|
||||
</Button>
|
||||
<Button plain size="small" className="flex items-center ml-2" onClick={() => setShowDashboardSelectionModal(true)}>
|
||||
<Button
|
||||
plain size="small"
|
||||
className="flex items-center ml-2"
|
||||
onClick={() => setShowDashboardSelectionModal(true)}
|
||||
disabled={!canAddToDashboard}
|
||||
>
|
||||
<Icon name="columns-gap" size="14" className="mr-2" color="teal"/>
|
||||
Add to Dashboard
|
||||
</Button>
|
||||
|
|
@ -201,13 +208,13 @@ function WidgetForm(props: Props) {
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<DashboardSelectionModal
|
||||
metricId={metric.metricId}
|
||||
show={showDashboardSelectionModal}
|
||||
closeHandler={() => setShowDashboardSelectionModal(false)}
|
||||
/>
|
||||
{ canAddToDashboard && (
|
||||
<DashboardSelectionModal
|
||||
metricId={metric.metricId}
|
||||
show={showDashboardSelectionModal}
|
||||
closeHandler={() => setShowDashboardSelectionModal(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<any>([]);
|
||||
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(() => (
|
||||
<div className={cn(className)}>
|
||||
<div className="flex items-baseline">
|
||||
<h2 className="text-2xl">Sessions</h2>
|
||||
<div className="ml-2 color-gray-medium">between <span className="font-medium color-gray-darkest">{startTime}</span> and <span className="font-medium color-gray-darkest">{endTime}</span> </div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-baseline">
|
||||
<h2 className="text-2xl">Sessions</h2>
|
||||
<div className="ml-2 color-gray-medium">between <span className="font-medium color-gray-darkest">{startTime}</span> and <span className="font-medium color-gray-darkest">{endTime}</span> </div>
|
||||
</div>
|
||||
|
||||
{ widget.metricType !== 'table' && (
|
||||
<div className="flex items-center ml-6">
|
||||
<span className="mr-2 color-gray-medium">Series</span>
|
||||
<Dropdown
|
||||
// className={stl.dropdown}
|
||||
className="font-medium flex items-center hover:bg-gray-light rounded px-2 py-1"
|
||||
direction="left"
|
||||
options={ seriesOptions }
|
||||
name="change"
|
||||
value={ activeSeries }
|
||||
onChange={ writeOption }
|
||||
id="change-dropdown"
|
||||
// icon={null}
|
||||
icon={ <Icon name="chevron-down" color="gray-dark" size="14" className="ml-2" /> }
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-3">
|
||||
<NoContent
|
||||
title="No recordings found"
|
||||
show={widget.sessions.length === 0}
|
||||
animatedIcon="no-results"
|
||||
>
|
||||
{widget.sessions.map((session: any) => (
|
||||
<SessionItem key={ session.sessionId } session={ session } />
|
||||
))}
|
||||
</NoContent>
|
||||
<Loader loading={widget.sessionsLoading}>
|
||||
<NoContent
|
||||
title="No recordings found"
|
||||
show={filteredSessions.length === 0}
|
||||
animatedIcon="no-results"
|
||||
>
|
||||
{filteredSessions.map((session: any) => (
|
||||
<SessionItem key={ session.sessionId } session={ session } />
|
||||
))}
|
||||
</NoContent>
|
||||
</Loader>
|
||||
</div>
|
||||
</div>
|
||||
));
|
||||
}
|
||||
|
||||
export default WidgetSessions;
|
||||
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);
|
||||
|
|
@ -219,7 +219,7 @@ export default class List extends React.PureComponent {
|
|||
<NoContent
|
||||
title="No Errors Found!"
|
||||
subtext="Please try to change your search parameters."
|
||||
icon="exclamation-circle"
|
||||
animatedIcon="empty-state"
|
||||
show={ !loading && list.size === 0}
|
||||
>
|
||||
<Loader loading={ loading }>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
79
frontend/app/mstore/types/session.ts
Normal file
79
frontend/app/mstore/types/session.ts
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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<any>
|
||||
}
|
||||
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<any> {
|
||||
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
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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<any>;
|
||||
getMetricChartData(metric: IWidget, data: any, isWidget: boolean): Promise<any>;
|
||||
fetchSessions(metricId: string, filter: any): Promise<any>
|
||||
}
|
||||
|
||||
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<any> {
|
||||
return this.client.post(`/metrics/${metricId}/sessions`, filter)
|
||||
.then(response => response.json())
|
||||
.then(response => response.data || []);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue