diff --git a/frontend/app/Router.js b/frontend/app/Router.js index c3a1721a6..d208c1baa 100644 --- a/frontend/app/Router.js +++ b/frontend/app/Router.js @@ -11,6 +11,7 @@ import UpdatePassword from 'Components/UpdatePassword/UpdatePassword'; import ClientPure from 'Components/Client/Client'; import OnboardingPure from 'Components/Onboarding/Onboarding'; import SessionPure from 'Components/Session/Session'; +import AssistPure from 'Components/Assist'; import BugFinderPure from 'Components/BugFinder/BugFinder'; import DashboardPure from 'Components/Dashboard/Dashboard'; import ErrorsPure from 'Components/Errors/Errors'; @@ -29,6 +30,7 @@ import { setSessionPath } from 'Duck/sessions'; const BugFinder = withSiteIdUpdater(BugFinderPure); const Dashboard = withSiteIdUpdater(DashboardPure); const Session = withSiteIdUpdater(SessionPure); +const Assist = withSiteIdUpdater(AssistPure); const Client = withSiteIdUpdater(ClientPure); const Onboarding = withSiteIdUpdater(OnboardingPure); const Errors = withSiteIdUpdater(ErrorsPure); @@ -39,6 +41,7 @@ const withObTab = routes.withObTab; const DASHBOARD_PATH = routes.dashboard(); const SESSIONS_PATH = routes.sessions(); +const ASSIST_PATH = routes.assist(); const ERRORS_PATH = routes.errors(); const ERROR_PATH = routes.error(); const FUNNEL_PATH = routes.funnel(); @@ -145,6 +148,7 @@ class Router extends React.Component { } + diff --git a/frontend/app/components/Assist/Assist.tsx b/frontend/app/components/Assist/Assist.tsx index 74f2095f8..2180bfc8e 100644 --- a/frontend/app/components/Assist/Assist.tsx +++ b/frontend/app/components/Assist/Assist.tsx @@ -1,11 +1,20 @@ import React from 'react'; -import ChatWindow from './ChatWindow'; - +import LiveSessionList from 'Shared/LiveSessionList'; +import LiveSessionSearch from 'Shared/LiveSessionSearch'; +import cn from 'classnames' export default function Assist() { return ( -
- {/* */} +
+
+ {/*
+
*/} +
+ +
+ +
+
) } diff --git a/frontend/app/components/BugFinder/SessionsMenu/SessionsMenu.js b/frontend/app/components/BugFinder/SessionsMenu/SessionsMenu.js index 7275d9ac0..2436d4be6 100644 --- a/frontend/app/components/BugFinder/SessionsMenu/SessionsMenu.js +++ b/frontend/app/components/BugFinder/SessionsMenu/SessionsMenu.js @@ -73,7 +73,7 @@ function SessionsMenu(props) { /> ))} -
+ {/*
onMenuItemClick({ name: 'Assist', type: 'live' })} /> -
+
*/}
diff --git a/frontend/app/components/Header/Header.js b/frontend/app/components/Header/Header.js index a133c42d7..38d958c17 100644 --- a/frontend/app/components/Header/Header.js +++ b/frontend/app/components/Header/Header.js @@ -4,6 +4,7 @@ import { NavLink, withRouter } from 'react-router-dom'; import cn from 'classnames'; import { sessions, + assist, client, errors, dashboard, @@ -27,6 +28,7 @@ import Alerts from '../Alerts/Alerts'; const DASHBOARD_PATH = dashboard(); const SESSIONS_PATH = sessions(); +const ASSIST_PATH = assist(); const ERRORS_PATH = errors(); const CLIENT_PATH = client(CLIENT_DEFAULT_TAB); const AUTOREFRESH_INTERVAL = 30 * 1000; @@ -86,6 +88,13 @@ const Header = (props) => { > { 'Sessions' } + + { 'Assist' } + - { showAssist && }
diff --git a/frontend/app/components/shared/Filters/FilterSelection/FilterSelection.tsx b/frontend/app/components/shared/Filters/FilterSelection/FilterSelection.tsx index 33bbbe3ad..95b8eaa8d 100644 --- a/frontend/app/components/shared/Filters/FilterSelection/FilterSelection.tsx +++ b/frontend/app/components/shared/Filters/FilterSelection/FilterSelection.tsx @@ -4,9 +4,9 @@ import LiveFilterModal from '../LiveFilterModal'; import OutsideClickDetectingDiv from 'Shared/OutsideClickDetectingDiv'; import { Icon } from 'UI'; import { connect } from 'react-redux'; -import { dashboard as dashboardRoute, isRoute } from "App/routes"; +import { assist as assistRoute, isRoute } from "App/routes"; -const DASHBOARD_ROUTE = dashboardRoute(); +const ASSIST_ROUTE = assistRoute(); interface Props { filter?: any; // event/filter @@ -43,7 +43,7 @@ function FilterSelection(props: Props) { {showModal && (
- { (isLive && !isRoute(DASHBOARD_ROUTE, window.location.pathname)) ? : } + { isRoute(ASSIST_ROUTE, window.location.pathname) ? : }
)}
diff --git a/frontend/app/components/shared/LiveSessionList/LiveSessionList.tsx b/frontend/app/components/shared/LiveSessionList/LiveSessionList.tsx new file mode 100644 index 000000000..9fd3f8e0e --- /dev/null +++ b/frontend/app/components/shared/LiveSessionList/LiveSessionList.tsx @@ -0,0 +1,132 @@ +import React, { useEffect } from 'react'; +import { fetchLiveList } from 'Duck/sessions'; +import { connect } from 'react-redux'; +import { NoContent, Loader, LoadMoreButton } from 'UI'; +import { List, Map } from 'immutable'; +import SessionItem from 'Shared/SessionItem'; +import withPermissions from 'HOCs/withPermissions' +import { KEYS } from 'Types/filter/customFilter'; +import { applyFilter, addAttribute } from 'Duck/filters'; +import { FilterCategory, FilterKey } from 'App/types/filter/filterType'; +import { addFilterByKeyAndValue, updateCurrentPage } from 'Duck/liveSearch'; + +const AUTOREFRESH_INTERVAL = .5 * 60 * 1000 +const PER_PAGE = 20; + +interface Props { + loading: Boolean, + list: List, + fetchLiveList: () => Promise, + applyFilter: () => void, + filters: any, + addAttribute: (obj) => void, + addFilterByKeyAndValue: (key: FilterKey, value: string) => void, + updateCurrentPage: (page: number) => void, + currentPage: number, +} + +function LiveSessionList(props: Props) { + const { loading, filters, list, currentPage } = props; + var timeoutId; + const hasUserFilter = filters.map(i => i.key).includes(KEYS.USERID); + const [sessions, setSessions] = React.useState(list); + + const displayedCount = Math.min(currentPage * PER_PAGE, sessions.size); + + const addPage = () => props.updateCurrentPage(props.currentPage + 1) + + useEffect(() => { + if (filters.size === 0) { + props.addFilterByKeyAndValue(FilterKey.USERID, ''); + } + }, []); + + useEffect(() => { + const filteredSessions = filters.size > 0 ? props.list.filter(session => { + let hasValidFilter = true; + filters.forEach(filter => { + if (!hasValidFilter) return; + + const _values = filter.value.filter(i => i !== '' && i !== null && i !== undefined).map(i => i.toLowerCase()); + if (filter.key === FilterKey.USERID) { + const _userId = session.userId ? session.userId.toLowerCase() : ''; + hasValidFilter = _values.length > 0 ? (_values.includes(_userId) && hasValidFilter) || _values.some(i => _userId.includes(i)) : hasValidFilter; + } + if (filter.category === FilterCategory.METADATA) { + const _source = session.metadata[filter.key] ? session.metadata[filter.key].toLowerCase() : ''; + hasValidFilter = _values.length > 0 ? (_values.includes(_source) && hasValidFilter) || _values.some(i => _source.includes(i)) : hasValidFilter; + } + }) + return hasValidFilter; + }) : props.list; + setSessions(filteredSessions); + }, [filters, list]); + + useEffect(() => { + props.fetchLiveList(); + timeout(); + return () => { + clearTimeout(timeoutId) + } + }, []) + + const onUserClick = (userId, userAnonymousId) => { + if (userId) { + props.addFilterByKeyAndValue(FilterKey.USERID, userId); + } else { + props.addFilterByKeyAndValue(FilterKey.USERANONYMOUSID, userAnonymousId); + } + } + + const timeout = () => { + timeoutId = setTimeout(() => { + props.fetchLiveList(); + timeout(); + }, AUTOREFRESH_INTERVAL); + } + + return ( +
+ + See how to {'enable Assist'} and ensure you're using tracker-assist v3.5.0 or higher. + + } + image={} + show={ !loading && sessions && sessions.size === 0} + > + + {sessions && sessions.take(displayedCount).map(session => ( + + ))} + + + + +
+ ) +} + +export default withPermissions(['ASSIST_LIVE', 'SESSION_REPLAY'])(connect( + (state) => ({ + list: state.getIn(['sessions', 'liveSessions']), + loading: state.getIn([ 'sessions', 'loading' ]), + filters: state.getIn([ 'liveSearch', 'instance', 'filters' ]), + currentPage: state.getIn(["liveSearch", "currentPage"]), + }), + { fetchLiveList, applyFilter, addAttribute, addFilterByKeyAndValue, updateCurrentPage } +)(LiveSessionList)); diff --git a/frontend/app/components/shared/LiveSessionList/index.js b/frontend/app/components/shared/LiveSessionList/index.js new file mode 100644 index 000000000..eb38fa3e7 --- /dev/null +++ b/frontend/app/components/shared/LiveSessionList/index.js @@ -0,0 +1 @@ +export { default } from './LiveSessionList' \ No newline at end of file diff --git a/frontend/app/components/shared/SessionItem/ErrorBars/ErrorBars.tsx b/frontend/app/components/shared/SessionItem/ErrorBars/ErrorBars.tsx new file mode 100644 index 000000000..64c7f91b4 --- /dev/null +++ b/frontend/app/components/shared/SessionItem/ErrorBars/ErrorBars.tsx @@ -0,0 +1,36 @@ +import React from 'react' +import cn from 'classnames' + +const GOOD = 'Good' +const LESS_CRITICAL = 'Less Critical' +const CRITICAL = 'Critical' +const getErrorState = (count: number) => { + if (count === 0) { return GOOD } + if (count < 2) { return LESS_CRITICAL } + return CRITICAL +} + + +interface Props { + count?: number +} +export default function ErrorBars(props: Props) { + const { count = 2 } = props + const state = React.useCallback(() => getErrorState(count), [count])() + const bgColor = { 'bg-red' : state === CRITICAL, 'bg-green' : state === GOOD, 'bg-red2' : state === LESS_CRITICAL } + return ( +
+
+
+ { (state === GOOD || state === LESS_CRITICAL || state === CRITICAL) &&
} + { (state === GOOD || state === CRITICAL) &&
} +
+
+
+
+
+
+
{state}
+
+ ) +} diff --git a/frontend/app/components/shared/SessionItem/ErrorBars/index.ts b/frontend/app/components/shared/SessionItem/ErrorBars/index.ts new file mode 100644 index 000000000..b6291d438 --- /dev/null +++ b/frontend/app/components/shared/SessionItem/ErrorBars/index.ts @@ -0,0 +1 @@ +export { default } from './ErrorBars'; \ No newline at end of file diff --git a/frontend/app/components/shared/SessionItem/SessionItem.js b/frontend/app/components/shared/SessionItem/SessionItem.js index 3abf12ca8..7d39e8e4e 100644 --- a/frontend/app/components/shared/SessionItem/SessionItem.js +++ b/frontend/app/components/shared/SessionItem/SessionItem.js @@ -18,6 +18,8 @@ import LiveTag from 'Shared/LiveTag'; import Bookmark from 'Shared/Bookmark'; import Counter from './Counter' import { withRouter } from 'react-router-dom'; +import SessionMetaList from './SessionMetaList'; +import ErrorBars from './ErrorBars'; const Label = ({ label = '', color = 'color-gray-medium'}) => (
{label}
@@ -61,64 +63,69 @@ export default class SessionItem extends React.PureComponent { const hasUserId = userId || userAnonymousId; return ( -
-
-
- -
-
(!disableUser && !hasUserFilter && hasUserId) && onUserClick(userId, userAnonymousId)} - > - +
+
+
+
+
+
+
(!disableUser && !hasUserFilter && hasUserId) && onUserClick(userId, userAnonymousId)} + > + {userDisplayName} +
+
30 Sessions
-
+
+
{formatTimeOrDate(startedAt, timezone) }
+
+ {!live && ( +
+ { eventsCount } + { eventsCount === 0 || eventsCount > 1 ? 'Events' : 'Event' } +
+ )} + - +
{ live ? : formattedDuration }
+
+
+
+
+ +
+ {userBrowser} - + {userOs} - + {userDeviceType} +
+
+
+
+
-
-
- - - - -
-
-
-
- { live ? : formattedDuration } -
-
- {!live && ( -
-
{ eventsCount }
-
+ ) +} diff --git a/frontend/app/components/shared/SessionItem/SessionMetaList/index.ts b/frontend/app/components/shared/SessionItem/SessionMetaList/index.ts new file mode 100644 index 000000000..18ad742da --- /dev/null +++ b/frontend/app/components/shared/SessionItem/SessionMetaList/index.ts @@ -0,0 +1 @@ +export { default } from './SessionMetaList'; \ No newline at end of file diff --git a/frontend/app/components/shared/SessionItem/sessionItem.css b/frontend/app/components/shared/SessionItem/sessionItem.css index cbf7bb2d1..f7fcde842 100644 --- a/frontend/app/components/shared/SessionItem/sessionItem.css +++ b/frontend/app/components/shared/SessionItem/sessionItem.css @@ -12,12 +12,12 @@ user-select: none; @mixin defaultHover; border-radius: 3px; - padding: 10px 10px; - padding-right: 15px; - margin-bottom: 15px; - background-color: white; - display: flex; - align-items: center; + /* padding: 10px 10px; */ + /* padding-right: 15px; */ + /* margin-bottom: 15px; */ + /* background-color: white; */ + /* display: flex; */ + /* align-items: center; */ border: solid thin #EEEEEE; & .favorite { diff --git a/frontend/app/routes.js b/frontend/app/routes.js index cdccc6327..0f10950df 100644 --- a/frontend/app/routes.js +++ b/frontend/app/routes.js @@ -82,6 +82,7 @@ const routerOBTabString = `:activeTab(${ Object.values(OB_TABS).join('|') })`; export const onboarding = (tab = routerOBTabString) => `/onboarding/${ tab }`; export const sessions = params => queried('/sessions', params); +export const assist = params => queried('/assist', params); export const session = (sessionId = ':sessionId', hash) => hashed(`/session/${ sessionId }`, hash); export const liveSession = (sessionId = ':sessionId', hash) => hashed(`/live/session/${ sessionId }`, hash); @@ -105,7 +106,7 @@ export const METRICS_QUERY_KEY = 'metrics'; export const SOURCE_QUERY_KEY = 'source'; export const WIDGET_QUERY_KEY = 'widget'; -const REQUIRED_SITE_ID_ROUTES = [ liveSession(''), session(''), sessions(), dashboard(''), error(''), errors(), onboarding(''), funnel(''), funnelIssue(''), ]; +const REQUIRED_SITE_ID_ROUTES = [ liveSession(''), session(''), sessions(), assist(), dashboard(''), error(''), errors(), onboarding(''), funnel(''), funnelIssue(''), ]; const routeNeedsSiteId = path => REQUIRED_SITE_ID_ROUTES.some(r => path.startsWith(r)); const siteIdToUrl = (siteId = ':siteId') => { if (Array.isArray(siteId)) { @@ -128,7 +129,7 @@ export function isRoute(route, path){ routeParts.every((p, i) => p.startsWith(':') || p === pathParts[ i ]); } -const SITE_CHANGE_AVALIABLE_ROUTES = [ sessions(), dashboard(), errors(), onboarding('')]; +const SITE_CHANGE_AVALIABLE_ROUTES = [ sessions(), assist(), dashboard(), errors(), onboarding('')]; export const siteChangeAvaliable = path => SITE_CHANGE_AVALIABLE_ROUTES.some(r => isRoute(r, path)); diff --git a/frontend/app/types/filter/filter.js b/frontend/app/types/filter/filter.js index be186e4f9..df31f1d0e 100644 --- a/frontend/app/types/filter/filter.js +++ b/frontend/app/types/filter/filter.js @@ -34,6 +34,7 @@ export default Record({ rangeValue, startDate, endDate, + // groupByUser: true, sort: 'startTs', order: 'desc',