feat(ui) - overview - error details modal

This commit is contained in:
Shekar Siri 2022-08-11 15:58:33 +02:00
parent f3bf7be8cc
commit 8805c84735
8 changed files with 201 additions and 225 deletions

View file

@ -2,82 +2,77 @@ import React from 'react';
import { connect } from 'react-redux';
import withSiteIdRouter from 'HOCs/withSiteIdRouter';
import { errors as errorsRoute, error as errorRoute } from 'App/routes';
import { NoContent , Loader, IconButton, Icon, Popup, BackLink, } from 'UI';
import { NoContent, Loader, IconButton, Icon, Popup, BackLink } from 'UI';
import { fetch, fetchTrace } from 'Duck/errors';
import MainSection from './MainSection';
import SideSection from './SideSection';
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
@connect(state =>({
errorIdInStore: state.getIn(["errors", "instance"]).errorId,
loading: state.getIn([ "errors", "fetch", "loading" ]) || state.getIn([ "errors", "fetchTrace", "loading" ]),
errorOnFetch: state.getIn(["errors", "fetch", "errors"]) || state.getIn([ "errors", "fetchTrace", "errors" ]),
}), {
fetch,
fetchTrace,
})
@connect(
(state) => ({
errorIdInStore: state.getIn(['errors', 'instance']).errorId,
loading: state.getIn(['errors', 'fetch', 'loading']) || state.getIn(['errors', 'fetchTrace', 'loading']),
errorOnFetch: state.getIn(['errors', 'fetch', 'errors']) || state.getIn(['errors', 'fetchTrace', 'errors']),
}),
{
fetch,
fetchTrace,
}
)
@withSiteIdRouter
export default class ErrorInfo extends React.PureComponent {
ensureInstance() {
const { errorId, loading, errorOnFetch } = this.props;
if (!loading &&
this.props.errorIdInStore !== errorId &&
errorId != null) {
this.props.fetch(errorId);
this.props.fetchTrace(errorId)
}
}
componentDidMount() {
this.ensureInstance();
}
componentDidUpdate() {
this.ensureInstance();
}
next = () => {
const { list, errorId } = this.props;
const curIndex = list.findIndex(e => e.errorId === errorId);
const next = list.get(curIndex + 1);
if (next != null) {
this.props.history.push(errorRoute(next.errorId))
}
}
prev = () => {
const { list, errorId } = this.props;
const curIndex = list.findIndex(e => e.errorId === errorId);
const prev = list.get(curIndex - 1);
if (prev != null) {
this.props.history.push(errorRoute(prev.errorId))
}
}
render() {
const {
loading,
errorIdInStore,
list,
errorId,
} = this.props;
ensureInstance() {
const { errorId, loading, errorOnFetch } = this.props;
if (!loading && this.props.errorIdInStore !== errorId && errorId != null) {
this.props.fetch(errorId);
this.props.fetchTrace(errorId);
}
}
componentDidMount() {
this.ensureInstance();
}
componentDidUpdate() {
this.ensureInstance();
}
next = () => {
const { list, errorId } = this.props;
const curIndex = list.findIndex((e) => e.errorId === errorId);
const next = list.get(curIndex + 1);
if (next != null) {
this.props.history.push(errorRoute(next.errorId));
}
};
prev = () => {
const { list, errorId } = this.props;
const curIndex = list.findIndex((e) => e.errorId === errorId);
const prev = list.get(curIndex - 1);
if (prev != null) {
this.props.history.push(errorRoute(prev.errorId));
}
};
render() {
const { loading, errorIdInStore, list, errorId } = this.props;
let nextDisabled = true,
prevDisabled = true;
if (list.size > 0) {
nextDisabled = loading || list.last().errorId === errorId;
prevDisabled = loading || list.first().errorId === errorId;
}
let nextDisabled = true,
prevDisabled = true;
if (list.size > 0) {
nextDisabled = loading || list.last().errorId === errorId;
prevDisabled = loading || list.first().errorId === errorId;
}
return (
<NoContent
title={
<div className="flex flex-col items-center justify-center">
<AnimatedSVG name={ICONS.EMPTY_STATE} size="170" />
<div className="mt-6 text-2xl">No Error Found!</div>
</div>
}
subtext="Please try to find existing one."
// animatedIcon="no-results"
show={ !loading && errorIdInStore == null }
>
{/* <div className="w-9/12 mb-4 flex justify-between">
return (
<NoContent
title={
<div className="flex flex-col items-center justify-center">
<AnimatedSVG name={ICONS.EMPTY_STATE} size="170" />
<div className="mt-6 text-2xl">No Error Found!</div>
</div>
}
subtext="Please try to find existing one."
// animatedIcon="no-results"
show={!loading && errorIdInStore == null}
>
{/* <div className="w-9/12 mb-4 flex justify-between">
<BackLink to={ errorsRoute() } label="Back" />
<div />
<div className="flex items-center">
@ -111,13 +106,13 @@ export default class ErrorInfo extends React.PureComponent {
</Popup>
</div>
</div> */}
<div className="flex" >
<Loader loading={ loading } className="w-9/12">
<MainSection className="w-9/12" />
<SideSection className="w-3/12" />
</Loader>
</div>
</NoContent>
);
}
}
<div className="flex">
<Loader loading={loading} className="w-9/12">
<MainSection className="w-9/12" />
<SideSection className="w-3/12" />
</Loader>
</div>
</NoContent>
);
}
}

