remove errors reducer, drop old components
This commit is contained in:
parent
3f9b485be6
commit
29576a775c
25 changed files with 399 additions and 1233 deletions
|
|
@ -1,21 +0,0 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import ErrorListItem from '../ErrorListItem';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { useObserver } from 'mobx-react-lite';
|
||||
|
||||
function ErrorsList(props) {
|
||||
const { errorStore, metricStore } = useStore();
|
||||
const metric = useObserver(() => metricStore.instance);
|
||||
|
||||
useEffect(() => {
|
||||
errorStore.fetchErrors();
|
||||
}, []);
|
||||
return (
|
||||
<div>
|
||||
Errors List
|
||||
<ErrorListItem error={{}} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ErrorsList;
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './ErrorsList';
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
import React from 'react';
|
||||
import ErrorsList from '../ErrorsList';
|
||||
|
||||
function ErrorsWidget(props) {
|
||||
return (
|
||||
<div>
|
||||
<ErrorsList />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ErrorsWidget;
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './ErrorsWidget';
|
||||
|
|
@ -1,89 +1,48 @@
|
|||
import { observer } from 'mobx-react-lite';
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import withSiteIdRouter from 'HOCs/withSiteIdRouter';
|
||||
import { error as errorRoute } from 'App/routes';
|
||||
import { NoContent, Loader } from 'UI';
|
||||
import { fetch, fetchTrace } from 'Duck/errors';
|
||||
import MainSection from './MainSection';
|
||||
import SideSection from './SideSection';
|
||||
|
||||
import { useStore } from 'App/mstore';
|
||||
import { Loader, NoContent } from 'UI';
|
||||
|
||||
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
|
||||
|
||||
@connect(
|
||||
(state) => ({
|
||||
errorIdInStore: state.getIn(['errors', 'instance']).errorId,
|
||||
list: state.getIn(['errors', 'instanceTrace']),
|
||||
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(prevProps) {
|
||||
if (prevProps.errorId !== this.props.errorId || prevProps.errorIdInStore !== this.props.errorIdInStore) {
|
||||
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;
|
||||
import MainSection from './MainSection';
|
||||
import SideSection from './SideSection';
|
||||
|
||||
let nextDisabled = true,
|
||||
prevDisabled = true;
|
||||
if (list.size > 0) {
|
||||
nextDisabled = loading || list.last().errorId === errorId;
|
||||
prevDisabled = loading || list.first().errorId === errorId;
|
||||
}
|
||||
function ErrorInfo(props) {
|
||||
const { errorStore } = useStore();
|
||||
|
||||
return (
|
||||
<NoContent
|
||||
title={
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<AnimatedSVG name={ICONS.EMPTY_STATE} size="170" />
|
||||
<div className="mt-4">No Error Found!</div>
|
||||
</div>
|
||||
}
|
||||
subtext="Please try to find existing one."
|
||||
show={!loading && errorIdInStore == null}
|
||||
>
|
||||
<div className="flex w-full">
|
||||
<Loader loading={loading} className="w-full">
|
||||
<MainSection className="w-9/12" />
|
||||
<SideSection className="w-3/12" />
|
||||
</Loader>
|
||||
const ensureInstance = () => {
|
||||
if (errorStore.isLoading) return;
|
||||
errorStore.fetch(props.errorId);
|
||||
errorStore.fetchTrace(props.errorId);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
ensureInstance();
|
||||
}, [props.errorId]);
|
||||
|
||||
const errorIdInStore = errorStore.instance?.errorId;
|
||||
const loading = errorStore.isLoading;
|
||||
return (
|
||||
<NoContent
|
||||
title={
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<AnimatedSVG name={ICONS.EMPTY_STATE} size="170" />
|
||||
<div className="mt-4">No Error Found!</div>
|
||||
</div>
|
||||
</NoContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
subtext="Please try to find existing one."
|
||||
show={!loading && errorIdInStore == null}
|
||||
>
|
||||
<div className="flex w-full">
|
||||
<Loader loading={loading} className="w-full">
|
||||
<MainSection className="w-9/12" />
|
||||
<SideSection className="w-3/12" />
|
||||
</Loader>
|
||||
</div>
|
||||
</NoContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(ErrorInfo);
|
||||
|
|
|
|||
|
|
@ -1,154 +1,134 @@
|
|||
import { RESOLVED } from 'Types/errorInfo';
|
||||
import { FilterKey } from 'Types/filter/filterType';
|
||||
import cn from 'classnames';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import cn from 'classnames';
|
||||
import withSiteIdRouter from 'HOCs/withSiteIdRouter';
|
||||
import { ErrorDetails, Icon, Loader, Button } from 'UI';
|
||||
import { sessions as sessionsRoute } from 'App/routes';
|
||||
import { RESOLVED } from 'Types/errorInfo';
|
||||
import { addFilterByKeyAndValue } from 'Duck/search';
|
||||
import { resolve, unresolve, ignore, toggleFavorite } from 'Duck/errors';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
|
||||
import { resentOrDate } from 'App/date';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { sessions as sessionsRoute } from 'App/routes';
|
||||
import Divider from 'Components/Errors/ui/Divider';
|
||||
import ErrorName from 'Components/Errors/ui/ErrorName';
|
||||
import Label from 'Components/Errors/ui/Label';
|
||||
import { FilterKey } from 'Types/filter/filterType';
|
||||
import { addFilterByKeyAndValue } from 'Duck/search';
|
||||
import { Button, ErrorDetails, Icon, Loader } from 'UI';
|
||||
|
||||
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,
|
||||
}
|
||||
)
|
||||
export default class MainSection extends React.PureComponent {
|
||||
resolve = () => {
|
||||
const { error } = this.props;
|
||||
this.props.resolve(error.errorId);
|
||||
};
|
||||
function MainSection(props) {
|
||||
const { errorStore } = useStore();
|
||||
const error = errorStore.instance;
|
||||
const trace = errorStore.instanceTrace;
|
||||
const sourcemapUploaded = errorStore.sourcemapUploaded;
|
||||
const loading = errorStore.isLoading;
|
||||
const addFilterByKeyAndValue = props.addFilterByKeyAndValue;
|
||||
const className = props.className;
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
findSessions = () => {
|
||||
this.props.addFilterByKeyAndValue(FilterKey.ERROR, this.props.error.message);
|
||||
const findSessions = () => {
|
||||
addFilterByKeyAndValue(FilterKey.ERROR, error.message);
|
||||
this.props.history.push(sessionsRoute());
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
error,
|
||||
trace,
|
||||
sourcemapUploaded,
|
||||
ignoreLoading,
|
||||
resolveToggleLoading,
|
||||
toggleFavoriteLoading,
|
||||
className,
|
||||
traceLoading,
|
||||
} = this.props;
|
||||
const isPlayer = window.location.pathname.includes('/session/');
|
||||
|
||||
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 flex-col">
|
||||
<div
|
||||
className="flex items-center color-gray-dark font-semibold"
|
||||
style={{ wordBreak: 'break-all' }}
|
||||
>
|
||||
{error.message}
|
||||
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 flex-col">
|
||||
<div
|
||||
className="flex items-center color-gray-dark font-semibold"
|
||||
style={{ wordBreak: 'break-all' }}
|
||||
>
|
||||
{error.message}
|
||||
</div>
|
||||
<div className="flex items-center mt-2">
|
||||
<div className="flex">
|
||||
<Label
|
||||
topValue={error.sessions}
|
||||
horizontal
|
||||
topValueSize="text-lg"
|
||||
bottomValue="Sessions"
|
||||
/>
|
||||
<Label
|
||||
topValue={error.users}
|
||||
horizontal
|
||||
topValueSize="text-lg"
|
||||
bottomValue="Users"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center mt-2">
|
||||
<div className="flex">
|
||||
<Label
|
||||
topValue={error.sessions}
|
||||
horizontal
|
||||
topValueSize="text-lg"
|
||||
bottomValue="Sessions"
|
||||
/>
|
||||
<Label
|
||||
topValue={error.users}
|
||||
horizontal
|
||||
topValueSize="text-lg"
|
||||
bottomValue="Users"
|
||||
/>
|
||||
</div>
|
||||
<div className="text-xs color-gray-medium">Over the past 30 days</div>
|
||||
<div className="text-xs color-gray-medium">
|
||||
Over the past 30 days
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
<div className="m-4">
|
||||
<div className="flex items-center">
|
||||
<h3 className="text-xl inline-block mr-2">Last session with this error</h3>
|
||||
<span className="font-thin text-sm">{resentOrDate(error.lastOccurrence)}</span>
|
||||
<Button className="ml-auto" variant="text-primary" onClick={this.findSessions}>
|
||||
Find all sessions with this error
|
||||
<Icon className="ml-1" name="next1" color="teal" />
|
||||
</Button>
|
||||
</div>
|
||||
<SessionBar className="my-4" session={error.lastHydratedSession} />
|
||||
{error.customTags.length > 0 ? (
|
||||
<div className="flex items-start flex-col">
|
||||
<div>
|
||||
<span className="font-semibold">More Info</span> <span className="text-disabled-text">(most recent call)</span>
|
||||
</div>
|
||||
<div className="mt-4 flex items-center gap-3 w-full flex-wrap">
|
||||
{error.customTags.map((tag) => (
|
||||
<div className="flex items-center rounded overflow-hidden bg-gray-lightest">
|
||||
<div className="bg-gray-light-shade py-1 px-2 text-disabled-text">{Object.entries(tag)[0][0]}</div> <div className="py-1 px-2 text-gray-dark">{Object.entries(tag)[0][1]}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
<Divider />
|
||||
<div className="m-4">
|
||||
<div className="flex items-center">
|
||||
<h3 className="text-xl inline-block mr-2">
|
||||
Last session with this error
|
||||
</h3>
|
||||
<span className="font-thin text-sm">
|
||||
{resentOrDate(error.lastOccurrence)}
|
||||
</span>
|
||||
<Button
|
||||
className="ml-auto"
|
||||
variant="text-primary"
|
||||
onClick={findSessions}
|
||||
>
|
||||
Find all sessions with this error
|
||||
<Icon className="ml-1" name="next1" color="teal" />
|
||||
</Button>
|
||||
</div>
|
||||
<SessionBar className="my-4" session={error.lastHydratedSession} />
|
||||
{error.customTags.length > 0 ? (
|
||||
<div className="flex items-start flex-col">
|
||||
<div>
|
||||
<span className="font-semibold">More Info</span>{' '}
|
||||
<span className="text-disabled-text">(most recent call)</span>
|
||||
</div>
|
||||
<div className="mt-4 flex items-center gap-3 w-full flex-wrap">
|
||||
{error.customTags.map((tag) => (
|
||||
<div className="flex items-center rounded overflow-hidden bg-gray-lightest">
|
||||
<div className="bg-gray-light-shade py-1 px-2 text-disabled-text">
|
||||
{Object.entries(tag)[0][0]}
|
||||
</div>{' '}
|
||||
<div className="py-1 px-2 text-gray-dark">
|
||||
{Object.entries(tag)[0][1]}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<Divider />
|
||||
<div className="m-4">
|
||||
<Loader loading={loading}>
|
||||
<ErrorDetails
|
||||
name={error.name}
|
||||
message={error.message}
|
||||
errorStack={trace}
|
||||
error={error}
|
||||
sourcemapUploaded={sourcemapUploaded}
|
||||
/>
|
||||
</Loader>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default withRouter(
|
||||
connect(null, { addFilterByKeyAndValue })(observer(MainSection))
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,154 +0,0 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import withSiteIdRouter from 'HOCs/withSiteIdRouter';
|
||||
import withPermissions from 'HOCs/withPermissions'
|
||||
import { UNRESOLVED, RESOLVED, IGNORED, BOOKMARK } from "Types/errorInfo";
|
||||
import { fetchBookmarks, editOptions } from "Duck/errors";
|
||||
import { applyFilter } from 'Duck/search';
|
||||
import { errors as errorsRoute, isRoute } from "App/routes";
|
||||
import withPageTitle from 'HOCs/withPageTitle';
|
||||
import cn from 'classnames';
|
||||
import SelectDateRange from 'Shared/SelectDateRange';
|
||||
import Period from 'Types/app/period';
|
||||
|
||||
import List from './List/List';
|
||||
import ErrorInfo from './Error/ErrorInfo';
|
||||
import Header from './Header';
|
||||
import SideMenuSection from './SideMenu/SideMenuSection';
|
||||
import SideMenuDividedItem from './SideMenu/SideMenuDividedItem';
|
||||
|
||||
const ERRORS_ROUTE = errorsRoute();
|
||||
|
||||
function getStatusLabel(status) {
|
||||
switch(status) {
|
||||
case UNRESOLVED:
|
||||
return "Unresolved";
|
||||
case RESOLVED:
|
||||
return "Resolved";
|
||||
case IGNORED:
|
||||
return "Ignored";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
@withPermissions(['ERRORS'], 'page-margin container-90')
|
||||
@withSiteIdRouter
|
||||
@connect(state => ({
|
||||
list: state.getIn([ "errors", "list" ]),
|
||||
status: state.getIn([ "errors", "options", "status" ]),
|
||||
filter: state.getIn([ 'search', 'instance' ]),
|
||||
}), {
|
||||
fetchBookmarks,
|
||||
applyFilter,
|
||||
editOptions,
|
||||
})
|
||||
@withPageTitle("Errors - OpenReplay")
|
||||
export default class Errors extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
filter: '',
|
||||
}
|
||||
}
|
||||
|
||||
ensureErrorsPage() {
|
||||
const { history } = this.props;
|
||||
if (!isRoute(ERRORS_ROUTE, history.location.pathname)) {
|
||||
history.push(ERRORS_ROUTE);
|
||||
}
|
||||
}
|
||||
|
||||
onStatusItemClick = ({ key }) => {
|
||||
this.props.editOptions({ status: key });
|
||||
}
|
||||
|
||||
onBookmarksClick = () => {
|
||||
this.props.editOptions({ status: BOOKMARK });
|
||||
}
|
||||
|
||||
onDateChange = (e) => {
|
||||
const dateValues = e.toJSON();
|
||||
this.props.applyFilter(dateValues);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
count,
|
||||
match: {
|
||||
params: { errorId }
|
||||
},
|
||||
status,
|
||||
list,
|
||||
history,
|
||||
filter,
|
||||
} = this.props;
|
||||
|
||||
const { startDate, endDate, rangeValue } = filter;
|
||||
const period = new Period({ start: startDate, end: endDate, rangeName: rangeValue });
|
||||
|
||||
return (
|
||||
<div className="page-margin container-90" >
|
||||
<div className={cn("side-menu", {'disabled' : !isRoute(ERRORS_ROUTE, history.location.pathname)})}>
|
||||
<SideMenuSection
|
||||
title="Errors"
|
||||
onItemClick={this.onStatusItemClick}
|
||||
items={[
|
||||
{
|
||||
key: UNRESOLVED,
|
||||
icon: "exclamation-circle",
|
||||
label: getStatusLabel(UNRESOLVED),
|
||||
active: status === UNRESOLVED,
|
||||
},
|
||||
{
|
||||
key: RESOLVED,
|
||||
icon: "check",
|
||||
label: getStatusLabel(RESOLVED),
|
||||
active: status === RESOLVED,
|
||||
},
|
||||
{
|
||||
key: IGNORED,
|
||||
icon: "ban",
|
||||
label: getStatusLabel(IGNORED),
|
||||
active: status === IGNORED,
|
||||
}
|
||||
]}
|
||||
/>
|
||||
<SideMenuDividedItem
|
||||
className="mt-3 mb-4"
|
||||
iconName="star"
|
||||
title="Bookmarks"
|
||||
active={ status === BOOKMARK }
|
||||
onClick={ this.onBookmarksClick }
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="side-menu-margined">
|
||||
{ errorId == null ?
|
||||
<>
|
||||
<div className="mb-5 flex items-baseline">
|
||||
<Header
|
||||
text={ status === BOOKMARK ? "Bookmarks" : getStatusLabel(status) }
|
||||
count={ list.size }
|
||||
/>
|
||||
<div className="ml-3 flex items-center">
|
||||
<span className="mr-2 color-gray-medium">Seen in</span>
|
||||
<SelectDateRange
|
||||
period={period}
|
||||
onChange={this.onDateChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<List
|
||||
status={ status }
|
||||
list={ list }
|
||||
/>
|
||||
</>
|
||||
:
|
||||
<ErrorInfo errorId={ errorId } list={ list } />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
import React from 'react';
|
||||
function Header({ text, count }) {
|
||||
return (
|
||||
<h3 className="text-2xl capitalize">
|
||||
<span>{ text }</span>
|
||||
{ count != null && <span className="ml-2 font-normal color-gray-medium">{ count }</span> }
|
||||
</h3>
|
||||
);
|
||||
}
|
||||
|
||||
Header.displayName = "Header";
|
||||
|
||||
export default Header;
|
||||
|
||||
|
|
@ -1,259 +0,0 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Set } from "immutable";
|
||||
import { NoContent, Loader, Checkbox, IconButton, Input, Pagination } from 'UI';
|
||||
import { merge, resolve, unresolve, ignore, updateCurrentPage, editOptions } from "Duck/errors";
|
||||
import { applyFilter } from 'Duck/filters';
|
||||
import { IGNORED, UNRESOLVED } from 'Types/errorInfo';
|
||||
import Divider from 'Components/Errors/ui/Divider';
|
||||
import ListItem from './ListItem/ListItem';
|
||||
import { debounce } from 'App/utils';
|
||||
import Select from 'Shared/Select';
|
||||
import EmptyStateSvg from '../../../svg/no-results.svg';
|
||||
|
||||
const sortOptionsMap = {
|
||||
'occurrence-desc': 'Last Occurrence',
|
||||
'occurrence-desc': 'First Occurrence',
|
||||
'sessions-asc': 'Sessions Ascending',
|
||||
'sessions-desc': 'Sessions Descending',
|
||||
'users-asc': 'Users Ascending',
|
||||
'users-desc': 'Users Descending',
|
||||
};
|
||||
const sortOptions = Object.entries(sortOptionsMap)
|
||||
.map(([ value, label ]) => ({ value, label }));
|
||||
|
||||
@connect(state => ({
|
||||
loading: state.getIn([ "errors", "loading" ]),
|
||||
resolveToggleLoading: state.getIn(["errors", "resolve", "loading"]) ||
|
||||
state.getIn(["errors", "unresolve", "loading"]),
|
||||
ignoreLoading: state.getIn([ "errors", "ignore", "loading" ]),
|
||||
mergeLoading: state.getIn([ "errors", "merge", "loading" ]),
|
||||
currentPage: state.getIn(["errors", "currentPage"]),
|
||||
limit: state.getIn(["errors", "limit"]),
|
||||
total: state.getIn([ 'errors', 'totalCount' ]),
|
||||
sort: state.getIn([ 'errors', 'options', 'sort' ]),
|
||||
order: state.getIn([ 'errors', 'options', 'order' ]),
|
||||
query: state.getIn([ "errors", "options", "query" ]),
|
||||
}), {
|
||||
merge,
|
||||
resolve,
|
||||
unresolve,
|
||||
ignore,
|
||||
applyFilter,
|
||||
updateCurrentPage,
|
||||
editOptions,
|
||||
})
|
||||
export default class List extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
checkedAll: false,
|
||||
checkedIds: Set(),
|
||||
query: props.query,
|
||||
}
|
||||
this.debounceFetch = debounce(this.props.editOptions, 1000);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.applyFilter({ });
|
||||
}
|
||||
|
||||
check = ({ errorId }) => {
|
||||
const { checkedIds } = this.state;
|
||||
const newCheckedIds = checkedIds.contains(errorId)
|
||||
? checkedIds.remove(errorId)
|
||||
: checkedIds.add(errorId);
|
||||
this.setState({
|
||||
checkedAll: newCheckedIds.size === this.props.list.size,
|
||||
checkedIds: newCheckedIds
|
||||
});
|
||||
}
|
||||
|
||||
checkAll = () => {
|
||||
if (this.state.checkedAll) {
|
||||
this.setState({
|
||||
checkedAll: false,
|
||||
checkedIds: Set(),
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
checkedAll: true,
|
||||
checkedIds: this.props.list.map(({ errorId }) => errorId).toSet(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
resetChecked = () => {
|
||||
this.setState({
|
||||
checkedAll: false,
|
||||
checkedIds: Set(),
|
||||
});
|
||||
}
|
||||
|
||||
currentCheckedIds() {
|
||||
return this.state.checkedIds
|
||||
.intersect(this.props.list.map(({ errorId }) => errorId).toSet());
|
||||
}
|
||||
|
||||
merge = () => {
|
||||
this.props.merge(currentCheckedIds().toJS()).then(this.resetChecked);
|
||||
}
|
||||
|
||||
applyToAllChecked(f) {
|
||||
return Promise.all(this.currentCheckedIds().map(f).toJS()).then(this.resetChecked);
|
||||
}
|
||||
|
||||
resolve = () => {
|
||||
this.applyToAllChecked(this.props.resolve);
|
||||
}
|
||||
|
||||
unresolve = () => {
|
||||
this.applyToAllChecked(this.props.unresolve);
|
||||
}
|
||||
|
||||
ignore = () => {
|
||||
this.applyToAllChecked(this.props.ignore);
|
||||
}
|
||||
|
||||
addPage = () => this.props.updateCurrentPage(this.props.currentPage + 1)
|
||||
|
||||
writeOption = ({ name, value }) => {
|
||||
const [ sort, order ] = value.split('-');
|
||||
if (name === 'sort') {
|
||||
this.props.editOptions({ sort, order });
|
||||
}
|
||||
}
|
||||
|
||||
// onQueryChange = ({ target: { value, name } }) => props.edit({ [ name ]: value })
|
||||
|
||||
onQueryChange = ({ target: { value, name } }) => {
|
||||
this.setState({ query: value });
|
||||
this.debounceFetch({ query: value });
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
list,
|
||||
status,
|
||||
loading,
|
||||
ignoreLoading,
|
||||
resolveToggleLoading,
|
||||
mergeLoading,
|
||||
currentPage,
|
||||
total,
|
||||
sort,
|
||||
order,
|
||||
limit,
|
||||
} = this.props;
|
||||
const {
|
||||
checkedAll,
|
||||
checkedIds,
|
||||
query,
|
||||
} = this.state;
|
||||
const someLoading = loading || ignoreLoading || resolveToggleLoading || mergeLoading;
|
||||
const currentCheckedIds = this.currentCheckedIds();
|
||||
|
||||
return (
|
||||
<div className="bg-white p-5 border-radius-3 thin-gray-border">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center" style={{ height: "36px" }}>
|
||||
<Checkbox
|
||||
className="mr-3"
|
||||
checked={ checkedAll }
|
||||
onChange={ this.checkAll }
|
||||
/>
|
||||
{ status === UNRESOLVED
|
||||
? <IconButton
|
||||
outline
|
||||
className="mr-3"
|
||||
label="Resolve"
|
||||
icon="check"
|
||||
size="small"
|
||||
loading={ resolveToggleLoading }
|
||||
onClick={ this.resolve }
|
||||
disabled={ someLoading || currentCheckedIds.size === 0}
|
||||
/>
|
||||
: <IconButton
|
||||
outline
|
||||
className="mr-3"
|
||||
label="Unresolve"
|
||||
icon="exclamation-circle"
|
||||
size="small"
|
||||
loading={ resolveToggleLoading }
|
||||
onClick={ this.unresolve }
|
||||
disabled={ someLoading || currentCheckedIds.size === 0}
|
||||
/>
|
||||
}
|
||||
{ status !== IGNORED &&
|
||||
<IconButton
|
||||
outline
|
||||
className="mr-3"
|
||||
label="Ignore"
|
||||
icon="ban"
|
||||
size="small"
|
||||
loading={ ignoreLoading }
|
||||
onClick={ this.ignore }
|
||||
disabled={ someLoading || currentCheckedIds.size === 0}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
<div className="flex items-center ml-6">
|
||||
<span className="mr-2 color-gray-medium">Sort By</span>
|
||||
<Select
|
||||
defaultValue={ `${sort}-${order}` }
|
||||
name="sort"
|
||||
plain
|
||||
options={ sortOptions }
|
||||
onChange={ this.writeOption }
|
||||
/>
|
||||
<Input
|
||||
style={{ width: '350px'}}
|
||||
wrapperClassName="ml-3"
|
||||
placeholder="Filter by name or message"
|
||||
icon="search"
|
||||
name="filter"
|
||||
onChange={ this.onQueryChange }
|
||||
value={query}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Divider />
|
||||
<NoContent
|
||||
title={
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<object style={{ width: "180px"}} type="image/svg+xml" data={EmptyStateSvg} />
|
||||
<span className="mr-2">No Errors Found!</span>
|
||||
</div>
|
||||
}
|
||||
subtext="Please try to change your search parameters."
|
||||
// animatedIcon="empty-state"
|
||||
show={ !loading && list.size === 0}
|
||||
>
|
||||
<Loader loading={ loading }>
|
||||
{ list.map(e =>
|
||||
<div key={e.errorId} style={{ opacity: e.disabled ? 0.5 : 1}}>
|
||||
<ListItem
|
||||
disabled={someLoading || e.disabled}
|
||||
key={e.errorId}
|
||||
error={e}
|
||||
checked={ checkedIds.contains(e.errorId) }
|
||||
onCheck={ this.check }
|
||||
/>
|
||||
<Divider/>
|
||||
</div>
|
||||
)}
|
||||
<div className="w-full flex items-center justify-center mt-4">
|
||||
<Pagination
|
||||
page={currentPage}
|
||||
total={total}
|
||||
onPageChange={(page) => this.props.updateCurrentPage(page)}
|
||||
limit={limit}
|
||||
debounceRequest={500}
|
||||
/>
|
||||
</div>
|
||||
</Loader>
|
||||
</NoContent>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,87 +0,0 @@
|
|||
import React from 'react';
|
||||
import { BarChart, Bar, YAxis, Tooltip, XAxis } from 'recharts';
|
||||
import cn from 'classnames';
|
||||
import { DateTime } from 'luxon'
|
||||
import { diffFromNowString } from 'App/date';
|
||||
import { error as errorRoute } from 'App/routes';
|
||||
import { IGNORED, RESOLVED } from 'Types/errorInfo';
|
||||
import { Checkbox, Link } from 'UI';
|
||||
import ErrorName from 'Components/Errors/ui/ErrorName';
|
||||
import Label from 'Components/Errors/ui/Label';
|
||||
import stl from './listItem.module.css';
|
||||
import { Styles } from '../../../Dashboard/Widgets/common';
|
||||
|
||||
const CustomTooltip = ({ active, payload, label }) => {
|
||||
if (active) {
|
||||
const p = payload[0].payload;
|
||||
const dateStr = p.timestamp ? DateTime.fromMillis(p.timestamp).toFormat('l') : ''
|
||||
return (
|
||||
<div className="rounded border bg-white p-2">
|
||||
<p className="label text-sm color-gray-medium">{dateStr}</p>
|
||||
<p className="text-sm">Sessions: {p.count}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
function ListItem({ className, onCheck, checked, error, disabled }) {
|
||||
|
||||
const getDateFormat = val => {
|
||||
const d = new Date(val);
|
||||
return (d.getMonth()+ 1) + '/' + d.getDate()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={ cn("flex justify-between cursor-pointer py-4", className) } id="error-item">
|
||||
<Checkbox
|
||||
disabled={disabled}
|
||||
checked={ checked }
|
||||
onChange={ () => onCheck(error) }
|
||||
/>
|
||||
|
||||
<div className={ cn("ml-3 flex-1 leading-tight", stl.name) } >
|
||||
<Link to={errorRoute(error.errorId)} >
|
||||
<ErrorName
|
||||
icon={error.status === IGNORED ? 'ban' : null }
|
||||
lineThrough={error.status === RESOLVED}
|
||||
name={ error.name }
|
||||
message={ error.stack0InfoString }
|
||||
bold={ !error.viewed }
|
||||
/>
|
||||
<div
|
||||
className={ cn("truncate color-gray-medium", { "line-through" : error.status === RESOLVED}) }
|
||||
>
|
||||
{ error.message }
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
<BarChart width={ 150 } height={ 40 } data={ error.chart }>
|
||||
<XAxis hide dataKey="timestamp" />
|
||||
<YAxis hide domain={[0, 'dataMax + 8']} />
|
||||
<Tooltip {...Styles.tooltip} label="Sessions" content={<CustomTooltip />} />
|
||||
<Bar name="Sessions" minPointSize={1} dataKey="count" fill="#A8E0DA" />
|
||||
</BarChart>
|
||||
<Label
|
||||
className={stl.sessions}
|
||||
topValue={ error.sessions }
|
||||
bottomValue="Sessions"
|
||||
/>
|
||||
<Label
|
||||
className={stl.users}
|
||||
topValue={ error.users }
|
||||
bottomValue="Users"
|
||||
/>
|
||||
<Label
|
||||
className={stl.occurrence}
|
||||
topValue={ `${diffFromNowString(error.lastOccurrence)} ago` }
|
||||
bottomValue="Last Seen"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
ListItem.displayName = "ListItem";
|
||||
export default ListItem;
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
.name {
|
||||
min-width: 55%;
|
||||
}
|
||||
|
||||
.sessions {
|
||||
width: 6%;
|
||||
}
|
||||
|
||||
.users {
|
||||
width: 5%;
|
||||
}
|
||||
|
||||
.occurrence {
|
||||
width: 15%;
|
||||
min-width: 152px;
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
import React from 'react';
|
||||
import { SideMenuitem } from "UI";
|
||||
import Divider from 'Components/Errors/ui/Divider';
|
||||
function SideMenuDividedItem({ className, noTopDivider = false, noBottomDivider = false, ...props }) {
|
||||
return (
|
||||
<div className={className}>
|
||||
{ !noTopDivider && <Divider /> }
|
||||
<SideMenuitem
|
||||
className="my-3"
|
||||
{ ...props }
|
||||
/>
|
||||
{ !noBottomDivider && <Divider /> }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
SideMenuDividedItem.displayName = "SideMenuDividedItem";
|
||||
|
||||
export default SideMenuDividedItem;
|
||||
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import stl from './sideMenuHeader.module.css';
|
||||
|
||||
function SideMenuHeader({ text, className }) {
|
||||
return (
|
||||
<div className={ cn(className, stl.label, "uppercase color-gray") }>
|
||||
{ text }
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
SideMenuHeader.displayName = "SideMenuHeader";
|
||||
export default SideMenuHeader;
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
import React from 'react';
|
||||
import { SideMenuitem } from 'UI';
|
||||
import SideMenuHeader from './SideMenuHeader';
|
||||
|
||||
function SideMenuSection({ title, items, onItemClick }) {
|
||||
return (
|
||||
<>
|
||||
<SideMenuHeader className="mb-4" text={ title }/>
|
||||
{ items.map(item =>
|
||||
<SideMenuitem
|
||||
key={ item.key }
|
||||
active={ item.active }
|
||||
title={ item.label }
|
||||
iconName={ item.icon }
|
||||
onClick={() => onItemClick(item)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
SideMenuSection.displayName = "SideMenuSection";
|
||||
|
||||
export default SideMenuSection;
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
.label {
|
||||
letter-spacing: 0.2em;
|
||||
color: gray;
|
||||
}
|
||||
|
|
@ -1,34 +1,40 @@
|
|||
import React, { useEffect } from 'react'
|
||||
import { fetchNewErrorsCount } from 'Duck/errors'
|
||||
import { connect } from 'react-redux'
|
||||
import stl from './errorsBadge.module.css'
|
||||
import {
|
||||
getDateRangeFromValue,
|
||||
DATE_RANGE_VALUES,
|
||||
} from 'App/dateRange';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import React, { useEffect } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { DATE_RANGE_VALUES, getDateRangeFromValue } from 'App/dateRange';
|
||||
import { useStore } from 'App/mstore';
|
||||
|
||||
import stl from './errorsBadge.module.css';
|
||||
|
||||
const AUTOREFRESH_INTERVAL = 5 * 60 * 1000;
|
||||
const weekRange = getDateRangeFromValue(DATE_RANGE_VALUES.LAST_7_DAYS);
|
||||
let intervalId = null
|
||||
let intervalId = null;
|
||||
|
||||
function ErrorsBadge({ errorsStats = {}, fetchNewErrorsCount, projects }) {
|
||||
function ErrorsBadge({ projects }) {
|
||||
const { errorsStore } = useStore();
|
||||
const errorsStats = errorsStore.stats;
|
||||
useEffect(() => {
|
||||
if (projects.size === 0 || !!intervalId) return;
|
||||
|
||||
const params = { startTimestamp: weekRange.start.ts, endTimestamp: weekRange.end.ts };
|
||||
fetchNewErrorsCount(params)
|
||||
|
||||
const params = {
|
||||
startTimestamp: weekRange.start.ts,
|
||||
endTimestamp: weekRange.end.ts,
|
||||
};
|
||||
errorsStore.fetchNewErrorsCount(params);
|
||||
|
||||
intervalId = setInterval(() => {
|
||||
fetchNewErrorsCount(params);
|
||||
errorsStore.fetchNewErrorsCount(params);
|
||||
}, AUTOREFRESH_INTERVAL);
|
||||
}, [projects])
|
||||
|
||||
}, [projects]);
|
||||
|
||||
return errorsStats.unresolvedAndUnviewed > 0 ? (
|
||||
<div>{<div className={stl.badge} /> }</div>
|
||||
) : ''
|
||||
<div>{<div className={stl.badge} />}</div>
|
||||
) : (
|
||||
''
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(state => ({
|
||||
errorsStats: state.getIn([ 'errors', 'stats' ]),
|
||||
projects: state.getIn([ 'site', 'list' ]),
|
||||
}), { fetchNewErrorsCount })(ErrorsBadge)
|
||||
export default connect((state) => ({
|
||||
projects: state.getIn(['site', 'list']),
|
||||
}))(observer(ErrorsBadge));
|
||||
|
|
|
|||
|
|
@ -1,239 +0,0 @@
|
|||
import { List, Map } from 'immutable';
|
||||
import { clean as cleanParams } from 'App/api_client';
|
||||
import ErrorInfo, { RESOLVED, UNRESOLVED, IGNORED, BOOKMARK } from 'Types/errorInfo';
|
||||
import { fetchListType, fetchType } from './funcTools/crud';
|
||||
import { createRequestReducer, ROOT_KEY } from './funcTools/request';
|
||||
import { array, success, failure, createListUpdater, mergeReducers } from './funcTools/tools';
|
||||
import { reduceThenFetchResource } from './search'
|
||||
|
||||
const name = "error";
|
||||
const idKey = "errorId";
|
||||
const PER_PAGE = 10;
|
||||
const DEFAULT_SORT = 'occurrence';
|
||||
const DEFAULT_ORDER = 'desc';
|
||||
|
||||
const EDIT_OPTIONS = `${name}/EDIT_OPTIONS`;
|
||||
const FETCH_LIST = fetchListType(name);
|
||||
const FETCH = fetchType(name);
|
||||
const FETCH_NEW_ERRORS_COUNT = fetchType('errors/FETCH_NEW_ERRORS_COUNT');
|
||||
const RESOLVE = "errors/RESOLVE";
|
||||
const UNRESOLVE = "errors/UNRESOLVE";
|
||||
const IGNORE = "errors/IGNORE";
|
||||
const MERGE = "errors/MERGE";
|
||||
const TOGGLE_FAVORITE = "errors/TOGGLE_FAVORITE";
|
||||
const FETCH_TRACE = "errors/FETCH_TRACE";
|
||||
const UPDATE_CURRENT_PAGE = "errors/UPDATE_CURRENT_PAGE";
|
||||
const UPDATE_KEY = `${name}/UPDATE_KEY`;
|
||||
|
||||
function chartWrapper(chart = []) {
|
||||
return chart.map(point => ({ ...point, count: Math.max(point.count, 0) }));
|
||||
}
|
||||
|
||||
const updateItemInList = createListUpdater(idKey);
|
||||
const updateInstance = (state, instance) => state.getIn([ "instance", idKey ]) === instance[ idKey ]
|
||||
? state.mergeIn([ "instance" ], instance)
|
||||
: state;
|
||||
|
||||
const initialState = Map({
|
||||
totalCount: 0,
|
||||
list: List(),
|
||||
instance: ErrorInfo(),
|
||||
instanceTrace: List(),
|
||||
stats: Map(),
|
||||
sourcemapUploaded: true,
|
||||
currentPage: 1,
|
||||
limit: PER_PAGE,
|
||||
options: Map({
|
||||
sort: DEFAULT_SORT,
|
||||
order: DEFAULT_ORDER,
|
||||
status: UNRESOLVED,
|
||||
query: '',
|
||||
}),
|
||||
// sort: DEFAULT_SORT,
|
||||
// order: DEFAULT_ORDER,
|
||||
});
|
||||
|
||||
|
||||
function reducer(state = initialState, action = {}) {
|
||||
let updError;
|
||||
switch (action.type) {
|
||||
case EDIT_OPTIONS:
|
||||
return state.mergeIn(["options"], action.instance).set('currentPage', 1);
|
||||
case success(FETCH):
|
||||
if (state.get("list").find(e => e.get("errorId") === action.id)) {
|
||||
return updateItemInList(state, { errorId: action.data.errorId, viewed: true })
|
||||
.set("instance", ErrorInfo(action.data));
|
||||
} else {
|
||||
return state.set("instance", ErrorInfo(action.data));
|
||||
}
|
||||
case failure(FETCH):
|
||||
return state.set("instance", ErrorInfo());
|
||||
case success(FETCH_TRACE):
|
||||
return state.set("instanceTrace", List(action.data.trace)).set('sourcemapUploaded', action.data.sourcemapUploaded);
|
||||
case success(FETCH_LIST):
|
||||
const { data } = action;
|
||||
return state
|
||||
.set("totalCount", data ? data.total : 0)
|
||||
.set("list", List(data && data.errors).map(ErrorInfo)
|
||||
.filter(e => e.parentErrorId == null)
|
||||
.map(e => e.update("chart", chartWrapper)))
|
||||
case success(RESOLVE):
|
||||
updError = { errorId: action.id, status: RESOLVED, disabled: true };
|
||||
return updateItemInList(updateInstance(state, updError), updError);
|
||||
case success(UNRESOLVE):
|
||||
updError = { errorId: action.id, status: UNRESOLVED, disabled: true };
|
||||
return updateItemInList(updateInstance(state, updError), updError);
|
||||
case success(IGNORE):
|
||||
updError = { errorId: action.id, status: IGNORED, disabled: true };
|
||||
return updateItemInList(updateInstance(state, updError), updError);
|
||||
case success(TOGGLE_FAVORITE):
|
||||
return state.mergeIn([ "instance" ], { favorite: !state.getIn([ "instance", "favorite" ]) })
|
||||
case success(MERGE):
|
||||
const ids = action.ids.slice(1);
|
||||
return state.update("list", list => list.filter(e => !ids.includes(e.errorId)));
|
||||
case success(FETCH_NEW_ERRORS_COUNT):
|
||||
return state.set('stats', action.data);
|
||||
case UPDATE_KEY:
|
||||
return state.set(action.key, action.value);
|
||||
case UPDATE_CURRENT_PAGE:
|
||||
return state.set('currentPage', action.page);
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
export default mergeReducers(
|
||||
reducer,
|
||||
createRequestReducer({
|
||||
[ ROOT_KEY ]: FETCH_LIST,
|
||||
fetch: FETCH,
|
||||
fetchTrace: FETCH_TRACE,
|
||||
resolve: RESOLVE,
|
||||
unresolve: UNRESOLVE,
|
||||
ignore: IGNORE,
|
||||
merge: MERGE,
|
||||
toggleFavorite: TOGGLE_FAVORITE,
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
export function fetch(id) {
|
||||
return {
|
||||
id,
|
||||
types: array(FETCH),
|
||||
call: c => c.get(`/errors/${id}`),
|
||||
}
|
||||
}
|
||||
|
||||
export function fetchTrace(id) {
|
||||
return {
|
||||
id,
|
||||
types: array(FETCH_TRACE),
|
||||
call: c => c.get(`/errors/${id}/sourcemaps`),
|
||||
}
|
||||
}
|
||||
|
||||
export const fetchList = (params = {}, clear = false) => (dispatch, getState) => {
|
||||
params.page = getState().getIn(['errors', 'currentPage']);
|
||||
params.limit = PER_PAGE;
|
||||
|
||||
const options = getState().getIn(['errors', 'options']).toJS();
|
||||
if (options.status === BOOKMARK) {
|
||||
options.bookmarked = true;
|
||||
options.status = 'all';
|
||||
}
|
||||
|
||||
return dispatch({
|
||||
types: array(FETCH_LIST),
|
||||
call: client => client.post('/errors/search', { ...params, ...options }),
|
||||
clear,
|
||||
params: cleanParams(params),
|
||||
});
|
||||
};
|
||||
|
||||
// export function fetchList(params = {}, clear = false) {
|
||||
// return {
|
||||
// types: array(FETCH_LIST),
|
||||
// call: client => client.post('/errors/search', params),
|
||||
// clear,
|
||||
// params: cleanParams(params),
|
||||
// };
|
||||
// }
|
||||
|
||||
export function fetchBookmarks() {
|
||||
return {
|
||||
types: array(FETCH_LIST),
|
||||
call: client => client.post('/errors/search?favorite', {})
|
||||
}
|
||||
}
|
||||
|
||||
export const resolve = (id) => (dispatch, getState) => {
|
||||
const list = getState().getIn(['errors', 'list']);
|
||||
const index = list.findIndex(e => e.get('errorId') === id);
|
||||
const error = list.get(index);
|
||||
if (error.get('status') === RESOLVED) return;
|
||||
|
||||
return dispatch({
|
||||
types: array(RESOLVE),
|
||||
id,
|
||||
call: client => client.get(`/errors/${ id }/solve`),
|
||||
})
|
||||
}
|
||||
|
||||
export const unresolve = (id) => (dispatch, getState) => {
|
||||
const list = getState().getIn(['errors', 'list']);
|
||||
const index = list.findIndex(e => e.get('errorId') === id);
|
||||
const error = list.get(index);
|
||||
if (error.get('status') === UNRESOLVED) return;
|
||||
|
||||
return dispatch({
|
||||
types: array(UNRESOLVE),
|
||||
id,
|
||||
call: client => client.get(`/errors/${ id }/unsolve`),
|
||||
})
|
||||
}
|
||||
|
||||
export const ignore = (id) => (dispatch, getState) => {
|
||||
const list = getState().getIn(['errors', 'list']);
|
||||
const index = list.findIndex(e => e.get('errorId') === id);
|
||||
const error = list.get(index);
|
||||
if (error.get('status') === IGNORED) return;
|
||||
|
||||
return dispatch({
|
||||
types: array(IGNORE),
|
||||
id,
|
||||
call: client => client.get(`/errors/${ id }/ignore`),
|
||||
})
|
||||
}
|
||||
|
||||
export function merge(ids) {
|
||||
return {
|
||||
types: array(MERGE),
|
||||
ids,
|
||||
call: client => client.post(`/errors/merge`, { errors: ids }),
|
||||
}
|
||||
}
|
||||
|
||||
export function toggleFavorite(id) {
|
||||
return {
|
||||
types: array(TOGGLE_FAVORITE),
|
||||
id,
|
||||
call: client => client.get(`/errors/${ id }/favorite`),
|
||||
}
|
||||
}
|
||||
|
||||
export function fetchNewErrorsCount(params = {}) {
|
||||
return {
|
||||
types: array(FETCH_NEW_ERRORS_COUNT),
|
||||
call: client => client.get(`/errors/stats`, params),
|
||||
}
|
||||
}
|
||||
|
||||
export const updateCurrentPage = reduceThenFetchResource((page) => ({
|
||||
type: UPDATE_CURRENT_PAGE,
|
||||
page,
|
||||
}));
|
||||
|
||||
export const editOptions = reduceThenFetchResource((instance) => ({
|
||||
type: EDIT_OPTIONS,
|
||||
instance
|
||||
}));
|
||||
|
|
@ -6,7 +6,6 @@ import Event from 'Types/filter/event';
|
|||
import CustomFilter, { KEYS } from 'Types/filter/customFilter';
|
||||
import withRequestState, { RequestTypes } from './requestStateCreator';
|
||||
import { fetchList as fetchSessionList } from './sessions';
|
||||
import { fetchList as fetchErrorsList } from './errors';
|
||||
|
||||
const ERRORS_ROUTE = errorsRoute();
|
||||
|
||||
|
|
@ -85,7 +84,7 @@ const reducer = (state = initialState, action = {}) => {
|
|||
case FETCH_LIST.SUCCESS:
|
||||
const flows = List(action.data).map(SavedFilter)
|
||||
let _state = state.set('list', flows)
|
||||
|
||||
|
||||
if (!hasFilterOptions) {
|
||||
const tmp = {}
|
||||
flows.forEach(i => {
|
||||
|
|
@ -119,8 +118,8 @@ const reducer = (state = initialState, action = {}) => {
|
|||
case SET_ACTIVE_KEY:
|
||||
return state.set('activeFilterKey', action.filterKey);
|
||||
case APPLY:
|
||||
return action.fromUrl
|
||||
? state.set('appliedFilter',
|
||||
return action.fromUrl
|
||||
? state.set('appliedFilter',
|
||||
Filter(action.filter)
|
||||
.set('events', state.getIn([ 'appliedFilter', 'events' ]))
|
||||
)
|
||||
|
|
@ -148,7 +147,7 @@ const reducer = (state = initialState, action = {}) => {
|
|||
if (action.index >= 0) // replacing an event
|
||||
return state.setIn([ 'appliedFilter', 'events', action.index ], event)
|
||||
else
|
||||
return state.updateIn([ 'appliedFilter', 'events' ], list => action.single
|
||||
return state.updateIn([ 'appliedFilter', 'events' ], list => action.single
|
||||
? List([ event ])
|
||||
: list.push(event));
|
||||
case REMOVE_EVENT:
|
||||
|
|
@ -166,7 +165,7 @@ const reducer = (state = initialState, action = {}) => {
|
|||
return state.setIn([ 'appliedFilter', 'events' ], List())
|
||||
.setIn([ 'appliedFilter', 'filters' ], List())
|
||||
.set('searchQuery', '');
|
||||
|
||||
|
||||
case ADD_ATTRIBUTE:
|
||||
const filter = CustomFilter(action.filter);
|
||||
|
||||
|
|
@ -174,7 +173,7 @@ const reducer = (state = initialState, action = {}) => {
|
|||
return state.setIn([ 'appliedFilter', 'filters', action.index], filter);
|
||||
else
|
||||
return state.updateIn([ 'appliedFilter', 'filters'], filters => filters.push(filter));
|
||||
|
||||
|
||||
case EDIT_ATTRIBUTE:
|
||||
return state.setIn([ 'appliedFilter', 'filters', action.index, action.key ], action.value );
|
||||
case REMOVE_ATTRIBUTE:
|
||||
|
|
@ -209,7 +208,7 @@ const reduceThenFetchResource = actionCreator => (...args) => (dispatch, getStat
|
|||
|
||||
// Hello AGILE!
|
||||
return isRoute(ERRORS_ROUTE, window.location.pathname)
|
||||
? dispatch(fetchErrorsList(filter))
|
||||
? null
|
||||
: dispatch(fetchSessionList(filter));
|
||||
}
|
||||
|
||||
|
|
@ -386,7 +385,7 @@ export const edit = instance => {
|
|||
export const updateValue = (filterType, index, value) => {
|
||||
return {
|
||||
type: UPDATE_VALUE,
|
||||
filterType,
|
||||
filterType,
|
||||
index,
|
||||
value
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import sources from './sources';
|
|||
import site from './site';
|
||||
import customFields from './customField';
|
||||
import integrations from './integrations';
|
||||
import errors from './errors';
|
||||
import funnels from './funnels';
|
||||
import customMetrics from './customMetrics';
|
||||
import search from './search';
|
||||
|
|
@ -22,7 +21,6 @@ const rootReducer = combineReducers({
|
|||
funnelFilters,
|
||||
site,
|
||||
customFields,
|
||||
errors,
|
||||
funnels,
|
||||
customMetrics,
|
||||
search,
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ import { List, Map } from 'immutable';
|
|||
import { DURATION_FILTER } from 'App/constants/storageKeys';
|
||||
import { errors as errorsRoute, isRoute } from 'App/routes';
|
||||
|
||||
import { fetchList as fetchErrorsList } from './errors';
|
||||
import {
|
||||
editType,
|
||||
fetchListType,
|
||||
|
|
@ -252,7 +251,7 @@ export const reduceThenFetchResource =
|
|||
|
||||
dispatch(updateLatestRequestTime());
|
||||
return isRoute(ERRORS_ROUTE, window.location.pathname)
|
||||
? dispatch(fetchErrorsList(filter))
|
||||
? null
|
||||
: dispatch(fetchSessionList(filter, forceFetch));
|
||||
};
|
||||
|
||||
|
|
@ -470,7 +469,7 @@ export const refreshFilterOptions = () => (dispatch, getState) => {
|
|||
const currentProject = getState().getIn(['site', 'instance']);
|
||||
return dispatch({
|
||||
type: REFRESH_FILTER_OPTIONS,
|
||||
isMobile: currentProject?.platform === 'ios'
|
||||
isMobile: currentProject?.platform === 'ios',
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,55 +1,91 @@
|
|||
import { makeAutoObservable } from "mobx"
|
||||
import { errorService } from "App/services"
|
||||
import Error from "./types/error"
|
||||
import { makeAutoObservable } from 'mobx';
|
||||
|
||||
import apiClient from 'App/api_client';
|
||||
import { errorService } from 'App/services';
|
||||
|
||||
import { ErrorInfo } from './types/error';
|
||||
|
||||
export default class ErrorStore {
|
||||
isLoading: boolean = false
|
||||
isSaving: boolean = false
|
||||
instance: ErrorInfo | null = null;
|
||||
instanceTrace: Record<string, any> = [];
|
||||
stats: Record<string, any> = {};
|
||||
sourcemapUploaded = false;
|
||||
isLoading = false;
|
||||
errorStates: Record<string, any> = {};
|
||||
|
||||
errors: any[] = []
|
||||
instance: Error | null = null
|
||||
constructor() {
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this, {
|
||||
|
||||
})
|
||||
setLoadingState(value: boolean) {
|
||||
this.isLoading = value;
|
||||
}
|
||||
|
||||
setErrorState(actionKey: string, error: any) {
|
||||
this.errorStates[actionKey] = error;
|
||||
}
|
||||
|
||||
setInstance(errorData: ErrorInfo | null) {
|
||||
this.instance = errorData ? new ErrorInfo(errorData) : null;
|
||||
}
|
||||
|
||||
setInstanceTrace(trace: any) {
|
||||
this.instanceTrace = trace || [];
|
||||
}
|
||||
|
||||
setSourcemapUploaded(value: boolean) {
|
||||
this.sourcemapUploaded = value;
|
||||
}
|
||||
|
||||
setStats(stats: any) {
|
||||
this.stats = stats;
|
||||
}
|
||||
|
||||
async fetchError(id: string) {
|
||||
const actionKey = 'fetchError';
|
||||
this.setLoadingState(true);
|
||||
this.setErrorState(actionKey, null);
|
||||
|
||||
try {
|
||||
const response = await errorService.fetchError(id);
|
||||
const errorData = response.data;
|
||||
this.setInstance(errorData);
|
||||
} catch (error) {
|
||||
this.setInstance(null);
|
||||
this.setErrorState(actionKey, error);
|
||||
} finally {
|
||||
this.setLoadingState(false);
|
||||
}
|
||||
}
|
||||
|
||||
updateKey(key: string, value: any) {
|
||||
this[key] = value
|
||||
}
|
||||
async fetchErrorTrace(id: string) {
|
||||
const actionKey = 'fetchErrorTrace';
|
||||
this.setLoadingState(true);
|
||||
this.setErrorState(actionKey, null);
|
||||
|
||||
fetchErrors(): Promise<any> {
|
||||
this.isLoading = true
|
||||
return new Promise((resolve, reject) => {
|
||||
errorService.all()
|
||||
.then(response => {
|
||||
const errors = response.map(e => new Error().fromJSON(e));
|
||||
this.errors = errors
|
||||
resolve(errors)
|
||||
}).catch(error => {
|
||||
reject(error)
|
||||
}).finally(() => {
|
||||
this.isLoading = false
|
||||
}
|
||||
)
|
||||
})
|
||||
try {
|
||||
const response = await errorService.fetchErrorTrace(id);
|
||||
this.setInstanceTrace(response.data.trace);
|
||||
this.setSourcemapUploaded(response.data.sourcemapUploaded);
|
||||
} catch (error) {
|
||||
this.setErrorState(actionKey, error);
|
||||
} finally {
|
||||
this.setLoadingState(false);
|
||||
}
|
||||
}
|
||||
|
||||
fetchError(errorId: string): Promise<any> {
|
||||
this.isLoading = true
|
||||
return new Promise((resolve, reject) => {
|
||||
errorService.one(errorId)
|
||||
.then(response => {
|
||||
const error = new Error().fromJSON(response);
|
||||
this.instance = error
|
||||
resolve(error)
|
||||
}).catch(error => {
|
||||
reject(error)
|
||||
}).finally(() => {
|
||||
this.isLoading = false
|
||||
}
|
||||
)
|
||||
})
|
||||
async fetchNewErrorsCount(params: any) {
|
||||
const actionKey = 'fetchNewErrorsCount';
|
||||
this.setLoadingState(true);
|
||||
this.setErrorState(actionKey, null);
|
||||
|
||||
try {
|
||||
const response = await errorService.fetchNewErrorsCount(params);
|
||||
this.setStats(response.data);
|
||||
} catch (error) {
|
||||
this.setErrorState(actionKey, error);
|
||||
} finally {
|
||||
this.setLoadingState(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,56 +1,102 @@
|
|||
import Session from './session';
|
||||
|
||||
export default class Error {
|
||||
sessionId: string = ''
|
||||
messageId: string = ''
|
||||
errorId: string = ''
|
||||
projectId: string = ''
|
||||
source: string = ''
|
||||
name: string = ''
|
||||
message: string = ''
|
||||
time: string = ''
|
||||
function: string = '?'
|
||||
stack0InfoString: string = ''
|
||||
status: string = ''
|
||||
|
||||
chart: any = []
|
||||
sessions: number = 0
|
||||
users: number = 0
|
||||
firstOccurrence: string = ''
|
||||
lastOccurrence: string = ''
|
||||
timestamp: string = ''
|
||||
sessionId: string = '';
|
||||
messageId: string = '';
|
||||
errorId: string = '';
|
||||
projectId: string = '';
|
||||
source: string = '';
|
||||
name: string = '';
|
||||
message: string = '';
|
||||
time: string = '';
|
||||
function: string = '?';
|
||||
stack0InfoString: string = '';
|
||||
status: string = '';
|
||||
|
||||
constructor() {
|
||||
}
|
||||
chart: any = [];
|
||||
sessions: number = 0;
|
||||
users: number = 0;
|
||||
firstOccurrence: string = '';
|
||||
lastOccurrence: string = '';
|
||||
timestamp: string = '';
|
||||
|
||||
fromJSON(json: any) {
|
||||
this.sessionId = json.sessionId
|
||||
this.messageId = json.messageId
|
||||
this.errorId = json.errorId
|
||||
this.projectId = json.projectId
|
||||
this.source = json.source
|
||||
this.name = json.name
|
||||
this.message = json.message
|
||||
this.time = json.time
|
||||
this.function = json.function
|
||||
this.stack0InfoString = getStck0InfoString(json.stack || [])
|
||||
this.status = json.status
|
||||
|
||||
this.chart = json.chart
|
||||
this.sessions = json.sessions
|
||||
this.users = json.users
|
||||
this.firstOccurrence = json.firstOccurrence
|
||||
this.lastOccurrence = json.lastOccurrence
|
||||
this.timestamp = json.timestamp
|
||||
|
||||
return this
|
||||
}
|
||||
constructor() {}
|
||||
}
|
||||
|
||||
function getStck0InfoString(stack: any) {
|
||||
const stack0 = stack[0];
|
||||
if (!stack0) return "";
|
||||
let s = stack0.function || "";
|
||||
if (stack0.url) {
|
||||
s += ` (${stack0.url})`;
|
||||
const stack0 = stack[0];
|
||||
if (!stack0) return '';
|
||||
let s = stack0.function || '';
|
||||
if (stack0.url) {
|
||||
s += ` (${stack0.url})`;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
export interface ErrorInfoData {
|
||||
errorId?: string;
|
||||
favorite: boolean;
|
||||
viewed: boolean;
|
||||
source: string;
|
||||
name: string;
|
||||
message: string;
|
||||
stack0InfoString: string;
|
||||
status: string;
|
||||
parentErrorId?: string;
|
||||
users: number;
|
||||
sessions: number;
|
||||
lastOccurrence: number;
|
||||
firstOccurrence: number;
|
||||
chart: any[];
|
||||
chart24: any[];
|
||||
chart30: any[];
|
||||
tags: string[];
|
||||
customTags: string[];
|
||||
lastHydratedSession: Session;
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
export class ErrorInfo implements ErrorInfoData {
|
||||
errorId?: string;
|
||||
favorite = false;
|
||||
viewed = false;
|
||||
source = '';
|
||||
name = '';
|
||||
message = '';
|
||||
stack0InfoString = '';
|
||||
status = '';
|
||||
parentErrorId?: string;
|
||||
users = 0;
|
||||
sessions = 0;
|
||||
lastOccurrence = Date.now();
|
||||
firstOccurrence = Date.now();
|
||||
chart: any[] = [];
|
||||
chart24: any[] = [];
|
||||
chart30: any[] = [];
|
||||
tags: string[] = [];
|
||||
customTags: string[] = [];
|
||||
lastHydratedSession: Session;
|
||||
disabled = false;
|
||||
|
||||
constructor(data?: Partial<ErrorInfoData>) {
|
||||
if (data) {
|
||||
Object.assign(this, data);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
if (data?.lastHydratedSession) {
|
||||
this.lastHydratedSession = new Session().fromJson(
|
||||
data.lastHydratedSession
|
||||
);
|
||||
} else {
|
||||
this.lastHydratedSession = new Session();
|
||||
}
|
||||
}
|
||||
|
||||
static fromJS(data: any): ErrorInfo {
|
||||
const { stack, lastHydratedSession, ...other } = data;
|
||||
return new ErrorInfo({
|
||||
...other,
|
||||
lastHydratedSession: new Session().fromJson(data.lastHydratedSession),
|
||||
stack0InfoString: getStck0InfoString(stack || []),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,27 @@
|
|||
import BaseService from './BaseService';
|
||||
|
||||
export default class ErrorService extends BaseService {
|
||||
all(params: any = {}): Promise<any[]> {
|
||||
return this.client.post('/errors/search', params)
|
||||
.then(r => r.json())
|
||||
.then((response: { data: any; }) => response.data || [])
|
||||
.catch(e => Promise.reject(e))
|
||||
}
|
||||
fetchError = async (id: string) => {
|
||||
const r = await this.client.get(`/errors/${id}`);
|
||||
|
||||
one(id: string): Promise<any> {
|
||||
return this.client.get(`/errors/${id}`)
|
||||
.then(r => r.json())
|
||||
.then((response: { data: any; }) => response.data || {})
|
||||
.catch(e => Promise.reject(e))
|
||||
}
|
||||
}
|
||||
return await r.json();
|
||||
};
|
||||
|
||||
fetchErrorList = async (params: Record<string, any>) => {
|
||||
const r = await this.client.post('/errors/search', params);
|
||||
|
||||
return await r.json();
|
||||
};
|
||||
|
||||
fetchErrorTrace = async (id: string) => {
|
||||
const r = await this.client.get(`/errors/${id}/sourcemaps`);
|
||||
|
||||
return await r.json();
|
||||
};
|
||||
|
||||
fetchNewErrorsCount = async (params: any) => {
|
||||
const r = await this.client.get('/errors/stats', params);
|
||||
|
||||
return await r.json();
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ export const KEYS = {
|
|||
UTM_SOURCE,
|
||||
UTM_MEDIUM,
|
||||
UTM_CAMPAIGN,
|
||||
|
||||
|
||||
DOM_COMPLETE,
|
||||
LARGEST_CONTENTFUL_PAINT_TIME,
|
||||
TIME_BETWEEN_EVENTS,
|
||||
|
|
@ -89,7 +89,7 @@ const getOperatorDefault = (type) => {
|
|||
if (type === KEYS.SLOW_SESSION) return 'true';
|
||||
if (type === KEYS.CLICK_RAGE) return 'true';
|
||||
if (type === KEYS.CLICK) return 'on';
|
||||
|
||||
|
||||
return 'is';
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { List, Map } from 'immutable';
|
|||
import Record from 'Types/Record';
|
||||
import { KEYS } from 'Types/filter/customFilter';
|
||||
import { TYPES } from 'Types/filter/event';
|
||||
import {
|
||||
import {
|
||||
DATE_RANGE_VALUES,
|
||||
CUSTOM_RANGE,
|
||||
getDateRangeFromValue
|
||||
|
|
@ -52,7 +52,7 @@ export default Record({
|
|||
const js = this.toJS();
|
||||
js.filters = js.filters.map(filter => {
|
||||
filter.type = filter.key
|
||||
|
||||
|
||||
delete filter.category
|
||||
delete filter.icon
|
||||
delete filter.operatorOptions
|
||||
|
|
@ -156,7 +156,6 @@ export const defaultFilters = [
|
|||
{ label: 'UTM Source', key: KEYS.UTM_SOURCE, type: KEYS.UTM_SOURCE, filterKey: KEYS.UTM_SOURCE, icon: 'exclamation-circle', isFilter: true },
|
||||
{ label: 'UTM Medium', key: KEYS.UTM_MEDIUM, type: KEYS.UTM_MEDIUM, filterKey: KEYS.UTM_MEDIUM, icon: 'exclamation-circle', isFilter: true },
|
||||
{ label: 'UTM Campaign', key: KEYS.UTM_CAMPAIGN, type: KEYS.UTM_CAMPAIGN, filterKey: KEYS.UTM_CAMPAIGN, icon: 'exclamation-circle', isFilter: true },
|
||||
|
||||
{ label: 'Fetch Requests', key: KEYS.FETCH, type: KEYS.FETCH, filterKey: KEYS.FETCH, icon: 'fetch', isFilter: false },
|
||||
{ label: 'GraphQL Queries', key: KEYS.GRAPHQL, type: KEYS.GRAPHQL, filterKey: KEYS.GRAPHQL, icon: 'vendors/graphql', isFilter: false },
|
||||
{ label: 'Store Actions', key: KEYS.STATEACTION, type: KEYS.STATEACTION, filterKey: KEYS.STATEACTION, icon: 'store', isFilter: false },
|
||||
|
|
@ -205,4 +204,4 @@ export const getEventIcon = (filter) => {
|
|||
return 'integrations/' + source;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue