From 0e99cd7c593b88ce425479d89b21474e58f56cfd Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Fri, 25 Nov 2022 16:54:33 +0100 Subject: [PATCH] feat(ui) - autoplay with pagination and checking for latest sessions, also a fix --- .../Autoplay/{Autoplay.js => Autoplay.tsx} | 63 ++++++-- .../Session_/Autoplay/{index.js => index.ts} | 0 .../SessionListContainer.tsx | 2 + .../LatestSessionsMessage.tsx | 31 ++++ .../components/LatestSessionsMessage/index.ts | 1 + .../components/SessionList/SessionList.tsx | 41 +++-- frontend/app/duck/search.js | 142 ++++++++++++------ frontend/app/duck/sessions.js | 22 ++- 8 files changed, 232 insertions(+), 70 deletions(-) rename frontend/app/components/Session_/Autoplay/{Autoplay.js => Autoplay.tsx} (59%) rename frontend/app/components/Session_/Autoplay/{index.js => index.ts} (100%) create mode 100644 frontend/app/components/shared/SessionListContainer/components/LatestSessionsMessage/LatestSessionsMessage.tsx create mode 100644 frontend/app/components/shared/SessionListContainer/components/LatestSessionsMessage/index.ts diff --git a/frontend/app/components/Session_/Autoplay/Autoplay.js b/frontend/app/components/Session_/Autoplay/Autoplay.tsx similarity index 59% rename from frontend/app/components/Session_/Autoplay/Autoplay.js rename to frontend/app/components/Session_/Autoplay/Autoplay.tsx index 63fdf6841..a83aa3997 100644 --- a/frontend/app/components/Session_/Autoplay/Autoplay.js +++ b/frontend/app/components/Session_/Autoplay/Autoplay.tsx @@ -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; +} +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)) ); diff --git a/frontend/app/components/Session_/Autoplay/index.js b/frontend/app/components/Session_/Autoplay/index.ts similarity index 100% rename from frontend/app/components/Session_/Autoplay/index.js rename to frontend/app/components/Session_/Autoplay/index.ts diff --git a/frontend/app/components/shared/SessionListContainer/SessionListContainer.tsx b/frontend/app/components/shared/SessionListContainer/SessionListContainer.tsx index d61208fc7..6a863e69b 100644 --- a/frontend/app/components/shared/SessionListContainer/SessionListContainer.tsx +++ b/frontend/app/components/shared/SessionListContainer/SessionListContainer.tsx @@ -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({
+ {activeTab !== 'notes' ? : }
); diff --git a/frontend/app/components/shared/SessionListContainer/components/LatestSessionsMessage/LatestSessionsMessage.tsx b/frontend/app/components/shared/SessionListContainer/components/LatestSessionsMessage/LatestSessionsMessage.tsx new file mode 100644 index 000000000..c1283d357 --- /dev/null +++ b/frontend/app/components/shared/SessionListContainer/components/LatestSessionsMessage/LatestSessionsMessage.tsx @@ -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 ? ( +
props.updateCurrentPage(1)} + > + Show {numberWithCommas(count)} new Sessions +
+ ) : ( + <> + ); +} + +export default connect( + (state: any) => ({ + latestSessions: state.getIn(['search', 'latestList']), + }), + { updateCurrentPage } +)(LatestSessionsMessage); diff --git a/frontend/app/components/shared/SessionListContainer/components/LatestSessionsMessage/index.ts b/frontend/app/components/shared/SessionListContainer/components/LatestSessionsMessage/index.ts new file mode 100644 index 000000000..8142abba2 --- /dev/null +++ b/frontend/app/components/shared/SessionListContainer/components/LatestSessionsMessage/index.ts @@ -0,0 +1 @@ +export { default } from './LatestSessionsMessage'; diff --git a/frontend/app/components/shared/SessionListContainer/components/SessionList/SessionList.tsx b/frontend/app/components/shared/SessionListContainer/components/SessionList/SessionList.tsx index c37b2b63d..97c02d4ca 100644 --- a/frontend/app/components/shared/SessionListContainer/components/SessionList/SessionList.tsx +++ b/frontend/app/components/shared/SessionListContainer/components/SessionList/SessionList.tsx @@ -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.ToDate) + const [noContentType, setNoContentType] = React.useState(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 && (
- Showing {(currentPage - 1) * PER_PAGE + 1} to{' '} - {(currentPage - 1) * PER_PAGE + list.size} of{' '} + Showing {(currentPage - 1) * pageSize + 1} to{' '} + {(currentPage - 1) * pageSize + list.size} of{' '} {numberWithCommas(total)} sessions.
props.updateCurrentPage(page)} - limit={PER_PAGE} + limit={pageSize} debounceRequest={1000} />
@@ -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); diff --git a/frontend/app/duck/search.js b/frontend/app/duck/search.js index 3c3aea8fa..569eec435 100644 --- a/frontend/app/duck/search.js +++ b/frontend/app/duck/search.js @@ -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)); +} \ No newline at end of file diff --git a/frontend/app/duck/sessions.js b/frontend/app/duck/sessions.js index 8213a6b57..ecce3d713 100644 --- a/frontend/app/duck/sessions.js +++ b/frontend/app/duck/sessions.js @@ -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, }; -} +} \ No newline at end of file