-
Title
+
+ setSelectedMetrics(list.map((i: any) => i.metricId))}
+ />
+ Title
+
Owner
Visibility
Last Modified
@@ -48,7 +67,15 @@ function MetricsList({ siteId }: { siteId: string }) {
{sliceListPerPage(list, metricStore.page - 1, metricStore.pageSize).map((metric: any) => (
-
+ {
+ e.stopPropagation();
+ toggleMetricSelection(parseInt(metric.metricId));
+ }}
+ />
))}
diff --git a/frontend/app/components/Dashboard/components/MetricsView/MetricsView.tsx b/frontend/app/components/Dashboard/components/MetricsView/MetricsView.tsx
index 6c39114cd..929868550 100644
--- a/frontend/app/components/Dashboard/components/MetricsView/MetricsView.tsx
+++ b/frontend/app/components/Dashboard/components/MetricsView/MetricsView.tsx
@@ -1,40 +1,26 @@
import React from 'react';
-import { Button, PageTitle, Icon, Link } from 'UI';
import withPageTitle from 'HOCs/withPageTitle';
import MetricsList from '../MetricsList';
-import MetricsSearch from '../MetricsSearch';
import { useStore } from 'App/mstore';
import { useObserver } from 'mobx-react-lite';
+import MetricViewHeader from '../MetricViewHeader';
interface Props {
- siteId: string;
+ siteId: string;
}
function MetricsView({ siteId }: Props) {
- const { metricStore } = useStore();
+ const { metricStore } = useStore();
- React.useEffect(() => {
- metricStore.fetchList();
- }, []);
- return useObserver(() => (
-
-
-
-
- Create custom Metrics to capture user frustrations, monitor your app's performance and track other KPIs.
-
-
-
- ));
+ React.useEffect(() => {
+ metricStore.fetchList();
+ }, []);
+
+ return useObserver(() => (
+
+
+
+
+ ));
}
-export default withPageTitle('Metrics - OpenReplay')(MetricsView);
+export default withPageTitle('Cards - OpenReplay')(MetricsView);
diff --git a/frontend/app/components/Modal/Modal.tsx b/frontend/app/components/Modal/Modal.tsx
index cc1e79e71..52f3d3cfe 100644
--- a/frontend/app/components/Modal/Modal.tsx
+++ b/frontend/app/components/Modal/Modal.tsx
@@ -1,9 +1,18 @@
import React, { useEffect } from 'react';
import ReactDOM from 'react-dom';
import ModalOverlay from './ModalOverlay';
+import cn from 'classnames';
import { useHistory } from 'react-router';
-export default function Modal({ component, props, hideModal }: any) {
+const DEFAULT_WIDTH = 350;
+interface Props {
+ component: any;
+ className?: string;
+ props: any;
+ hideModal?: boolean;
+ width?: number;
+}
+function Modal({ component, className = 'bg-white', props, hideModal }: Props) {
const history = useHistory();
useEffect(() => {
@@ -12,11 +21,15 @@ export default function Modal({ component, props, hideModal }: any) {
document.querySelector('body').style.overflow = 'visible';
}
});
- });
- return component ? (
+ });return component ? (
ReactDOM.createPortal(
- {component}
+
+ {component}
+
,
document.querySelector('#modal-root')
)
@@ -24,3 +37,17 @@ export default function Modal({ component, props, hideModal }: any) {
<>>
);
}
+
+Modal.Header = ({ title }: { title: string }) => {
+ return (
+
+ );
+};
+
+Modal.Content = ({ children, className = 'p-4' }: { children: any; className?: string }) => {
+ return
{children}
;
+};
+
+export default Modal;
diff --git a/frontend/app/components/Modal/index.tsx b/frontend/app/components/Modal/index.tsx
index 860063c77..3df7a98cb 100644
--- a/frontend/app/components/Modal/index.tsx
+++ b/frontend/app/components/Modal/index.tsx
@@ -3,60 +3,59 @@ import React, { Component, createContext } from 'react';
import Modal from './Modal';
const ModalContext = createContext({
- component: null,
- props: {
- right: true,
- onClose: () => {},
- },
- showModal: (component: any, props: any) => {},
- hideModal: () => {},
+ component: null,
+ props: {
+ right: true,
+ onClose: () => {},
+ },
+ showModal: (component: any, props: any) => {},
+ hideModal: () => {},
});
export class ModalProvider extends Component {
- handleKeyDown = (e: any) => {
- if (e.keyCode === 27) {
- this.hideModal();
- }
- };
-
- showModal = (component, props = { right: true }) => {
- this.setState({
- component,
- props,
- });
- document.addEventListener('keydown', this.handleKeyDown);
- document.querySelector('body').style.overflow = 'hidden';
- };
-
- hideModal = () => {
- document.removeEventListener('keydown', this.handleKeyDown);
- document.querySelector('body').style.overflow = 'visible';
- const { props } = this.state;
- if (props.onClose) {
- props.onClose();
- }
- this.setState({
- component: null,
- props: {},
- });
- };
-
- state = {
- component: null,
- get isModalActive() { return this.component !== null },
- props: {},
- showModal: this.showModal,
- hideModal: this.hideModal,
- };
-
- render() {
- return (
-
-
- {this.props.children}
-
- );
+ handleKeyDown = (e: any) => {
+ if (e.keyCode === 27) {
+ this.hideModal();
}
+ };
+
+ showModal = (component, props = { right: true }) => {
+ this.setState({
+ component,
+ props,
+ });
+ document.addEventListener('keydown', this.handleKeyDown);
+ document.querySelector('body').style.overflow = 'hidden';
+ };
+
+ hideModal = () => {
+ document.removeEventListener('keydown', this.handleKeyDown);
+ document.querySelector('body').style.overflow = 'visible';
+ const { props } = this.state;
+ if (props.onClose) {
+ props.onClose();
+ }
+ this.setState({
+ component: null,
+ props: {},
+ });
+ };
+
+ state = {
+ component: null,
+ get isModalActive() { return this.component !== null },props: {},
+ showModal: this.showModal,
+ hideModal: this.hideModal,
+ };
+
+ render() {
+ return (
+
+
+ {this.props.children}
+
+ );
+ }
}
export const ModalConsumer = ModalContext.Consumer;
diff --git a/frontend/app/components/Modal/withModal.tsx b/frontend/app/components/Modal/withModal.tsx
index ec5030e54..9c7319eed 100644
--- a/frontend/app/components/Modal/withModal.tsx
+++ b/frontend/app/components/Modal/withModal.tsx
@@ -1,9 +1,7 @@
import React from 'react';
import { ModalConsumer } from './';
-
-export default BaseComponent => React.memo(props => (
-
- { value => }
-
-));
\ No newline at end of file
+export default (BaseComponent) =>
+ React.memo((props) => (
+
{(value) => }
+ ));
diff --git a/frontend/app/mstore/dashboardStore.ts b/frontend/app/mstore/dashboardStore.ts
index c474ba326..bf3a970d8 100644
--- a/frontend/app/mstore/dashboardStore.ts
+++ b/frontend/app/mstore/dashboardStore.ts
@@ -119,7 +119,7 @@ export default class DashboardStore {
return this.dashboards.filter((d) => ids.includes(d.dashboardId));
}
- initDashboard(dashboard: Dashboard) {
+ initDashboard(dashboard?: Dashboard) {
this.dashboardInstance = dashboard
? new Dashboard().fromJson(dashboard)
: new Dashboard();
diff --git a/frontend/app/mstore/metricStore.ts b/frontend/app/mstore/metricStore.ts
index 11bcf8894..04a706d32 100644
--- a/frontend/app/mstore/metricStore.ts
+++ b/frontend/app/mstore/metricStore.ts
@@ -1,153 +1,164 @@
-import { makeAutoObservable, computed } from "mobx"
-import Widget from "./types/widget";
-import { metricService, errorService } from "App/services";
+import { makeAutoObservable, computed } from 'mobx';
+import Widget from './types/widget';
+import { metricService, errorService } from 'App/services';
import { toast } from 'react-toastify';
-import Error from "./types/error";
+import Error from './types/error';
export default class MetricStore {
- isLoading: boolean = false
- isSaving: boolean = false
+ isLoading: boolean = false;
+ isSaving: boolean = false;
- metrics: Widget[] = []
- instance = new Widget()
+ metrics: Widget[] = [];
+ instance = new Widget();
- page: number = 1
- pageSize: number = 10
- metricsSearch: string = ""
- sort: any = {}
+ page: number = 1;
+ pageSize: number = 10;
+ metricsSearch: string = '';
+ sort: any = { by: 'desc' };
- sessionsPage: number = 1
- sessionsPageSize: number = 10
+ sessionsPage: number = 1;
+ sessionsPageSize: number = 10;
- constructor() {
- makeAutoObservable(this)
+ constructor() {
+ makeAutoObservable(this);
+ }
+
+ @computed
+ get sortedWidgets() {
+ return [...this.metrics].sort((a, b) => b.lastModified - a.lastModified);
+ }
+
+ // State Actions
+ init(metric?: Widget | null) {
+ this.instance.update(metric || new Widget());
+ }
+
+ updateKey(key: string, value: any) {
+ // @ts-ignore
+ this[key] = value;
+ }
+
+ merge(object: any) {
+ Object.assign(this.instance, object);
+ this.instance.updateKey('hasChanged', true);
+ }
+
+ reset(id: string) {
+ const metric = this.findById(id);
+ if (metric) {
+ this.instance = metric;
}
+ }
- @computed
- get sortedWidgets() {
- return [...this.metrics].sort((a, b) => b.lastModified - a.lastModified)
+ addToList(metric: Widget) {
+ this.metrics.push(metric);
+ }
+
+ updateInList(metric: Widget) {
+ // @ts-ignore
+ const index = this.metrics.findIndex((m: Widget) => m[Widget.ID_KEY] === metric[Widget.ID_KEY]);
+ if (index >= 0) {
+ this.metrics[index] = metric;
}
+ }
- // State Actions
- init(metric?: Widget | null) {
- this.instance.update(metric || new Widget())
- }
+ findById(id: string) {
+ // @ts-ignore
+ return this.metrics.find((m) => m[Widget.ID_KEY] === id);
+ }
- updateKey(key: string, value: any) {
- // @ts-ignore
- this[key] = value
- }
+ removeById(id: string): void {
+ // @ts-ignore
+ this.metrics = this.metrics.filter((m) => m[Widget.ID_KEY] !== id);
+ }
- merge(object: any) {
- Object.assign(this.instance, object)
- this.instance.updateKey('hasChanged', true)
- }
+ get paginatedList(): Widget[] {
+ const start = (this.page - 1) * this.pageSize;
+ const end = start + this.pageSize;
+ return this.metrics.slice(start, end);
+ }
- reset(id: string) {
- const metric = this.findById(id)
- if (metric) {
- this.instance = metric
- }
- }
-
- addToList(metric: Widget) {
- this.metrics.push(metric)
- }
-
- updateInList(metric: Widget) {
- // @ts-ignore
- const index = this.metrics.findIndex((m: Widget) => m[Widget.ID_KEY] === metric[Widget.ID_KEY])
- if (index >= 0) {
- this.metrics[index] = metric
- }
- }
-
- findById(id: string) {
- // @ts-ignore
- return this.metrics.find(m => m[Widget.ID_KEY] === id)
- }
-
- removeById(id: string): void {
- // @ts-ignore
- this.metrics = this.metrics.filter(m => m[Widget.ID_KEY] !== id)
- }
-
- get paginatedList(): Widget[] {
- const start = (this.page - 1) * this.pageSize
- const end = start + this.pageSize
- return this.metrics.slice(start, end)
- }
-
- // API Communication
- save(metric: Widget, dashboardId?: string): Promise
{
- const wasCreating = !metric.exists()
- this.isSaving = true
- return new Promise((resolve, reject) => {
- metricService.saveMetric(metric, dashboardId)
- .then((metric: any) => {
- const _metric = new Widget().fromJson(metric)
- if (wasCreating) {
- toast.success('Metric created successfully')
- this.addToList(_metric)
- this.instance = _metric
- } else {
- toast.success('Metric updated successfully')
- this.updateInList(_metric)
- }
- resolve(_metric)
- }).catch(() => {
- toast.error('Error saving metric')
- reject()
- }).finally(() => {
- this.instance.updateKey('hasChanged', false)
- this.isSaving = false
- })
+ // API Communication
+ save(metric: Widget, dashboardId?: string): Promise {
+ const wasCreating = !metric.exists();
+ this.isSaving = true;
+ return new Promise((resolve, reject) => {
+ metricService
+ .saveMetric(metric, dashboardId)
+ .then((metric: any) => {
+ const _metric = new Widget().fromJson(metric);
+ if (wasCreating) {
+ toast.success('Metric created successfully');
+ this.addToList(_metric);
+ this.instance = _metric;
+ } else {
+ toast.success('Metric updated successfully');
+ this.updateInList(_metric);
+ }
+ resolve(_metric);
})
- }
-
- fetchList() {
- this.isLoading = true
- return metricService.getMetrics()
- .then((metrics: any[]) => {
- this.metrics = metrics.map(m => new Widget().fromJson(m))
- }).finally(() => {
- this.isLoading = false
- })
- }
-
- fetch(id: string, period?: any) {
- this.isLoading = true
- return metricService.getMetric(id)
- .then((metric: any) => {
- return this.instance = new Widget().fromJson(metric, period)
- })
- .finally(() => {
- this.isLoading = false
- })
- }
-
- delete(metric: Widget) {
- this.isSaving = true
- // @ts-ignore
- return metricService.deleteMetric(metric[Widget.ID_KEY])
- .then(() => {
- // @ts-ignore
- this.removeById(metric[Widget.ID_KEY])
- toast.success('Metric deleted successfully')
- }).finally(() => {
- this.instance.updateKey('hasChanged', false)
- this.isSaving = false
- })
- }
-
- fetchError(errorId: any): Promise {
- return new Promise((resolve, reject) => {
- errorService.one(errorId).then((error: any) => {
- resolve(new Error().fromJSON(error))
- }).catch((error: any) => {
- toast.error('Failed to fetch error details.')
- reject(error)
- })
+ .catch(() => {
+ toast.error('Error saving metric');
+ reject();
})
- }
+ .finally(() => {
+ this.instance.updateKey('hasChanged', false);
+ this.isSaving = false;
+ });
+ });
+ }
+
+ fetchList() {
+ this.isLoading = true;
+ return metricService
+ .getMetrics()
+ .then((metrics: any[]) => {
+ this.metrics = metrics.map((m) => new Widget().fromJson(m));
+ })
+ .finally(() => {
+ this.isLoading = false;
+ });
+ }
+
+ fetch(id: string, period?: any) {
+ this.isLoading = true;
+ return metricService
+ .getMetric(id)
+ .then((metric: any) => {
+ return (this.instance = new Widget().fromJson(metric, period));
+ })
+ .finally(() => {
+ this.isLoading = false;
+ });
+ }
+
+ delete(metric: Widget) {
+ this.isSaving = true;
+ // @ts-ignore
+ return metricService
+ .deleteMetric(metric[Widget.ID_KEY])
+ .then(() => {
+ // @ts-ignore
+ this.removeById(metric[Widget.ID_KEY]);
+ toast.success('Metric deleted successfully');
+ })
+ .finally(() => {
+ this.instance.updateKey('hasChanged', false);
+ this.isSaving = false;
+ });
+ }
+
+ fetchError(errorId: any): Promise {
+ return new Promise((resolve, reject) => {
+ errorService
+ .one(errorId)
+ .then((error: any) => {
+ resolve(new Error().fromJSON(error));
+ })
+ .catch((error: any) => {
+ toast.error('Failed to fetch error details.');
+ reject(error);
+ });
+ });
+ }
}