diff --git a/frontend/app/api_client.js b/frontend/app/api_client.js
index df531c956..6de7eb121 100644
--- a/frontend/app/api_client.js
+++ b/frontend/app/api_client.js
@@ -28,7 +28,7 @@ const siteIdRequiredPaths = [
'/unprocessed',
'/notes',
'/feature-flags',
- // '/custom_metrics/sessions',
+ '/check-recording-status'
];
const noStoringFetchPathStarts = [
diff --git a/frontend/app/components/shared/AnimatedSVG/AnimatedSVG.tsx b/frontend/app/components/shared/AnimatedSVG/AnimatedSVG.tsx
index e209fefa5..3374236bd 100644
--- a/frontend/app/components/shared/AnimatedSVG/AnimatedSVG.tsx
+++ b/frontend/app/components/shared/AnimatedSVG/AnimatedSVG.tsx
@@ -25,6 +25,7 @@ export enum ICONS {
NO_DASHBOARDS = 'ca-no-dashboards',
NO_PROJECTS = 'ca-no-projects',
NO_FFLAGS = 'no-fflags',
+ PROCESSING = 'ca-processing',
}
const ICONS_SVGS = {
@@ -52,6 +53,7 @@ const ICONS_SVGS = {
[ICONS.NO_DASHBOARDS]: require('../../../svg/ca-no-dashboards.svg').default,
[ICONS.NO_PROJECTS]: require('../../../svg/ca-no-projects.svg').default,
[ICONS.NO_FFLAGS]: require('../../../svg/no-fflags.svg').default,
+ [ICONS.PROCESSING]: require('../../../svg/ca-processing.svg').default,
};
interface Props {
diff --git a/frontend/app/components/shared/SessionsTabOverview/SessionsTabOverview.tsx b/frontend/app/components/shared/SessionsTabOverview/SessionsTabOverview.tsx
index 8080bae32..07266a103 100644
--- a/frontend/app/components/shared/SessionsTabOverview/SessionsTabOverview.tsx
+++ b/frontend/app/components/shared/SessionsTabOverview/SessionsTabOverview.tsx
@@ -2,31 +2,42 @@ import React from 'react';
import SessionList from './components/SessionList';
import SessionHeader from './components/SessionHeader';
import NotesList from './components/Notes/NoteList';
-import { connect } from 'react-redux';
+import { connect, DefaultRootState } from 'react-redux';
import LatestSessionsMessage from './components/LatestSessionsMessage';
+import RecordingStatus from 'Shared/SessionsTabOverview/components/RecordingStatus';
function SessionsTabOverview({
- activeTab,
- members,
-}: {
+ activeTab,
+ members,
+ sites,
+ siteId
+ }: {
activeTab: string;
members: object[];
+ sites: object[];
+ siteId: string;
}) {
+ const activeSite: any = sites.find((s: any) => s.id === siteId);
+ const hasNoRecordings = !activeSite || !activeSite.recorded;
+
return (
-
+
-
+
- {activeTab !== 'notes' ?
:
}
+ {activeTab !== 'notes' ?
:
+
}
);
}
export default connect(
- (state) => ({
+ (state: any) => ({
// @ts-ignore
activeTab: state.getIn(['search', 'activeTab', 'type']),
// @ts-ignore
members: state.getIn(['members', 'list']),
- }),
+ siteId: state.getIn(['site', 'siteId']),
+ sites: state.getIn(['site', 'list'])
+ })
)(SessionsTabOverview);
diff --git a/frontend/app/components/shared/SessionsTabOverview/components/RecordingStatus.tsx b/frontend/app/components/shared/SessionsTabOverview/components/RecordingStatus.tsx
new file mode 100644
index 000000000..d522b9481
--- /dev/null
+++ b/frontend/app/components/shared/SessionsTabOverview/components/RecordingStatus.tsx
@@ -0,0 +1,37 @@
+import React from 'react';
+import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
+import { Icon } from 'UI';
+
+interface Props {
+ data: any;
+}
+
+function RecordingStatus(props: Props) {
+ const { data } = props;
+
+
+ return (
+
+
+
+
Processing your first session.
+
+
+
+
+
Your tracker seems to be correctly setup!
+
+
+
+
+
+
There are {data.count} ongoing session(s). Once they're complete they'll show up here within a few
+ minutes.
+
+
+
+
+ );
+}
+
+export default RecordingStatus;
\ No newline at end of file
diff --git a/frontend/app/components/shared/SessionsTabOverview/components/SessionList/SessionList.tsx b/frontend/app/components/shared/SessionsTabOverview/components/SessionList/SessionList.tsx
index 468246608..b1f3417da 100644
--- a/frontend/app/components/shared/SessionsTabOverview/components/SessionList/SessionList.tsx
+++ b/frontend/app/components/shared/SessionsTabOverview/components/SessionList/SessionList.tsx
@@ -10,12 +10,15 @@ import {
addFilterByKeyAndValue,
updateCurrentPage,
setScrollPosition,
- checkForLatestSessions,
+ checkForLatestSessions
} from 'Duck/search';
import { numberWithCommas } from 'App/utils';
import { fetchListActive as fetchMetadata } from 'Duck/customField';
import { toggleFavorite } from 'Duck/sessions';
import SessionDateRange from './SessionDateRange';
+import RecordingStatus from 'Shared/SessionsTabOverview/components/RecordingStatus';
+import { sessionService } from 'App/services';
+import { updateProjectRecordingStatus } from 'Duck/site';
enum NoContentType {
Bookmarked,
@@ -23,8 +26,17 @@ enum NoContentType {
ToDate,
}
+type SessionStatus = {
+ status: number;
+ count: number;
+}
+
const AUTOREFRESH_INTERVAL = 5 * 60 * 1000;
let sessionTimeOut: any = null;
+let sessionStatusTimeOut: any = null;
+
+const STATUS_FREQUENCY = 5000;
+
interface Props extends RouteComponentProps {
loading: boolean;
list: any;
@@ -40,11 +52,15 @@ interface Props extends RouteComponentProps {
setScrollPosition: (scrollPosition: number) => void;
fetchSessions: (filters: any, force: boolean) => void;
fetchMetadata: () => void;
+ updateProjectRecordingStatus: (siteId: string, status: boolean) => void;
activeTab: any;
isEnterprise?: boolean;
checkForLatestSessions: () => void;
toggleFavorite: (sessionId: string) => Promise
;
+ sites: object[];
+ siteId: string;
}
+
function SessionList(props: Props) {
const [noContentType, setNoContentType] = React.useState(NoContentType.ToDate);
const {
@@ -58,48 +74,91 @@ function SessionList(props: Props) {
metaList,
activeTab,
isEnterprise = false,
+ sites,
+ siteId
} = props;
const _filterKeys = filters.map((i: any) => i.key);
const hasUserFilter =
_filterKeys.includes(FilterKey.USERID) || _filterKeys.includes(FilterKey.USERANONYMOUSID);
const isBookmark = activeTab.type === 'bookmark';
const isVault = isBookmark && isEnterprise;
+ const activeSite: any = sites.find((s: any) => s.id === siteId);
+ const hasNoRecordings = !activeSite || !activeSite.recorded;
+
const NO_CONTENT = React.useMemo(() => {
if (isBookmark && !isEnterprise) {
setNoContentType(NoContentType.Bookmarked);
return {
icon: ICONS.NO_BOOKMARKS,
- message: 'No sessions bookmarked.',
+ message: 'No sessions bookmarked.'
};
} else if (isVault) {
setNoContentType(NoContentType.Vaulted);
return {
icon: ICONS.NO_SESSIONS_IN_VAULT,
- message: 'No sessions found in vault.',
+ message: 'No sessions found in vault.'
};
}
setNoContentType(NoContentType.ToDate);
return {
icon: ICONS.NO_SESSIONS,
- message: ,
+ message:
};
}, [isBookmark, isVault, activeTab]);
+ const [statusData, setStatusData] = React.useState({ status: 0, count: 0 });
+
+
+ const fetchStatus = async () => {
+ const response = await sessionService.getRecordingStatus();
+ setStatusData({
+ status: response.recording_status,
+ count: response.sessions_count
+ });
+ };
+
+
+ useEffect(() => {
+ if (!hasNoRecordings) {
+ return;
+ }
+
+ fetchStatus();
+
+ sessionStatusTimeOut = setInterval(() => {
+ fetchStatus();
+ }, STATUS_FREQUENCY);
+
+ return () => clearInterval(sessionStatusTimeOut);
+ }, [hasNoRecordings]);
+
+
+ useEffect(() => {
+ if (!hasNoRecordings && statusData.status === 0) {
+ return;
+ }
+
+ if (statusData.status === 2) { // recording && processed
+ props.updateProjectRecordingStatus(activeSite.id, true);
+ props.fetchSessions(null, true);
+ clearInterval(sessionStatusTimeOut);
+ }
+ }, [statusData]);
useEffect(() => {
const id = setInterval(() => {
if (!document.hidden) {
- props.checkForLatestSessions()
+ props.checkForLatestSessions();
}
- }, AUTOREFRESH_INTERVAL)
- return () => clearInterval(id)
- }, [])
+ }, AUTOREFRESH_INTERVAL);
+ return () => clearInterval(id);
+ }, []);
useEffect(() => {
// handle scroll position
const { scrollY } = props;
window.scrollTo(0, scrollY);
- if (total === 0 && !loading) {
+ if (total === 0 && !loading && !hasNoRecordings) {
setTimeout(() => {
props.fetchSessions(null, true);
}, 300);
@@ -117,7 +176,7 @@ function SessionList(props: Props) {
return;
}
- sessionTimeOut = setTimeout(function () {
+ sessionTimeOut = setTimeout(function() {
if (!document.hidden) {
props.checkForLatestSessions();
}
@@ -147,73 +206,73 @@ function SessionList(props: Props) {
return (
-
-
-
-
- {NO_CONTENT.message}
- {/* {noContentType === NoContentType.ToDate ? (
-
-
+ {hasNoRecordings && statusData.status >= 1 ?
: (
+ <>
+
+
+
+
+ {NO_CONTENT.message}
- ) : null} */}
-
-
- }
- subtext={
-
- {(isVault || isBookmark) && (
-
- {isVault
- ? 'Extend the retention period of any session by adding it to your vault directly from the player screen.'
- : 'Effortlessly find important sessions by bookmarking them directly from the player screen.'}
- )}
-
props.fetchSessions(null, true)}
- >
- Refresh
-
-
- }
- show={!loading && list.length === 0}
- >
- {list.map((session: any) => (
-
-
-
- ))}
-
+ }
+ subtext={
+
+ {(isVault || isBookmark) && (
+
+ {isVault
+ ? 'Extend the retention period of any session by adding it to your vault directly from the player screen.'
+ : 'Effortlessly find important sessions by bookmarking them directly from the player screen.'}
+
+ )}
+
props.fetchSessions(null, true)}
+ >
+ Refresh
+
+
+ }
+ show={!loading && list.length === 0}
+ >
+ {list.map((session: any) => (
+
+
+
+ ))}
+
+
+ {total > 0 && (
+
+
+ Showing {(currentPage - 1) * pageSize + 1} to{' '}
+ {(currentPage - 1) * pageSize + list.length} of{' '}
+ {numberWithCommas(total)} sessions.
+
+
props.updateCurrentPage(page)}
+ limit={pageSize}
+ debounceRequest={1000}
+ />
+
+ )}
+ >
- {total > 0 && (
-
-
- Showing {(currentPage - 1) * pageSize + 1} to{' '}
- {(currentPage - 1) * pageSize + list.length} of{' '}
- {numberWithCommas(total)} sessions.
-
-
props.updateCurrentPage(page)}
- limit={pageSize}
- debounceRequest={1000}
- />
-
)}
);
@@ -232,6 +291,8 @@ export default connect(
activeTab: state.getIn(['search', 'activeTab']),
pageSize: state.getIn(['search', 'pageSize']),
isEnterprise: state.getIn(['user', 'account', 'edition']) === 'ee',
+ siteId: state.getIn(['site', 'siteId']),
+ sites: state.getIn(['site', 'list'])
}),
{
updateCurrentPage,
@@ -241,5 +302,6 @@ export default connect(
fetchMetadata,
checkForLatestSessions,
toggleFavorite,
+ updateProjectRecordingStatus
}
)(withRouter(SessionList));
diff --git a/frontend/app/duck/site.js b/frontend/app/duck/site.js
index 348642f16..9678b94a9 100644
--- a/frontend/app/duck/site.js
+++ b/frontend/app/duck/site.js
@@ -1,30 +1,30 @@
import Site from 'Types/site';
import GDPR from 'Types/site/gdpr';
-import {
- mergeReducers,
- createItemInListUpdater,
- success,
- array,
- createListUpdater,
+import {
+ mergeReducers,
+ createItemInListUpdater,
+ success,
+ array,
+ createListUpdater
} from './funcTools/tools';
-import {
- createCRUDReducer,
- getCRUDRequestTypes,
- createInit,
- createEdit,
- createRemove,
- createUpdate,
- saveType,
+import {
+ createCRUDReducer,
+ getCRUDRequestTypes,
+ createInit,
+ createEdit,
+ createRemove,
+ createUpdate,
+ saveType
} from './funcTools/crud';
import { createRequestReducer } from './funcTools/request';
-import { Map, List, fromJS } from "immutable";
+import { Map, List, fromJS } from 'immutable';
import { GLOBAL_HAS_NO_RECORDINGS, SITE_ID_STORAGE_KEY } from 'App/constants/storageKeys';
const storedSiteId = localStorage.getItem(SITE_ID_STORAGE_KEY);
const name = 'project';
const idKey = 'id';
-const itemInListUpdater = createItemInListUpdater(idKey)
+const itemInListUpdater = createItemInListUpdater(idKey);
const updateItemInList = createListUpdater(idKey);
const EDIT_GDPR = 'sites/EDIT_GDPR';
@@ -37,94 +37,101 @@ const SAVE_GDPR_SUCCESS = success(SAVE_GDPR);
const FETCH_LIST_SUCCESS = success(FETCH_LIST);
const SAVE = saveType('sites/SAVE');
+const UPDATE_PROJECT_RECORDING_STATUS = 'sites/UPDATE_PROJECT_RECORDING_STATUS';
+
const initialState = Map({
- list: List(),
- instance: fromJS(),
- remainingSites: undefined,
- siteId: null,
- active: null,
+ list: List(),
+ instance: fromJS(),
+ remainingSites: undefined,
+ siteId: null,
+ active: null
});
const reducer = (state = initialState, action = {}) => {
- switch(action.type) {
- case EDIT_GDPR:
- return state.mergeIn([ 'instance', 'gdpr' ], action.gdpr);
- case FETCH_GDPR_SUCCESS:
- return state.mergeIn([ 'instance', 'gdpr' ], action.data);
- case success(SAVE):
- const newSite = Site(action.data);
- return updateItemInList(state, newSite)
- .set('siteId', newSite.get('id'))
- .set('active', newSite);
- case SAVE_GDPR_SUCCESS:
- const gdpr = GDPR(action.data);
- return state.setIn([ 'instance', 'gdpr' ], gdpr);
- case FETCH_LIST_SUCCESS:
- let siteId = state.get("siteId");
- const siteIds = action.data.map(s => parseInt(s.projectId))
- const siteExists = siteIds.includes(siteId);
- if (action.siteIdFromPath && siteIds.includes(parseInt(action.siteIdFromPath))) {
- siteId = action.siteIdFromPath;
- } else if (!siteId || !siteExists) {
- siteId = siteIds.includes(parseInt(storedSiteId))
- ? storedSiteId
- : action.data[0].projectId;
- }
- const list = List(action.data.map(Site));
- const hasRecordings = list.some(s => s.recorded);
- if (!hasRecordings) {
- localStorage.setItem(GLOBAL_HAS_NO_RECORDINGS, true)
- } else {
- localStorage.removeItem(GLOBAL_HAS_NO_RECORDINGS)
- }
-
- return state.set('list', list)
- .set('siteId', siteId)
- .set('active', list.find(s => parseInt(s.id) === parseInt(siteId)));
- case SET_SITE_ID:
- const _siteId = action.siteId ? action.siteId : state.get('list').get(0).id;
- localStorage.setItem(SITE_ID_STORAGE_KEY, _siteId)
- const site = state.get('list').find(s => parseInt(s.id) == _siteId);
- return state.set('siteId', _siteId).set('active', site);
- }
- return state;
+ switch (action.type) {
+ case EDIT_GDPR:
+ return state.mergeIn(['instance', 'gdpr'], action.gdpr);
+ case FETCH_GDPR_SUCCESS:
+ return state.mergeIn(['instance', 'gdpr'], action.data);
+ case success(SAVE):
+ const newSite = Site(action.data);
+ return updateItemInList(state, newSite)
+ .set('siteId', newSite.get('id'))
+ .set('active', newSite);
+ case SAVE_GDPR_SUCCESS:
+ const gdpr = GDPR(action.data);
+ return state.setIn(['instance', 'gdpr'], gdpr);
+ case FETCH_LIST_SUCCESS:
+ let siteId = state.get('siteId');
+ const siteIds = action.data.map(s => parseInt(s.projectId));
+ const siteExists = siteIds.includes(siteId);
+ if (action.siteIdFromPath && siteIds.includes(parseInt(action.siteIdFromPath))) {
+ siteId = action.siteIdFromPath;
+ } else if (!siteId || !siteExists) {
+ siteId = siteIds.includes(parseInt(storedSiteId))
+ ? storedSiteId
+ : action.data[0].projectId;
+ }
+ const list = List(action.data.map(Site));
+ const hasRecordings = list.some(s => s.recorded);
+ if (!hasRecordings) {
+ localStorage.setItem(GLOBAL_HAS_NO_RECORDINGS, true);
+ } else {
+ localStorage.removeItem(GLOBAL_HAS_NO_RECORDINGS);
+ }
+
+ return state.set('list', list)
+ .set('siteId', siteId)
+ .set('active', list.find(s => parseInt(s.id) === parseInt(siteId)));
+ case SET_SITE_ID:
+ const _siteId = action.siteId ? action.siteId : state.get('list').get(0).id;
+ localStorage.setItem(SITE_ID_STORAGE_KEY, _siteId);
+ const site = state.get('list').find(s => parseInt(s.id) == _siteId);
+ return state.set('siteId', _siteId).set('active', site);
+ case UPDATE_PROJECT_RECORDING_STATUS:
+ const { siteId: _siteIdToUpdate, status } = action;
+ const siteToUpdate = state.get('list').find(s => parseInt(s.id) === parseInt(_siteIdToUpdate));
+ const updatedSite = siteToUpdate.set('recorded', status);
+ return updateItemInList(state, updatedSite);
+ }
+ return state;
};
export function editGDPR(gdpr) {
return {
type: EDIT_GDPR,
- gdpr,
+ gdpr
};
}
export function fetchGDPR(siteId) {
return {
types: array(FETCH_GDPR),
- call: client => client.get(`/${ siteId }/gdpr`),
- }
+ call: client => client.get(`/${siteId}/gdpr`)
+ };
}
export const saveGDPR = (siteId, gdpr) => (dispatch, getState) => {
const g = getState().getIn(['site', 'instance', 'gdpr']);
return dispatch({
types: array(SAVE_GDPR),
- call: client => client.post(`/${ siteId }/gdpr`, g.toData()),
+ call: client => client.post(`/${siteId}/gdpr`, g.toData())
});
-}
+};
export function fetchList(siteId) {
- return {
- types: array(FETCH_LIST),
- call: client => client.get('/projects'),
- siteIdFromPath: siteId
- };
+ return {
+ types: array(FETCH_LIST),
+ call: client => client.get('/projects'),
+ siteIdFromPath: siteId
+ };
}
export function save(site) {
- return {
- types: array(SAVE),
- call: client => client.post(`/projects`, site.toData()),
- }
+ return {
+ types: array(SAVE),
+ call: client => client.post(`/projects`, site.toData())
+ };
}
// export const fetchList = createFetchList(name);
@@ -135,18 +142,26 @@ export const update = createUpdate(name);
export const remove = createRemove(name);
export function setSiteId(siteId) {
- return {
- type: SET_SITE_ID,
- siteId,
- };
- }
+ return {
+ type: SET_SITE_ID,
+ siteId
+ };
+}
+
+export const updateProjectRecordingStatus = (siteId, status) => {
+ return {
+ type: UPDATE_PROJECT_RECORDING_STATUS,
+ siteId,
+ status
+ };
+};
export default mergeReducers(
- reducer,
- createCRUDReducer(name, Site, idKey),
- createRequestReducer({
- saveGDPR: SAVE_GDPR,
- ...getCRUDRequestTypes(name),
- }),
+ reducer,
+ createCRUDReducer(name, Site, idKey),
+ createRequestReducer({
+ saveGDPR: SAVE_GDPR,
+ ...getCRUDRequestTypes(name)
+ })
);
diff --git a/frontend/app/services/SessionService.ts b/frontend/app/services/SessionService.ts
index aaf511873..b438c95f7 100644
--- a/frontend/app/services/SessionService.ts
+++ b/frontend/app/services/SessionService.ts
@@ -78,4 +78,12 @@ export default class SettingsService {
.then(j => j.data || [])
.catch(Promise.reject)
}
+
+ getRecordingStatus(): Promise {
+ return this.client
+ .get('/check-recording-status')
+ .then(r => r.json())
+ .then(j => j.data || {})
+ .catch(Promise.reject)
+ }
}
diff --git a/frontend/app/svg/ca-processing.svg b/frontend/app/svg/ca-processing.svg
new file mode 100644
index 000000000..b5bd49272
--- /dev/null
+++ b/frontend/app/svg/ca-processing.svg
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+