View file

@ -5,105 +5,89 @@ import withSiteIdRouter from 'HOCs/withSiteIdRouter';
import { ErrorDetails, IconButton, Icon, Loader, Button } from 'UI';
import { sessions as sessionsRoute } from 'App/routes';
import { TYPES as EV_FILER_TYPES } from 'Types/filter/event';
import { UNRESOLVED, RESOLVED, IGNORED } from "Types/errorInfo";
import { UNRESOLVED, RESOLVED, IGNORED } from 'Types/errorInfo';
import { addFilterByKeyAndValue } from 'Duck/search';
import { resolve,unresolve,ignore, toggleFavorite } from "Duck/errors";
import { resolve, unresolve, ignore, toggleFavorite } from 'Duck/errors';
import { resentOrDate } from 'App/date';
import Divider from 'Components/Errors/ui/Divider';
import ErrorName from 'Components/Errors/ui/ErrorName';
import Label from 'Components/Errors/ui/Label';
import SharePopup from 'Shared/SharePopup'
import SharePopup from 'Shared/SharePopup';
import { FilterKey } from 'Types/filter/filterType';
import SessionBar from './SessionBar';
@withSiteIdRouter
@connect(state => ({
error: state.getIn([ "errors", "instance" ]),
trace: state.getIn([ "errors", "instanceTrace" ]),
sourcemapUploaded: state.getIn([ "errors", "sourcemapUploaded" ]),
resolveToggleLoading: state.getIn(["errors", "resolve", "loading"]) ||
state.getIn(["errors", "unresolve", "loading"]),
ignoreLoading: state.getIn([ "errors", "ignore", "loading" ]),
toggleFavoriteLoading: state.getIn([ "errors", "toggleFavorite", "loading" ]),
traceLoading: state.getIn([ "errors", "fetchTrace", "loading"]),
}),{
resolve,
unresolve,
ignore,
toggleFavorite,
addFilterByKeyAndValue,
})
@connect(
(state) => ({
error: state.getIn(['errors', 'instance']),
trace: state.getIn(['errors', 'instanceTrace']),
sourcemapUploaded: state.getIn(['errors', 'sourcemapUploaded']),
resolveToggleLoading: state.getIn(['errors', 'resolve', 'loading']) || state.getIn(['errors', 'unresolve', 'loading']),
ignoreLoading: state.getIn(['errors', 'ignore', 'loading']),
toggleFavoriteLoading: state.getIn(['errors', 'toggleFavorite', 'loading']),
traceLoading: state.getIn(['errors', 'fetchTrace', 'loading']),
}),
{
resolve,
unresolve,
ignore,
toggleFavorite,
addFilterByKeyAndValue,
}
)
export default class MainSection extends React.PureComponent {
resolve = () => {
const { error } = this.props;
this.props.resolve(error.errorId)
}
resolve = () => {
const { error } = this.props;
this.props.resolve(error.errorId);
};
unresolve = () => {
const { error } = this.props;
this.props.unresolve(error.errorId)
}
unresolve = () => {
const { error } = this.props;
this.props.unresolve(error.errorId);
};
ignore = () => {
const { error } = this.props;
this.props.ignore(error.errorId)
}
bookmark = () => {
const { error } = this.props;
this.props.toggleFavorite(error.errorId);
}
ignore = () => {
const { error } = this.props;
this.props.ignore(error.errorId);
};
bookmark = () => {
const { error } = this.props;
this.props.toggleFavorite(error.errorId);
};
findSessions = () => {
this.props.addFilterByKeyAndValue(FilterKey.ERROR, this.props.error.message);
this.props.history.push(sessionsRoute());
}
findSessions = () => {
this.props.addFilterByKeyAndValue(FilterKey.ERROR, this.props.error.message);
this.props.history.push(sessionsRoute());
};
render() {
const {
error,
trace,
sourcemapUploaded,
ignoreLoading,
resolveToggleLoading,
toggleFavoriteLoading,
className,
traceLoading,
} = this.props;
render() {
const { error, trace, sourcemapUploaded, ignoreLoading, resolveToggleLoading, toggleFavoriteLoading, className, traceLoading } = this.props;
return (
<div className={cn(className, "bg-white border-radius-3 thin-gray-border mb-6")} >
<div className="m-4">
<ErrorName
className="text-lg leading-relaxed"
name={ error.name }
message={ error.stack0InfoString }
lineThrough={ error.status === RESOLVED }
/>
<div className="flex justify-between items-center">
<div className="flex items-center color-gray-dark" style={{ wordBreak: 'break-all'}}>
{ error.message }
</div>
<div className="text-center">
<div className="flex">
<Label
topValue={ error.sessions }
topValueSize="text-lg"
bottomValue="Sessions"
/>
<Label
topValue={ error.users }
topValueSize="text-lg"
bottomValue="Users"
/>
</div>
<div className="text-xs color-gray-medium">Over the past 30 days</div>
</div>
</div>
</div>
return (
<div className={cn(className, 'bg-white border-radius-3 thin-gray-border mb-6')}>
<div className="m-4">
<ErrorName
className="text-lg leading-relaxed"
name={error.name}
message={error.stack0InfoString}
lineThrough={error.status === RESOLVED}
/>
<div className="flex justify-between items-center">
<div className="flex items-center color-gray-dark" style={{ wordBreak: 'break-all' }}>
{error.message}
</div>
<div className="text-center">
<div className="flex">
<Label topValue={error.sessions} topValueSize="text-lg" bottomValue="Sessions" />
<Label topValue={error.users} topValueSize="text-lg" bottomValue="Users" />
</div>
<div className="text-xs color-gray-medium">Over the past 30 days</div>
</div>
</div>
</div>
{/* <Divider />
{/* <Divider />
<div className="flex m-4">
{ error.status === UNRESOLVED
? <IconButton
@ -158,35 +142,29 @@ export default class MainSection extends React.PureComponent {
}
/>
</div> */}
<Divider />
<div className="m-4">
<h3 className="text-xl inline-block mr-2">Last session with this error</h3>
<span className="font-thin text-sm">{ resentOrDate(error.lastOccurrence) }</span>
<SessionBar
className="my-4"
session={ error.lastHydratedSession }
/>
<Button
variant="text-primary"
onClick={ this.findSessions }
>
Find all sessions with this error
<Icon className="ml-1" name="next1" color="teal" />
</Button>
</div>
<Divider />
<div className="m-4">
<Loader loading={ traceLoading }>
<ErrorDetails
name={error.name}
message={error.message}
errorStack={trace}
sourcemapUploaded={sourcemapUploaded}
/>
</Loader>
</div>
</div>
);
}
}
<Divider />
<div className="m-4">
<h3 className="text-xl inline-block mr-2">Last session with this error</h3>
<span className="font-thin text-sm">{resentOrDate(error.lastOccurrence)}</span>
<SessionBar className="my-4" session={error.lastHydratedSession} />
<Button variant="text-primary" onClick={this.findSessions}>
Find all sessions with this error
<Icon className="ml-1" name="next1" color="teal" />
</Button>
</div>
<Divider />
<div className="m-4">
<Loader loading={traceLoading}>
<ErrorDetails
name={error.name}
message={error.message}
errorStack={trace}
error={error}
sourcemapUploaded={sourcemapUploaded}
/>
</Loader>
</div>
</div>
);
}
}

