feat(ui) - errors details modal

This commit is contained in:
Shekar Siri 2022-06-20 17:25:48 +02:00
parent 1bcb0dfc01
commit e30e5c22ad
27 changed files with 1121 additions and 776 deletions

View file

@ -1,20 +1,38 @@
import React from 'react';
import React, { useEffect } from 'react';
import { Pagination } from 'UI';
import ErrorListItem from '../../../components/Errors/ErrorListItem';
import { withRouter, RouteComponentProps } from 'react-router-dom';
import { useModal } from 'App/components/Modal';
import ErrorDetailsModal from '../../../components/Errors/ErrorDetailsModal';
const PER_PAGE = 5;
interface Props {
metric: any;
isTemplate?: boolean;
isEdit?: boolean;
history: any,
}
function CustomMetricTableErrors(props: Props) {
function CustomMetricTableErrors(props: RouteComponentProps<Props>) {
const { metric, isEdit = false } = props;
const errorId = new URLSearchParams(props.location.search).get("errorId");
const { showModal } = useModal();
const onErrorClick = (error: any) => {
props.history.replace({search: (new URLSearchParams({errorId : error.errorId})).toString()});
}
useEffect(() => {
if (!errorId) return;
showModal(<ErrorDetailsModal errorId={errorId} />, { right: true, onClose: () => {
props.history.replace({search: ""});
}});
}, [errorId])
return (
<div>
{metric.data.errors && metric.data.errors.map((error: any, index: any) => (
<ErrorListItem error={error} />
<ErrorListItem error={error} onClick={() => onErrorClick(error)} />
))}
{isEdit && (
@ -36,7 +54,7 @@ function CustomMetricTableErrors(props: Props) {
);
}
export default CustomMetricTableErrors;
export default withRouter(CustomMetricTableErrors) as React.FunctionComponent<RouteComponentProps<Props>>;
const ViewMore = ({ total, limit }: any) => total > limit && (
<div className="my-4 flex items-center justify-center cursor-pointer w-fit mx-auto">

View file

@ -0,0 +1,69 @@
import React, { useState } from 'react'
import ErrorFrame from './ErrorFrame'
import cn from 'classnames';
import { IconButton, Icon } from 'UI';
import { connect } from 'react-redux';
const docLink = 'https://docs.openreplay.com/installation/upload-sourcemaps';
interface Props {
error: any,
errorStack: any,
}
function ErrorDetails({ className, name = "Error", message, errorStack, sourcemapUploaded }: any) {
const [showRaw, setShowRaw] = useState(false)
const firstFunc = errorStack.first() && errorStack.first().function
const openDocs = () => {
window.open(docLink, '_blank');
}
return (
<div className={className} >
{ !sourcemapUploaded && (
<div
style={{ backgroundColor: 'rgba(204, 0, 0, 0.1)' }}
className="font-normal flex items-center text-sm font-regular color-red border p-2 rounded"
>
<Icon name="info" size="16" color="red" />
<div className="ml-2">Source maps must be uploaded to OpenReplay to be able to see stack traces. <a href="#" className="color-red font-medium underline" style={{ textDecoration: 'underline' }} onClick={openDocs}>Learn more.</a></div>
</div>
) }
<div className="flex items-center my-3">
<h3 className="text-xl mr-auto">
Stacktrace
</h3>
<div className="flex justify-end mr-2">
<IconButton
onClick={() => setShowRaw(false) }
label="FULL"
plain={!showRaw}
primaryText={!showRaw}
/>
<IconButton
primaryText={showRaw}
onClick={() => setShowRaw(true) }
plain={showRaw}
label="RAW"
/>
</div>
</div>
<div className="mb-6 code-font" data-hidden={showRaw}>
<div className="leading-relaxed font-weight-bold">{ name }</div>
<div style={{ wordBreak: 'break-all'}}>{message}</div>
</div>
{ showRaw &&
<div className="mb-3 code-font">{name} : {firstFunc ? firstFunc : '?' }</div>
}
{ errorStack.map((frame: any, i: any) => (
<div className="mb-3" key={frame.key}>
<ErrorFrame frame={frame} showRaw={showRaw} isFirst={i == 0} />
</div>
))
}
</div>
)
}
ErrorDetails.displayName = "ErrorDetails";
export default ErrorDetails;

View file

@ -0,0 +1,53 @@
import React, { useState } from 'react'
import { Icon } from 'UI';
import cn from 'classnames';
import stl from './errorFrame.module.css';
function ErrorFrame({ frame = {}, showRaw, isFirst }) {
const [open, setOpen] = useState(isFirst)
const hasContext = frame.context && frame.context.length > 0;
return (
<div>
{ showRaw ?
<div className={stl.rawLine}>at { frame.function ? frame.function : '?' } <span className="color-gray-medium">({`${frame.filename}:${frame.lineNo}:${frame.colNo}`})</span></div>
:
<div className={stl.formatted}>
<div className={cn(stl.header, 'flex items-center cursor-pointer')} onClick={() => setOpen(!open)}>
<div className="truncate">
<span className="font-medium">{ frame.absPath }</span>
{ frame.function &&
<>
<span>{' in '}</span>
<span className="font-medium"> {frame.function} </span>
</>
}
<span>{' at line '}</span>
<span className="font-medium">
{frame.lineNo}:{frame.colNo}
</span>
</div>
{ hasContext &&
<div className="ml-auto mr-3">
<Icon name={ open ? 'minus' : 'plus'} size="14" color="gray-medium" />
</div>
}
</div>
{ open && hasContext &&
<ol start={ frame.context[0][0]} className={stl.content}>
{ frame.context.map(i => (
<li
key={i[0]}
className={ cn("leading-7 text-sm break-all h-auto pl-2", { [stl.errorLine] :i[0] == frame.lineNo }) }
>
<span>{ i[1].replace(/ /g, "\u00a0") }</span>
</li>
))}
</ol>
}
</div>
}
</div>
)
}
export default ErrorFrame;

View file

@ -0,0 +1,25 @@
.rawLine {
margin-left: 30px;
font-family: 'Menlo', 'monaco', 'consolas', monospace;
font-size: 13px;
}
.formatted {
border: solid thin #EEE;
border-radius: 3px;
}
.header {
background-color: $gray-lightest;
padding: 8px;
border-bottom: solid thin #EEE;
}
.content {
font-family: 'Menlo', 'monaco', 'consolas', monospace;
list-style-position: inside;
list-style-type: decimal-leading-zero;
}
.errorLine {
background-color: $teal;
color: white !important;
font-weight: bold;
}

View file

@ -0,0 +1 @@
export { default } from './ErrorFrame';

View file

@ -0,0 +1 @@
export { default } from './ErrorDetails';

View file

@ -0,0 +1,35 @@
import React, { useEffect } from 'react';
import { withRouter, RouteComponentProps } from 'react-router-dom';
import { useStore } from 'App/mstore';
import { NoContent } from 'UI';
// import ErrorDetails from '../ErrorDetails/ErrorDetails';
import ErrorInfo from '../../../../Errors/Error/ErrorInfo';
interface Props {
errorId: any
}
function ErrorDetailsModal(props: RouteComponentProps) {
const [error, setError] = React.useState(null);
// const [loading, setLoading] = React.useState(true);
// const { metricStore } = useStore();
// const fetchData = async () => {
// await metricStore.fetchError(props.errorId).then(setError);
// setLoading(false);
// }
// useEffect(() => {
// // console.log('errorId', props.errorId);
// fetchData();
// }, [])
return (
<div style={{ width: '85vw', maxWidth: '1200px' }} className="bg-white h-screen p-4 overflow-y-auto">
{/* <NoContent show={!loading && !error}> */}
{/* <ErrorDetails error={error} /> */}
<ErrorInfo errorId={props.errorId} list={[]} />
{/* </NoContent> */}
</div>
);
}
export default withRouter(ErrorDetailsModal);

View file

@ -0,0 +1 @@
export { default } from './ErrorDetailsModal'

View file

@ -9,17 +9,27 @@ import ErrorLabel from '../ErrorLabel';
import { BarChart, Bar, YAxis, Tooltip, XAxis } from 'recharts';
import { Styles } from '../../../Widgets/common';
import { diffFromNowString } from 'App/date';
import { useModal } from '../../../../Modal';
import ErrorDetailsModal from '../ErrorDetailsModal';
interface Props {
error: any;
className?: string;
onClick: () => void;
}
function ErrorListItem(props: Props) {
const { error, className = '' } = props;
// const { showModal } = useModal();
// const onClick = () => {
// alert('test')
// showModal(<ErrorDetailsModal />, { right: true });
// }
return (
<div
className={ cn("border p-3 grid grid-cols-12 gap-4 cursor-pointer py-4 hover:bg-active-blue mb-3", className) }
id="error-item"
onClick={props.onClick}
>
<div className={ cn("col-span-6 leading-tight") } >
<div>

View file

@ -5,7 +5,6 @@ import { Loader } from 'UI';
import FunnelIssuesDropdown from '../FunnelIssuesDropdown';
import FunnelIssuesSort from '../FunnelIssuesSort';
import FunnelIssuesList from '../FunnelIssuesList';
import { DateTime } from 'luxon';
import { debounce } from 'App/utils';
import useIsMounted from 'App/hooks/useIsMounted';
@ -13,12 +12,31 @@ function FunnelIssues() {
const { metricStore, dashboardStore } = useStore();
const [data, setData] = useState<any>({ issues: [] });
const [loading, setLoading] = useState(false);
const widget: any = useObserver(() => metricStore.instance);
const funnel = useObserver(() => widget.data.funnel || { stages: [] });
const stages = useObserver(() => funnel.stages.filter((stage: any) => stage.isActive));
const isMounted = useIsMounted()
const fetchIssues = (filter: any) => {
if (!isMounted()) return;
setLoading(true)
widget.fetchIssues(filter).then((res: any) => {
const newFilter = {
...filter,
series: filter.series.map((item: any) => {
return {
...item,
filter: {
...item.filter,
filters: item.filter.filters.filter((filter: any, index: any) => {
const stage = widget.data.funnel.stages[index];
return stage &&stage.isActive
})
}
}
}),
}
widget.fetchIssues(newFilter).then((res: any) => {
setData(res)
}).finally(() => {
setLoading(false)
@ -26,15 +44,13 @@ function FunnelIssues() {
}
const filter = useObserver(() => dashboardStore.drillDownFilter);
const widget: any = useObserver(() => 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');
const drillDownPeriod = useObserver(() => dashboardStore.drillDownPeriod);
const debounceRequest: any = React.useCallback(debounce(fetchIssues, 1000), []);
const depsString = JSON.stringify(widget.series);
useEffect(() => {
debounceRequest({ ...filter, series: widget.toJsonDrilldown(), page: metricStore.sessionsPage, limit: metricStore.sessionsPageSize });
}, [filter.startTimestamp, filter.endTimestamp, filter.filters, depsString, metricStore.sessionsPage]);
}, [stages.length, drillDownPeriod, filter.filters, depsString, metricStore.sessionsPage]);
return useObserver(() => (
<div className="my-8">

View file

@ -1,19 +1,38 @@
import { useStore } from 'App/mstore';
import { useObserver } from 'mobx-react-lite';
import React from 'react';
import React, { useEffect } from 'react';
import FunnelIssuesListItem from '../FunnelIssuesListItem';
import { withRouter, RouteComponentProps } from 'react-router-dom';
import { NoContent } from 'UI';
import { useModal } from 'App/components/Modal';
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
import FunnelIssueModal from '../FunnelIssueModal';
interface Props {
loading?: boolean;
issues: any;
history: any;
location: any;
}
function FunnelIssuesList(props: Props) {
function FunnelIssuesList(props: RouteComponentProps<Props>) {
const { issues, loading } = props;
const { funnelStore } = useStore();
const issuesSort = useObserver(() => funnelStore.issuesSort);
const issuesFilter = useObserver(() => funnelStore.issuesFilter.map((issue: any) => issue.value));
const { showModal } = useModal();
const issueId = new URLSearchParams(props.location.search).get("issueId");
const onIssueClick = (issue: any) => {
props.history.replace({search: (new URLSearchParams({issueId : issue.issueId})).toString()});
}
useEffect(() => {
if (!issueId) return;
showModal(<FunnelIssueModal issueId={issueId} />, { right: true, onClose: () => {
props.history.replace({search: ""});
}});
}, [issueId]);
let filteredIssues = useObserver(() => issuesFilter.length > 0 ? issues.filter((issue: any) => issuesFilter.includes(issue.type)) : issues);
filteredIssues = useObserver(() => issuesSort.sort ? filteredIssues.slice().sort((a: { [x: string]: number; }, b: { [x: string]: number; }) => a[issuesSort.sort] - b[issuesSort.sort]): filteredIssues);
@ -31,11 +50,11 @@ function FunnelIssuesList(props: Props) {
>
{filteredIssues.map((issue: any, index: React.Key) => (
<div key={index} className="mb-4">
<FunnelIssuesListItem issue={issue} />
<FunnelIssuesListItem issue={issue} onClick={() => onIssueClick(issue)} />
</div>
))}
</NoContent>
))
}
export default FunnelIssuesList;
export default withRouter(FunnelIssuesList) as React.FunctionComponent<RouteComponentProps<Props>>;

View file

@ -8,13 +8,14 @@ import FunnelIssueModal from '../FunnelIssueModal';
interface Props {
issue: any;
inDetails?: boolean;
onClick?: () => void;
}
function FunnelIssuesListItem(props) {
const { issue, inDetails = false } = props;
const { showModal } = useModal();
const onClick = () => {
showModal(<FunnelIssueModal issueId={issue.issueId} />, { right: true });
}
function FunnelIssuesListItem(props: Props) {
const { issue, inDetails = false, onClick } = props;
// const { showModal } = useModal();
// const onClick = () => {
// showModal(<FunnelIssueModal issueId={issue.issueId} />, { right: true });
// }
return (
<div className={cn('flex flex-col bg-white w-full rounded border relative hover:bg-active-blue', { 'cursor-pointer bg-hover' : !inDetails })} onClick={!inDetails ? onClick : () => null}>
{/* {inDetails && (

View file

@ -29,6 +29,7 @@ function WidgetChart(props: Props) {
const { dashboardStore, metricStore } = useStore();
const _metric: any = useObserver(() => metricStore.instance);
const period = useObserver(() => dashboardStore.period);
const drillDownPeriod = useObserver(() => dashboardStore.drillDownPeriod);
const drillDownFilter = useObserver(() => dashboardStore.drillDownFilter);
const colors = Styles.customMetricColors;
const [loading, setLoading] = useState(true)
@ -44,14 +45,14 @@ function WidgetChart(props: Props) {
const onChartClick = (event: any) => {
if (event) {
if (isTableWidget || isPieChart) {
if (isTableWidget || isPieChart) { // get the filter of clicked row
const periodTimestamps = period.toTimestamps()
drillDownFilter.merge({
filters: event,
startTimestamp: periodTimestamps.startTimestamp,
endTimestamp: periodTimestamps.endTimestamp,
});
} else {
} else { // get the filter of clicked chart point
const payload = event.activePayload[0].payload;
const timestamp = payload.timestamp;
const periodTimestamps = getStartAndEndTimestampsByDensity(timestamp, period.start, period.end, params.density);
@ -82,9 +83,10 @@ function WidgetChart(props: Props) {
return
};
prevMetricRef.current = metric;
const payload = isWidget ? { ...params } : { ...metricParams, ...metric.toJson() };
const timestmaps = drillDownPeriod.toTimestamps();
const payload = isWidget ? { ...params } : { ...metricParams, ...timestmaps, ...metric.toJson() };
debounceRequest(metric, payload, isWidget);
}, [period, depsString, _metric.page, metric.metricType, metric.metricOf, metric.viewType]);
}, [drillDownPeriod, period, depsString, _metric.page, metric.metricType, metric.metricOf, metric.viewType]);
const renderChart = () => {

View file

@ -0,0 +1,36 @@
import React from 'react';
import SelectDateRange from 'Shared/SelectDateRange';
import { useStore } from 'App/mstore';
import { useObserver } from 'mobx-react-lite';
interface Props {
}
function WidgetDateRange(props: Props) {
const { dashboardStore } = useStore();
const period = useObserver(() => dashboardStore.drillDownPeriod);
const drillDownFilter = useObserver(() => dashboardStore.drillDownFilter);
const onChangePeriod = (period: any) => {
dashboardStore.setDrillDownPeriod(period);
const periodTimestamps = period.toTimestamps();
drillDownFilter.merge({
startTimestamp: periodTimestamps.startTimestamp,
endTimestamp: periodTimestamps.endTimestamp,
})
}
return (
<>
<span className="mr-1 color-gray-medium">Time Range</span>
<SelectDateRange
period={period}
// onChange={(period: any) => metric.setPeriod(period)}
onChange={onChangePeriod}
right={true}
/>
</>
);
}
export default WidgetDateRange;

View file

@ -6,6 +6,7 @@ import { SegmentSelection } from 'UI';
import { useObserver } from 'mobx-react-lite';
import SelectDateRange from 'Shared/SelectDateRange';
import { FilterKey } from 'Types/filter/filterType';
import WidgetDateRange from '../WidgetDateRange/WidgetDateRange';
// import Period, { LAST_24_HOURS, LAST_30_DAYS } from 'Types/app/period';
interface Props {
@ -17,22 +18,47 @@ function WidgetPreview(props: Props) {
const metric: any = useObserver(() => metricStore.instance);
const isTimeSeries = metric.metricType === 'timeseries';
const isTable = metric.metricType === 'table';
// const drillDownFilter = useObserver(() => dashboardStore.drillDownFilter);
const drillDownFilter = useObserver(() => dashboardStore.drillDownFilter);
const disableVisualization = useObserver(() => metric.metricOf === FilterKey.SESSIONS || metric.metricOf === FilterKey.ERRORS);
const period = useObserver(() => dashboardStore.drillDownPeriod);
// const period = useObserver(() => dashboardStore.drillDownPeriod);
const chagneViewType = (e, { name, value }: any) => {
metric.update({ [ name ]: value });
}
const onChangePeriod = (period: any) => {
dashboardStore.setDrillDownPeriod(period);
// const onChangePeriod = (period: any) => {
// dashboardStore.setDrillDownPeriod(period);
// const periodTimestamps = period.toTimestamps();
// drillDownFilter.merge({
// startTimestamp: periodTimestamps.startTimestamp,
// endTimestamp: periodTimestamps.endTimestamp,
// })
// }
const getWidgetTitle = () => {
if (isTimeSeries) {
return 'Time Series';
} else if (isTable) {
if (metric.metricOf === FilterKey.SESSIONS) {
return 'Table of Sessions';
// return <div>Sessions <span className="color-gray-medium">{metric.data.total}</span></div>;
} else if (metric.metricOf === FilterKey.ERRORS) {
return 'Table of Errors';
// return <div>Errors <span className="color-gray-medium">{metric.data.total}</span></div>;
} else {
return 'Table';
}
} else if (metric.metricType === 'funnel') {
return 'Funnel';
}
}
return useObserver(() => (
<div className={cn(className)}>
<div className="flex items-center justify-between">
<h2 className="text-2xl">Trend</h2>
<div className="flex items-center justify-between mb-2">
<h2 className="text-2xl">
{getWidgetTitle()}
</h2>
<div className="flex items-center">
{isTimeSeries && (
<>
@ -72,14 +98,8 @@ function WidgetPreview(props: Props) {
</>
)}
<div className="mx-4" />
<span className="mr-1 color-gray-medium">Time Range</span>
<SelectDateRange
period={period}
// onChange={(period: any) => metric.setPeriod(period)}
onChange={onChangePeriod}
right={true}
/>
</div>
<WidgetDateRange />
</div>
</div>
<div className="bg-white rounded p-4">
<WidgetWrapper widget={metric} isPreview={true} isWidget={false} />

View file

@ -23,13 +23,13 @@ function WidgetSessions(props: Props) {
const { dashboardStore, metricStore } = useStore();
const filter = useObserver(() => dashboardStore.drillDownFilter);
const widget: any = useObserver(() => metricStore.instance);
const drillDownPeriod = useObserver(() => dashboardStore.drillDownPeriod);
// const drillDownPeriod = useObserver(() => dashboardStore.drillDownPeriod);
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');
const [timestamps, setTimestamps] = useState<any>({
startTimestamp: 0,
endTimestamp: 0,
});
// const [timestamps, setTimestamps] = useState<any>({
// startTimestamp: 0,
// endTimestamp: 0,
// });
const [seriesOptions, setSeriesOptions] = useState([
{ label: 'All', value: 'all' },
]);
@ -63,11 +63,11 @@ function WidgetSessions(props: Props) {
debounceRequest(widget.metricId, { ...filter, series: widget.toJsonDrilldown(), page: metricStore.sessionsPage, limit: metricStore.sessionsPageSize });
}, [filter.startTimestamp, filter.endTimestamp, filter.filters, depsString, metricStore.sessionsPage]);
useEffect(() => {
const timestamps = drillDownPeriod.toTimestamps();
console.log('timestamps', timestamps);
debounceRequest(widget.metricId, { startTime: timestamps.startTimestamp, endTime: timestamps.endTimestamp, series: widget.toJsonDrilldown(), page: metricStore.sessionsPage, limit: metricStore.sessionsPageSize });
}, [drillDownPeriod]);
// useEffect(() => {
// const timestamps = drillDownPeriod.toTimestamps();
// // console.log('timestamps', timestamps);
// debounceRequest(widget.metricId, { startTime: timestamps.startTimestamp, endTime: timestamps.endTimestamp, series: widget.toJsonDrilldown(), page: metricStore.sessionsPage, limit: metricStore.sessionsPageSize });
// }, [drillDownPeriod]);
return useObserver(() => (
<div className={cn(className)}>

View file

@ -65,7 +65,6 @@ function WidgetWrapper(props: Props) {
const ref: any = useRef(null)
const dragDropRef: any = dragRef(dropRef(ref))
const addOverlay = isTemplate || (!isPredefined && isWidget)
return useObserver(() => (

View file

@ -36,6 +36,7 @@ function DistributionBar({ className, title, partitions }) {
{`${ Math.round(p.prc) }%`}
</div>
}
className="w-full"
>
<div
className="h-full bg-tealx"

View file

@ -145,7 +145,7 @@ export default class MainSection extends React.PureComponent {
loading={ toggleFavoriteLoading }
onClick={ this.bookmark }
/>
<SharePopup
{/* <SharePopup
entity="errors"
id={ error.errorId }
trigger={
@ -156,7 +156,7 @@ export default class MainSection extends React.PureComponent {
icon="share-alt"
/>
}
/>
/> */}
</div>
<Divider />
<div className="m-4">

View file

@ -5,6 +5,7 @@ import cn from 'classnames';
import stl from './FunnelWidget.module.css';
import { Icon } from 'UI';
import { useObserver } from 'mobx-react-lite';
import { NoContent } from 'UI';
interface Props {
metric: Widget;
@ -13,14 +14,13 @@ interface Props {
function FunnelWidget(props: Props) {
const { metric, isWidget = false } = props;
const funnel = metric.data.funnel || { stages: [] };
// pic firt and last from array
const totalSteps = funnel.stages.length;
const stages = isWidget ? [...funnel.stages.slice(0, 1), funnel.stages[funnel.stages.length - 1]] : funnel.stages;
const hasMoreSteps = funnel.stages.length > 2;
const lastStage = funnel.stages[funnel.stages.length - 1];
return useObserver(() => (
<>
<NoContent show={!stages || stages.length === 0}>
<div className="w-full">
{ !isWidget && (
stages.map((filter: any, index: any) => (
@ -69,7 +69,7 @@ function FunnelWidget(props: Props) {
</div>
</div>
</div>
</>
</NoContent>
));
}

View file

@ -6,6 +6,7 @@ const ModalContext = createContext({
component: null,
props: {
right: false,
onClose: () => {},
},
showModal: (component: any, props: any) => {},
hideModal: () => {}
@ -19,7 +20,7 @@ export class ModalProvider extends Component {
}
}
showModal = (component, props = {}) => {
showModal = (component, props = { }) => {
this.setState({
component,
props
@ -28,6 +29,10 @@ export class ModalProvider extends Component {
};
hideModal = () => {
const { props } = this.state;
if (props.onClose) {
props.onClose();
};
this.setState({
component: null,
props: {}

View file

@ -1,7 +1,8 @@
import { makeAutoObservable, runInAction, observable, action, reaction, computed } from "mobx"
import Widget, { IWidget } from "./types/widget";
import { metricService } from "App/services";
import { metricService, errorService } from "App/services";
import { toast } from 'react-toastify';
import Error from "./types/error";
export interface IMetricStore {
paginatedList: any;
@ -29,6 +30,7 @@ export interface IMetricStore {
updateInList(metric: IWidget): void
findById(metricId: string): void
removeById(metricId: string): void
fetchError(errorId: string): Promise<any>
// API
save(metric: IWidget, dashboardId?: string): Promise<any>
@ -76,6 +78,8 @@ export default class MetricStore implements IMetricStore {
fetch: action,
delete: action,
fetchError: action,
paginatedList: computed,
})
}
@ -187,6 +191,17 @@ export default class MetricStore implements IMetricStore {
this.isSaving = false
})
}
fetchError(errorId: any): Promise<any> {
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)
})
})
}
}
const sampleJsonFunnel = {

View file

@ -1,7 +1,7 @@
import { makeAutoObservable, runInAction, observable, action } from "mobx"
import FilterSeries from "./filterSeries";
import { DateTime } from 'luxon';
import { metricService } from "App/services";
import { metricService, errorService } from "App/services";
import Session from "App/mstore/types/session";
import Funnelissue from 'App/mstore/types/funnelIssue';
import { issueOptions } from 'App/constants/filterOptions';

View file

@ -1,14 +1,16 @@
import BaseService from './BaseService';
import { fetchErrorCheck } from 'App/utils'
export default class ErrorService extends BaseService {
all(params: any = {}): Promise<any[]> {
return this.client.post('/errors/search', params)
.then(response => response.json())
.then(response => response.data || []);
.then(fetchErrorCheck)
.then((response: { data: any; }) => response.data || []);
}
one(id: string): Promise<any> {
return this.client.get(`/errors/${id}`)
.then(response => response.json())
.then(fetchErrorCheck)
.then((response: { data: any; }) => response.data || {});
}
}

File diff suppressed because it is too large Load diff

View file

@ -48,8 +48,8 @@
"react-json-view": "^1.21.3",
"react-lazyload": "^3.2.0",
"react-redux": "^5.1.2",
"react-router": "^4.3.1",
"react-router-dom": "^4.3.1",
"react-router": "^5.3.3",
"react-router-dom": "^5.3.3",
"react-select": "^5.3.2",
"react-svg-map": "^2.2.0",
"react-tippy": "^1.4.0",