feat(ui) - errors pagination

This commit is contained in:
Shekar Siri 2022-03-15 17:43:06 +01:00
parent 4807d980f9
commit 7168e660cc
7 changed files with 156 additions and 133 deletions

View file

@ -1,23 +1,18 @@
import { connect } from 'react-redux';
import withSiteIdRouter from 'HOCs/withSiteIdRouter';
import withPermissions from 'HOCs/withPermissions'
import { UNRESOLVED, RESOLVED, IGNORED } from "Types/errorInfo";
import { getRE } from 'App/utils';
import { fetchBookmarks } from "Duck/errors";
import { UNRESOLVED, RESOLVED, IGNORED, BOOKMARK } from "Types/errorInfo";
import { fetchBookmarks, editOptions } from "Duck/errors";
import { applyFilter } from 'Duck/filters';
import { fetchList as fetchSlackList } from 'Duck/integrations/slack';
import { errors as errorsRoute, isRoute } from "App/routes";
import EventFilter from 'Components/BugFinder/EventFilter';
import DateRange from 'Components/BugFinder/DateRange';
import withPageTitle from 'HOCs/withPageTitle';
import { SavedSearchList } from 'UI';
import List from './List/List';
import ErrorInfo from './Error/ErrorInfo';
import Header from './Header';
import SideMenuSection from './SideMenu/SideMenuSection';
import SideMenuHeader from './SideMenu/SideMenuHeader';
import SideMenuDividedItem from './SideMenu/SideMenuDividedItem';
const ERRORS_ROUTE = errorsRoute();
@ -39,44 +34,26 @@ function getStatusLabel(status) {
@withSiteIdRouter
@connect(state => ({
list: state.getIn([ "errors", "list" ]),
status: state.getIn([ "errors", "options", "status" ]),
}), {
fetchBookmarks,
applyFilter,
fetchSlackList,
editOptions,
})
@withPageTitle("Errors - OpenReplay")
export default class Errors extends React.PureComponent {
state = {
status: UNRESOLVED,
bookmarksActive: false,
currentList: this.props.list.filter(e => e.status === UNRESOLVED),
filter: '',
constructor(props) {
super(props)
this.state = {
filter: '',
}
}
componentDidMount() {
this.props.fetchSlackList(); // Delete after implementing cache
}
onFilterChange = ({ target: { value } }) => this.setState({ filter: value })
componentDidUpdate(prevProps, prevState) {
const { bookmarksActive, status, filter } = this.state;
const { list } = this.props;
if (prevProps.list !== list
|| prevState.status !== status
|| prevState.bookmarksActive !== bookmarksActive
|| prevState.filter !== filter) {
const unfiltered = bookmarksActive
? list
: list.filter(e => e.status === status);
const filterRE = getRE(filter);
this.setState({
currentList: unfiltered
.filter(e => filterRE.test(e.name) || filterRE.test(e.message)),
})
}
}
ensureErrorsPage() {
const { history } = this.props;
if (!isRoute(ERRORS_ROUTE, history.location.pathname)) {
@ -85,22 +62,11 @@ export default class Errors extends React.PureComponent {
}
onStatusItemClick = ({ key }) => {
if (this.state.bookmarksActive) {
this.props.applyFilter();
}
this.setState({
status: key,
bookmarksActive: false,
});
this.ensureErrorsPage();
this.props.editOptions({ status: key });
}
onBookmarksClick = () => {
this.setState({
bookmarksActive: true,
});
this.props.fetchBookmarks();
this.ensureErrorsPage();
this.props.editOptions({ status: BOOKMARK });
}
@ -110,8 +76,9 @@ export default class Errors extends React.PureComponent {
match: {
params: { errorId }
},
status,
list,
} = this.props;
const { status, bookmarksActive, currentList } = this.state;
return (
<div className="page-margin container-90" >
@ -137,14 +104,14 @@ export default class Errors extends React.PureComponent {
icon: "ban",
label: getStatusLabel(IGNORED),
active: status === IGNORED,
}
}
]}
/>
<SideMenuDividedItem
className="mt-3 mb-4"
iconName="star"
title="Bookmarks"
active={ bookmarksActive }
active={ status === BOOKMARK }
onClick={ this.onBookmarksClick }
/>
</div>
@ -154,8 +121,8 @@ export default class Errors extends React.PureComponent {
<>
<div className="mb-5 flex">
<Header
text={ bookmarksActive ? "Bookmarks" : getStatusLabel(status) }
count={ currentList.size }
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>
@ -164,12 +131,11 @@ export default class Errors extends React.PureComponent {
</div>
<List
status={ status }
list={ currentList }
onFilterChange={this.onFilterChange}
list={ list }
/>
</>
:
<ErrorInfo errorId={ errorId } list={ currentList } />
<ErrorInfo errorId={ errorId } list={ list } />
}
</div>
</div>

View file

@ -1,53 +1,62 @@
import cn from 'classnames';
import { connect } from 'react-redux';
import { Set, List as ImmutableList } from "immutable";
import { NoContent, Loader, Checkbox, LoadMoreButton, IconButton, Input, DropdownPlain } from 'UI';
import { merge, resolve, unresolve, ignore, updateCurrentPage } from "Duck/errors";
import { NoContent, Loader, Checkbox, LoadMoreButton, IconButton, Input, DropdownPlain, Pagination } from 'UI';
import { merge, resolve, unresolve, ignore, updateCurrentPage, editOptions } from "Duck/errors";
import { applyFilter } from 'Duck/filters';
import { IGNORED, RESOLVED, UNRESOLVED } from 'Types/errorInfo';
import SortDropdown from 'Components/BugFinder/Filters/SortDropdown';
import Divider from 'Components/Errors/ui/Divider';
import ListItem from './ListItem/ListItem';
import { debounce } from 'App/utils';
const PER_PAGE = 5;
const DEFAULT_SORT = 'lastOccurrence';
const DEFAULT_ORDER = 'desc';
const sortOptionsMap = {
'lastOccurrence-desc': 'Last Occurrence',
'firstOccurrence-desc': 'First Occurrence',
'sessions-asc': 'Sessions Ascending',
'sessions-desc': 'Sessions Descending',
'users-asc': 'Users Ascending',
'users-desc': 'Users Descending',
'firstOccurrence-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, text ]) => ({ value, text }));
@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"]),
currentPage: state.getIn(["errors", "currentPage"]),
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,
updateCurrentPage,
editOptions,
})
export default class List extends React.PureComponent {
state = {
checkedAll: false,
checkedIds: Set(),
sort: {}
constructor(props) {
super(props)
this.state = {
checkedAll: false,
checkedIds: Set(),
query: props.query,
}
this.debounceFetch = debounce(this.props.editOptions, 1000);
}
componentDidMount() {
this.props.applyFilter({ sort: DEFAULT_SORT, order: DEFAULT_ORDER, events: ImmutableList(), filters: ImmutableList() });
if (this.props.list.size === 0) {
this.props.applyFilter({ });
}
}
check = ({ errorId }) => {
@ -111,8 +120,14 @@ export default class List extends React.PureComponent {
writeOption = (e, { name, value }) => {
const [ sort, order ] = value.split('-');
const sign = order === 'desc' ? -1 : 1;
this.setState({ sort: { sort, order }})
if (name === 'sort') {
this.props.editOptions({ sort, order });
}
}
onQueryChange = (e, { value }) => {
this.setState({ query: value });
this.debounceFetch({ query: value });
}
render() {
@ -123,19 +138,18 @@ export default class List extends React.PureComponent {
ignoreLoading,
resolveToggleLoading,
mergeLoading,
onFilterChange,
currentPage,
currentPage,
total,
sort,
order,
} = this.props;
const {
checkedAll,
checkedIds,
sort
query,
} = this.state;
const someLoading = loading || ignoreLoading || resolveToggleLoading || mergeLoading;
const currentCheckedIds = this.currentCheckedIds();
const displayedCount = Math.min(currentPage * PER_PAGE, list.size);
let _list = sort.sort ? list.sortBy(i => i[sort.sort]) : list;
_list = sort.order === 'desc' ? _list.reverse() : _list;
return (
<div className="bg-white p-5 border-radius-3 thin-gray-border">
@ -182,33 +196,35 @@ export default class List extends React.PureComponent {
}
</div>
<div className="flex items-center ml-6">
<span className="mr-2 color-gray-medium">Sort By</span>
<span className="mr-2 color-gray-medium">Sort By</span>
<DropdownPlain
name="type"
options={ sortOptions }
onChange={ this.writeOption }
/>
<Input
defaultValue={ `${sort}-${order}` }
name="sort"
options={ sortOptions }
onChange={ this.writeOption }
/>
<Input
style={{ width: '350px'}}
className="input-small ml-3"
placeholder="Filter by Name or Message"
icon="search"
iconPosition="left"
name="filter"
onChange={ onFilterChange }
/>
</div>
</div>
<Divider />
<NoContent
title="No Errors Found!"
subtext="Please try to change your search parameters."
icon="exclamation-circle"
show={ !loading && list.size === 0}
>
<Loader loading={ loading }>
{ _list.take(displayedCount).map(e =>
<>
className="input-small ml-3"
placeholder="Filter by Name or Message"
icon="search"
iconPosition="left"
name="filter"
onChange={ this.onQueryChange }
value={query}
/>
</div>
</div>
<Divider />
<NoContent
title="No Errors Found!"
subtext="Please try to change your search parameters."
icon="exclamation-circle"
show={ !loading && list.size === 0}
>
<Loader loading={ loading }>
{ list.map(e =>
<div key={e.errorId}>
<ListItem
disabled={someLoading}
key={e.errorId}
@ -217,16 +233,19 @@ export default class List extends React.PureComponent {
onCheck={ this.check }
/>
<Divider/>
</>
</div>
)}
<LoadMoreButton
className="mt-3"
displayedCount={displayedCount}
totalCount={list.size}
onClick={this.addPage}
/>
</Loader>
</NoContent>
<div className="w-full flex items-center justify-center mt-4">
<Pagination
page={currentPage}
totalPages={Math.ceil(total / PER_PAGE)}
onPageChange={(page) => this.props.updateCurrentPage(page)}
limit={PER_PAGE}
debounceRequest={500}
/>
</div>
</Loader>
</NoContent>
</div>
);
}

View file

@ -21,7 +21,7 @@ function DropdownPlain({ name, label, options, onChange, defaultValue, wrapperSt
options={ options }
onChange={ onChange }
defaultValue={ defaultValue || options[ 0 ].value }
icon={null}
// icon={null}
disabled={disabled}
icon={ <Icon name="chevron-down" color="gray-dark" size="14" className={stl.dropdownIcon} /> }
/>

View file

@ -1,13 +1,18 @@
import { List, Map } from 'immutable';
import { clean as cleanParams } from 'App/api_client';
import ErrorInfo, { RESOLVED, UNRESOLVED, IGNORED } from 'Types/errorInfo';
import ErrorInfo, { RESOLVED, UNRESOLVED, IGNORED, BOOKMARK } from 'Types/errorInfo';
import { createFetch, fetchListType, fetchType } from './funcTools/crud';
import { createRequestReducer, ROOT_KEY } from './funcTools/request';
import { array, request, success, failure, createListUpdater, mergeReducers } from './funcTools/tools';
import { reduceThenFetchResource } from './search'
const name = "error";
const idKey = "errorId";
const PER_PAGE = 5;
const DEFAULT_SORT = 'lastOccurrence';
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');
@ -18,6 +23,7 @@ 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) }));
@ -35,13 +41,23 @@ const initialState = Map({
instanceTrace: List(),
stats: Map(),
sourcemapUploaded: true,
currentPage: 1,
currentPage: 1,
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);
case success(FETCH):
return state.set("instance", ErrorInfo(action.data));
case success(FETCH_TRACE):
@ -69,8 +85,10 @@ function reducer(state = initialState, action = {}) {
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_CURRENT_PAGE:
return state.set('currentPage', action.page);
case UPDATE_KEY:
return state.set(action.key, action.value);
case UPDATE_CURRENT_PAGE:
return state.set('currentPage', action.page);
}
return state;
}
@ -106,14 +124,31 @@ export function fetchTrace(id) {
}
}
export function fetchList(params = {}, clear = false) {
return {
types: array(FETCH_LIST),
call: client => client.post('/errors/search', params),
clear,
params: cleanParams(params),
};
}
export const fetchList = (params = {}, clear = false) => (dispatch, getState) => {
params.page = getState().getIn(['errors', 'currentPage']);
params.limit = PER_PAGE;
const options = getState().getIn(['errors', 'options']);
if (options.get("status") === BOOKMARK) {
options.bookmarked = true;
}
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 {
@ -169,9 +204,12 @@ export function fetchNewErrorsCount(params = {}) {
}
}
export function updateCurrentPage(page) {
return {
type: 'errors/UPDATE_CURRENT_PAGE',
export const updateCurrentPage = reduceThenFetchResource((page) => ({
type: UPDATE_CURRENT_PAGE,
page,
};
}
}));
export const editOptions = reduceThenFetchResource((instance) => ({
type: EDIT_OPTIONS,
instance
}));

View file

@ -126,7 +126,7 @@ export const filterMap = ({category, value, key, operator, sourceOperator, sourc
filters: filters ? filters.map(filterMap) : [],
});
const reduceThenFetchResource = actionCreator => (...args) => (dispatch, getState) => {
export const reduceThenFetchResource = actionCreator => (...args) => (dispatch, getState) => {
dispatch(actionCreator(...args));
const filter = getState().getIn([ 'search', 'instance']).toData();

View file

@ -5,6 +5,7 @@ import Session from './session';
export const RESOLVED = "resolved";
export const UNRESOLVED = "unresolved";
export const IGNORED = "ignored";
export const BOOKMARK = "bookmark";
function getStck0InfoString(stack) {

View file

@ -237,6 +237,5 @@ export const isGreaterOrEqualVersion = (version, compareTo) => {
export const sliceListPerPage = (list, page, perPage = 10) => {
const start = page * perPage;
const end = start + perPage;
console.log(start, end)
return list.slice(start, end);
}