feat(ui) - autoplay with pagination and checking for latest sessions, also a fix

This commit is contained in:
Shekar Siri 2022-11-25 16:54:33 +01:00
parent 3048d12c7c
commit 0e99cd7c59
8 changed files with 232 additions and 70 deletions

View file

@ -4,13 +4,52 @@ import { setAutoplayValues } from 'Duck/sessions';
import { session as sessionRoute } from 'App/routes';
import { Link, Icon, Toggler, Tooltip } from 'UI';
import { Controls as PlayerControls, connectPlayer } from 'Player';
import { withRouter, RouteComponentProps } from 'react-router-dom';
import cn from 'classnames';
import { fetchAutoplaySessions } from 'Duck/search';
function Autoplay(props) {
const { previousId, nextId, autoplay, disabled } = props;
const PER_PAGE = 10;
interface Props extends RouteComponentProps {
previousId: string;
nextId: string;
autoplay: boolean;
defaultList: any;
currentPage: number;
total: number;
setAutoplayValues?: () => void;
toggleAutoplay?: () => void;
latestRequestTime: any;
sessionIds: any;
fetchAutoplaySessions?: (page: number) => Promise<void>;
}
function Autoplay(props: Props) {
const {
previousId,
nextId,
currentPage,
total,
autoplay,
sessionIds,
latestRequestTime,
match: {
// @ts-ignore
params: { siteId, sessionId },
},
} = props;
const disabled = sessionIds.length === 0;
useEffect(() => {
props.setAutoplayValues();
if (latestRequestTime) {
props.setAutoplayValues();
const totalPages = Math.ceil(total / PER_PAGE);
const index = sessionIds.indexOf(sessionId);
// check for the last page and load the next
if (currentPage !== totalPages && index === sessionIds.length - 1) {
props.fetchAutoplaySessions(currentPage + 1).then(props.setAutoplayValues);
}
}
}, []);
return (
@ -62,21 +101,23 @@ function Autoplay(props) {
);
}
const connectAutoplay = connect(
(state) => ({
export default connect(
(state: any) => ({
previousId: state.getIn(['sessions', 'previousId']),
nextId: state.getIn(['sessions', 'nextId']),
currentPage: state.getIn(['search', 'currentPage']) || 1,
total: state.getIn(['sessions', 'total']) || 0,
sessionIds: state.getIn(['sessions', 'sessionIds']) || [],
latestRequestTime: state.getIn(['search', 'latestRequestTime']),
}),
{ setAutoplayValues }
);
export default connectAutoplay(
{ setAutoplayValues, fetchAutoplaySessions }
)(
connectPlayer(
(state) => ({
(state: any) => ({
autoplay: state.autoplay,
}),
{
toggleAutoplay: PlayerControls.toggleAutoplay,
}
)(Autoplay)
)(withRouter(Autoplay))
);

View file

@ -4,6 +4,7 @@ import SessionHeader from './components/SessionHeader';
import NotesList from './components/Notes/NoteList';
import { connect } from 'react-redux';
import { fetchList as fetchMembers } from 'Duck/member';
import LatestSessionsMessage from './components/LatestSessionsMessage';
function SessionListContainer({
activeTab,
@ -21,6 +22,7 @@ function SessionListContainer({
<div className="widget-wrapper">
<SessionHeader />
<div className="border-b" />
<LatestSessionsMessage />
{activeTab !== 'notes' ? <SessionList /> : <NotesList members={members} />}
</div>
);

View file

@ -0,0 +1,31 @@
import React from 'react';
import { connect } from 'react-redux';
import { updateCurrentPage } from 'Duck/search';
import { numberWithCommas } from 'App/utils'
interface Props {
latestSessions: any;
updateCurrentPage: (page: number) => void;
}
function LatestSessionsMessage(props: Props) {
const { latestSessions = [] } = props;
const count = latestSessions.length;
return count > 0 ? (
<div
className="bg-amber-50 p-1 flex w-full border-b text-center justify-center link"
style={{ backgroundColor: 'rgb(255 251 235)' }}
onClick={() => props.updateCurrentPage(1)}
>
Show {numberWithCommas(count)} new Sessions
</div>
) : (
<></>
);
}
export default connect(
(state: any) => ({
latestSessions: state.getIn(['search', 'latestList']),
}),
{ updateCurrentPage }
)(LatestSessionsMessage);

View file

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

View file

@ -9,24 +9,25 @@ import {
addFilterByKeyAndValue,
updateCurrentPage,
setScrollPosition,
checkForLatestSessions,
} from 'Duck/search';
import useTimeout from 'App/hooks/useTimeout';
import { numberWithCommas } from 'App/utils';
import { fetchListActive as fetchMetadata } from 'Duck/customField';
enum NoContentType {
Bookmarked,
Vaulted,
ToDate,
Bookmarked,
Vaulted,
ToDate,
}
const AUTOREFRESH_INTERVAL = 5 * 60 * 1000;
const PER_PAGE = 10;
let sessionTimeOut: any = null;
interface Props {
loading: boolean;
list: any;
currentPage: number;
pageSize: number;
total: number;
filters: any;
lastPlayedSessionId: string;
@ -39,13 +40,15 @@ interface Props {
fetchMetadata: () => void;
activeTab: any;
isEnterprise?: boolean;
checkForLatestSessions: () => void;
}
function SessionList(props: Props) {
const [noContentType, setNoContentType] = React.useState<NoContentType>(NoContentType.ToDate)
const [noContentType, setNoContentType] = React.useState<NoContentType>(NoContentType.ToDate);
const {
loading,
list,
currentPage,
pageSize,
total,
filters,
lastPlayedSessionId,
@ -60,19 +63,19 @@ function SessionList(props: Props) {
const isVault = isBookmark && isEnterprise;
const NO_CONTENT = React.useMemo(() => {
if (isBookmark && !isEnterprise) {
setNoContentType(NoContentType.Bookmarked)
setNoContentType(NoContentType.Bookmarked);
return {
icon: ICONS.NO_BOOKMARKS,
message: 'No sessions bookmarked.',
};
} else if (isVault) {
setNoContentType(NoContentType.Vaulted)
setNoContentType(NoContentType.Vaulted);
return {
icon: ICONS.NO_SESSIONS_IN_VAULT,
message: 'No sessions found in vault.',
};
}
setNoContentType(NoContentType.ToDate)
setNoContentType(NoContentType.ToDate);
return {
icon: ICONS.NO_SESSIONS,
message: 'No relevant sessions found for the selected time period.',
@ -81,7 +84,7 @@ function SessionList(props: Props) {
useTimeout(() => {
if (!document.hidden) {
props.fetchSessions(null, true);
props.checkForLatestSessions();
}
}, AUTOREFRESH_INTERVAL);
@ -107,7 +110,7 @@ function SessionList(props: Props) {
sessionTimeOut = setTimeout(function () {
if (!document.hidden) {
props.fetchSessions(null, true);
props.checkForLatestSessions();
}
}, 5000);
};
@ -182,15 +185,15 @@ function SessionList(props: Props) {
{total > 0 && (
<div className="flex items-center justify-between p-5">
<div>
Showing <span className="font-medium">{(currentPage - 1) * PER_PAGE + 1}</span> to{' '}
<span className="font-medium">{(currentPage - 1) * PER_PAGE + list.size}</span> of{' '}
Showing <span className="font-medium">{(currentPage - 1) * pageSize + 1}</span> to{' '}
<span className="font-medium">{(currentPage - 1) * pageSize + list.size}</span> of{' '}
<span className="font-medium">{numberWithCommas(total)}</span> sessions.
</div>
<Pagination
page={currentPage}
totalPages={Math.ceil(total / PER_PAGE)}
totalPages={Math.ceil(total / pageSize)}
onPageChange={(page) => props.updateCurrentPage(page)}
limit={PER_PAGE}
limit={pageSize}
debounceRequest={1000}
/>
</div>
@ -210,7 +213,15 @@ export default connect(
total: state.getIn(['sessions', 'total']) || 0,
scrollY: state.getIn(['search', 'scrollY']),
activeTab: state.getIn(['search', 'activeTab']),
pageSize: state.getIn(['search', 'pageSize']),
isEnterprise: state.getIn(['user', 'account', 'edition']) === 'ee',
}),
{ updateCurrentPage, addFilterByKeyAndValue, setScrollPosition, fetchSessions, fetchMetadata }
{
updateCurrentPage,
addFilterByKeyAndValue,
setScrollPosition,
fetchSessions,
fetchMetadata,
checkForLatestSessions,
}
)(SessionList);

View file

@ -5,16 +5,18 @@ import { array, success, createListUpdater, mergeReducers } from './funcTools/to
import Filter from 'Types/filter';
import SavedFilter from 'Types/filter/savedFilter';
import { errors as errorsRoute, isRoute } from 'App/routes';
import { fetchList as fetchSessionList } from './sessions';
import { fetchList as fetchSessionList, fetchAutoplayList } from './sessions';
import { fetchList as fetchErrorsList } from './errors';
import { FilterCategory, FilterKey } from 'Types/filter/filterType';
import { filtersMap, liveFiltersMap, generateFilterOptions } from 'Types/filter/newFilter';
import { DURATION_FILTER } from 'App/constants/storageKeys';
import Period, { CUSTOM_RANGE } from 'Types/app/period';
const ERRORS_ROUTE = errorsRoute();
const name = 'search';
const idKey = 'searchId';
const PER_PAGE = 10;
const FETCH_LIST = fetchListType(name);
const FETCH_FILTER_SEARCH = fetchListType(`${name}/FILTER_SEARCH`);
@ -33,25 +35,30 @@ const SET_ACTIVE_TAB = `${name}/SET_ACTIVE_TAB`;
const SET_SCROLL_POSITION = `${name}/SET_SCROLL_POSITION`;
const REFRESH_FILTER_OPTIONS = 'filters/REFRESH_FILTER_OPTIONS';
const CHECK_LATEST = fetchListType(`${name}/CHECK_LATEST`);
const UPDATE_LATEST_REQUEST_TIME = 'filters/UPDATE_LATEST_REQUEST_TIME'
function chartWrapper(chart = []) {
return chart.map((point) => ({ ...point, count: Math.max(point.count, 0) }));
}
// function chartWrapper(chart = []) {
// return chart.map((point) => ({ ...point, count: Math.max(point.count, 0) }));
// }
const savedSearchIdKey = 'searchId';
const updateItemInList = createListUpdater(savedSearchIdKey);
const updateInstance = (state, instance) =>
state.getIn(['savedSearch', savedSearchIdKey]) === instance[savedSearchIdKey] ? state.mergeIn(['savedSearch'], SavedFilter(instance)) : state;
// const savedSearchIdKey = 'searchId';
// const updateItemInList = createListUpdater(savedSearchIdKey);
// const updateInstance = (state, instance) =>
// state.getIn(['savedSearch', savedSearchIdKey]) === instance[savedSearchIdKey] ? state.mergeIn(['savedSearch'], SavedFilter(instance)) : state;
const initialState = Map({
filterList: generateFilterOptions(filtersMap),
filterListLive: generateFilterOptions(liveFiltersMap),
list: List(),
latestRequestTime: null,
latestList: List(),
alertMetricId: null,
instance: new Filter({ filters: [] }),
savedSearch: new SavedFilter({}),
filterSearchList: {},
currentPage: 1,
pageSize: PER_PAGE,
activeTab: { name: 'All', type: 'all' },
scrollY: 0,
});
@ -73,6 +80,10 @@ function reducer(state = initialState, action = {}) {
'list',
List(data.map(SavedFilter)).sortBy((i) => i.searchId)
);
case UPDATE_LATEST_REQUEST_TIME:
return state.set('latestRequestTime', Date.now()).set('latestList', [])
case success(CHECK_LATEST):
return state.set('latestList', action.data)
case success(FETCH_FILTER_SEARCH):
const groupedList = action.data.reduce((acc, item) => {
const { projectId, type, value } = item;
@ -131,52 +142,69 @@ export const filterMap = ({ category, value, key, operator, sourceOperator, sour
filters: filters ? filters.map(filterMap) : [],
});
const getFilters = (state) => {
const filter = state.getIn(['search', 'instance']).toData();
const activeTab = state.getIn(['search', 'activeTab']);
if (activeTab.type !== 'all' && activeTab.type !== 'bookmark' && activeTab.type !== 'vault') {
const tmpFilter = filtersMap[FilterKey.ISSUE];
tmpFilter.value = [activeTab.type];
filter.filters = filter.filters.concat(tmpFilter);
}
if (activeTab.type === 'bookmark' || activeTab.type === 'vault') {
filter.bookmarked = true;
}
filter.filters = filter.filters.map(filterMap);
// duration filter from local storage
if (!filter.filters.find((f) => f.type === FilterKey.DURATION)) {
const durationFilter = JSON.parse(localStorage.getItem(DURATION_FILTER) || '{"count": 0}');
let durationValue = parseInt(durationFilter.count);
if (durationValue > 0) {
const value = [0];
durationValue = durationFilter.countType === 'min' ? durationValue * 60 * 1000 : durationValue * 1000;
if (durationFilter.operator === '<') {
value[0] = durationValue;
} else if (durationFilter.operator === '>') {
value[1] = durationValue;
}
filter.filters = filter.filters.concat({
type: FilterKey.DURATION,
operator: 'is',
value,
});
}
}
return filter;
}
export const reduceThenFetchResource =
(actionCreator) =>
(...args) =>
(dispatch, getState) => {
dispatch(actionCreator(...args));
const filter = getState().getIn(['search', 'instance']).toData();
const activeTab = getState().getIn(['search', 'activeTab']);
if (activeTab.type === 'notes') return;
if (activeTab.type !== 'all' && activeTab.type !== 'bookmark' && activeTab.type !== 'vault') {
const tmpFilter = filtersMap[FilterKey.ISSUE];
tmpFilter.value = [activeTab.type];
filter.filters = filter.filters.concat(tmpFilter);
}
if (activeTab.type === 'bookmark' || activeTab.type === 'vault') {
filter.bookmarked = true;
}
filter.filters = filter.filters.map(filterMap);
filter.limit = 10;
const filter = getFilters(getState());
filter.limit = PER_PAGE;
filter.page = getState().getIn(['search', 'currentPage']);
const forceFetch = filter.filters.length === 0 || args[1] === true;
// duration filter from local storage
if (!filter.filters.find((f) => f.type === FilterKey.DURATION)) {
const durationFilter = JSON.parse(localStorage.getItem(DURATION_FILTER) || '{"count": 0}');
let durationValue = parseInt(durationFilter.count);
if (durationValue > 0) {
const value = [0];
durationValue = durationFilter.countType === 'min' ? durationValue * 60 * 1000 : durationValue * 1000;
if (durationFilter.operator === '<') {
value[0] = durationValue;
} else if (durationFilter.operator === '>') {
value[1] = durationValue;
}
filter.filters = filter.filters.concat({
type: FilterKey.DURATION,
operator: 'is',
value,
});
}
// reset the timestamps to latest
if (filter.rangeValue !== CUSTOM_RANGE) {
const period = new Period({ rangeName: filter.rangeValue })
const newTimestamps = period.toJSON();
filter.startDate = newTimestamps.startDate
filter.endDate = newTimestamps.endDate
}
dispatch(updateLatestRequestTime())
return isRoute(ERRORS_ROUTE, window.location.pathname) ? dispatch(fetchErrorsList(filter)) : dispatch(fetchSessionList(filter, forceFetch));
};
@ -353,3 +381,33 @@ export const setScrollPosition = (scrollPosition) => {
scrollPosition,
};
};
export const updateLatestRequestTime = () => {
return {
type: UPDATE_LATEST_REQUEST_TIME
}
}
export const checkForLatestSessions = () => (dispatch, getState) => {
const state = getState();
const filter = getFilters(state);
const latestRequestTime = state.getIn(['search', 'latestRequestTime'])
if (!!latestRequestTime) {
const period = new Period({ rangeName: CUSTOM_RANGE, start: latestRequestTime, end: Date.now() })
const newTimestamps = period.toJSON();
filter.startDate = newTimestamps.startDate
filter.endDate = newTimestamps.endDate
}
return dispatch({
types: array(CHECK_LATEST),
call: (client) => client.post(`/sessions/search/ids`, filter),
});
}
export const fetchAutoplaySessions = (page) => (dispatch, getState) => {
const filter = getFilters(getState());
filter.page = page;
filter.limit = PER_PAGE;
return dispatch(fetchAutoplayList(filter));
}

View file

@ -11,6 +11,7 @@ import { getDateRangeFromValue } from 'App/dateRange';
const name = 'sessions';
const INIT = 'sessions/INIT';
const FETCH_LIST = new RequestTypes('sessions/FETCH_LIST');
const FETCH_AUTOPLAY_LIST = new RequestTypes('sessions/FETCH_AUTOPLAY_LIST');
const FETCH = new RequestTypes('sessions/FETCH');
const FETCH_FAVORITE_LIST = new RequestTypes('sessions/FETCH_FAVORITE_LIST');
const FETCH_LIVE_LIST = new RequestTypes('sessions/FETCH_LIVE_LIST');
@ -96,6 +97,10 @@ const reducer = (state = initialState, action = {}) => {
list.filter(({ favorite }) => favorite)
)
.set('total', total);
case FETCH_AUTOPLAY_LIST.SUCCESS:
let sessionIds = state.get('sessionIds');
sessionIds = sessionIds.concat(action.data.map(i => i.sessionId + ''))
return state.set('sessionIds', sessionIds.filter((i, index) => sessionIds.indexOf(i) === index ))
case SET_AUTOPLAY_VALUES: {
const sessionIds = state.get('sessionIds');
const currentSessionId = state.get('current').sessionId;
@ -257,7 +262,7 @@ function init(session) {
export const fetchList =
(params = {}, force = false) =>
(dispatch, getState) => {
(dispatch) => {
if (!force) { // compare with the last fetched filter
const oldFilters = getSessionFilter();
if (compareJsonObjects(oldFilters, cleanSessionFilters(params))) {
@ -273,6 +278,19 @@ export const fetchList =
});
};
export const fetchAutoplayList =
(params = {}) =>
(dispatch) => {
setSessionFilter(cleanSessionFilters(params));
return dispatch({
types: FETCH_AUTOPLAY_LIST.toArray(),
call: (client) => client.post('/sessions/search/ids', params),
params: cleanParams(params),
});
};
export function fetchErrorStackList(sessionId, errorId) {
return {
types: FETCH_ERROR_STACK.toArray(),
@ -436,4 +454,4 @@ export function updateLastPlayedSession(sessionId) {
type: LAST_PLAYED_SESSION_ID,
sessionId,
};
}
}