View file

@ -64,7 +64,7 @@ export default class Exceptions extends React.PureComponent {
show={ !loading && errorStack.size === 0 }
title="Nothing found!"
>
<ErrorDetails error={ currentError.name } errorStack={errorStack} sourcemapUploaded={sourcemapUploaded} />
<ErrorDetails error={ currentError } errorStack={errorStack} sourcemapUploaded={sourcemapUploaded} />
</NoContent>
</Loader>
</div>

View file

@ -36,10 +36,10 @@ function OverviewPanel(props: Props) {
};
return (
<BottomBlock style={{ height: '260px' }}>
<BottomBlock style={{ height: '263px' }}>
<BottomBlock.Header>
<span className="font-semibold color-gray-medium mr-4">Overview</span>
<div className="flex items-center">
<div className="flex items-center h-20">
<FeatureSelection list={selectedFeatures} updateList={setSelectedFeatures} />
</div>
</BottomBlock.Header>

View file

@ -10,7 +10,7 @@ interface Props {
endTime?: number;
renderElement?: (item: any) => React.ReactNode;
}
function EventRow(props: Props) {
const EventRow = React.memo((props: Props) => {
const { title, className, list = [], endTime = 0 } = props;
const scale = 100 / endTime;
const _list = React.useMemo(() => {
@ -22,8 +22,8 @@ function EventRow(props: Props) {
})
}, [list]);
return (
<div className={cn('h-20 w-full flex flex-col py-2 px-4', className)}>
<div className="uppercase color-gray-medium">{title}</div>
<div className={cn('w-full flex flex-col py-2', className)} style={{ height: '66px'}}>
<div className="uppercase color-gray-medium ml-4">{title}</div>
<div className="relative w-full py-3">
{_list.map((item: any, index: number) => {
return (
@ -36,7 +36,7 @@ function EventRow(props: Props) {
</div>
</div>
);
}
});
export default connectPlayer((state: any) => ({
endTime: state.endTime,

View file

@ -2,16 +2,17 @@ import React from 'react';
import { connectPlayer, Controls } from 'App/player';
import { toggleBottomBlock, NETWORK, EXCEPTIONS, PERFORMANCE } from 'Duck/components/player';
import { useModal } from 'App/components/Modal';
import { Icon, ErrorDetails } from 'UI';
import { Icon, ErrorDetails, Popup } from 'UI';
import { Tooltip } from 'react-tippy';
import { TYPES as EVENT_TYPES } from 'Types/session/event';
import StackEventModal from '../StackEventModal';
import ErrorDetailsModal from 'App/components/Dashboard/components/Errors/ErrorDetailsModal';
interface Props {
pointer: any;
type: any;
}
function TimelinePointer(props: Props) {
const TimelinePointer = React.memo((props: Props) => {
const { showModal, hideModal } = useModal();
const createEventClickHandler = (pointer: any, type: any) => (e: any) => {
e.stopPropagation();
@ -20,8 +21,8 @@ function TimelinePointer(props: Props) {
return;
}
if (type === 'EXCEPTIONS') {
showModal(<ErrorDetails error={pointer} />, { right: true });
if (type === 'ERRORS') {
showModal(<ErrorDetailsModal errorId={pointer.errorId} />, { right: true });
}
if (type === 'EVENT') {
@ -32,8 +33,8 @@ function TimelinePointer(props: Props) {
const renderNetworkElement = (item: any) => {
return (
<Tooltip
html={
<Popup
content={
<div className="">
<b>{item.success ? 'Slow resource: ' : 'Missing resource:'}</b>
<br />
@ -46,14 +47,14 @@ function TimelinePointer(props: Props) {
<div onClick={createEventClickHandler(item, NETWORK)} className="cursor-pointer">
<div className="h-2 w-2 rounded-full bg-red" />
</div>
</Tooltip>
</Popup>
);
};
const renderClickRageElement = (item: any) => {
return (
<Tooltip
html={
<Popup
content={
<div className="">
<b>{'Click Rage'}</b>
</div>
@ -64,14 +65,14 @@ function TimelinePointer(props: Props) {
<div onClick={createEventClickHandler(item, null)} className="cursor-pointer">
<Icon className="bg-white" name="funnel/emoji-angry" color="red" size="16" />
</div>
</Tooltip>
</Popup>
);
};
const renderStackEventElement = (item: any) => {
return (
<Tooltip
html={
<Popup
content={
<div className="">
<b>{'Stack Event'}</b>
</div>
@ -82,15 +83,14 @@ function TimelinePointer(props: Props) {
<div onClick={createEventClickHandler(item, 'EVENT')} className="cursor-pointer w-1 h-4 bg-red">
{/* <Icon className="rounded-full bg-white" name="funnel/exclamation-circle-fill" color="red" size="16" /> */}
</div>
</Tooltip>
</Popup>
);
};
const renderPerformanceElement = (item: any) => {
console.log('item', item)
return (
<Tooltip
html={
<Popup
content={
<div className="">
<b>{item.name}</b>
</div>
@ -101,14 +101,14 @@ function TimelinePointer(props: Props) {
<div onClick={createEventClickHandler(item, EXCEPTIONS)} className="cursor-pointer w-1 h-4 bg-red">
{/* <Icon className="rounded-full bg-white" name="funnel/exclamation-circle-fill" color="red" size="16" /> */}
</div>
</Tooltip>
</Popup>
);
};
const renderExceptionElement = (item: any) => {
return (
<Tooltip
html={
<Popup
content={
<div className="">
<b>{'Exception'}</b>
<br />
@ -118,10 +118,10 @@ function TimelinePointer(props: Props) {
delay={0}
position="top"
>
<div onClick={createEventClickHandler(item, EXCEPTIONS)} className="cursor-pointer">
<div onClick={createEventClickHandler(item, 'ERRORS')} className="cursor-pointer">
<Icon className="rounded-full bg-white" name="funnel/exclamation-circle-fill" color="red" size="16" />
</div>
</Tooltip>
</Popup>
);
};
@ -145,6 +145,6 @@ function TimelinePointer(props: Props) {
}
};
return <div>{render()}</div>;
}
});
export default TimelinePointer;

View file

@ -12,7 +12,7 @@ import CustomDragLayer from './CustomDragLayer';
import { debounce } from 'App/utils';
import { Tooltip } from 'react-tippy';
const BOUNDRY = 15;
const BOUNDRY = 0;
function getTimelinePosition(value, scale) {
const pos = value * scale;

View file

@ -1,7 +1,7 @@
import React, { useEffect, useState } from 'react';
import ErrorFrame from '../ErrorFrame/ErrorFrame';
import { fetchErrorStackList } from 'Duck/sessions';
import { IconButton, Icon } from 'UI';
import { Button, Icon } from 'UI';
import { connect } from 'react-redux';
const docLink = 'https://docs.openreplay.com/installation/upload-sourcemaps';
@ -46,8 +46,12 @@ function ErrorDetails(props: Props) {
<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" />
<Button variant={!showRaw ? 'text-primary' : 'text'} onClick={() => setShowRaw(false)}>
FULL
</Button>
<Button variant={showRaw ? 'text-primary' : 'text'} onClick={() => setShowRaw(true)}>
RAW
</Button>
</div>
</div>
<div className="mb-6 code-font" data-hidden={showRaw}>
@ -59,7 +63,6 @@ function ErrorDetails(props: Props) {
{error.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} />