feat(ui) - errors details modal
This commit is contained in:
parent
1bcb0dfc01
commit
e30e5c22ad
27 changed files with 1121 additions and 776 deletions
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './ErrorFrame';
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './ErrorDetails';
|
||||
|
|
@ -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);
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './ErrorDetailsModal'
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>>;
|
||||
|
|
@ -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 && (
|
||||
|
|
|
|||
|
|
@ -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 = () => {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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} />
|
||||
|
|
|
|||
|
|
@ -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)}>
|
||||
|
|
|
|||
|
|
@ -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(() => (
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ function DistributionBar({ className, title, partitions }) {
|
|||
{`${ Math.round(p.prc) }%`}
|
||||
</div>
|
||||
}
|
||||
className="w-full"
|
||||
>
|
||||
<div
|
||||
className="h-full bg-tealx"
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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: {}
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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 || {});
|
||||
}
|
||||
}
|
||||
1442
frontend/package-lock.json
generated
1442
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue