feat ui: remake cobrowse subtabs (#2086)
This commit is contained in:
parent
217201eb5c
commit
d008b65ac0
13 changed files with 92 additions and 111 deletions
|
|
@ -23,7 +23,6 @@ const components: any = {
|
|||
FunnelIssueDetails: lazy(() => import('Components/Funnels/FunnelIssueDetails')),
|
||||
FunnelPagePure: lazy(() => import('Components/Funnels/FunnelPage')),
|
||||
MultiviewPure: lazy(() => import('Components/Session_/Multiview/Multiview')),
|
||||
AssistStatsPure: lazy(() => import('Components/AssistStats')),
|
||||
UsabilityTestingPure: lazy(() => import('Components/UsabilityTesting/UsabilityTesting')),
|
||||
UsabilityTestEditPure: lazy(() => import('Components/UsabilityTesting/TestEdit')),
|
||||
UsabilityTestOverviewPure: lazy(() => import('Components/UsabilityTesting/TestOverview')),
|
||||
|
|
@ -41,7 +40,6 @@ const enhancedComponents: any = {
|
|||
FunnelsDetails: withSiteIdUpdater(components.FunnelDetailsPure),
|
||||
FunnelIssue: withSiteIdUpdater(components.FunnelIssueDetails),
|
||||
Multiview: withSiteIdUpdater(components.MultiviewPure),
|
||||
AssistStats: withSiteIdUpdater(components.AssistStatsPure),
|
||||
UsabilityTesting: withSiteIdUpdater(components.UsabilityTestingPure),
|
||||
UsabilityTestEdit: withSiteIdUpdater(components.UsabilityTestEditPure),
|
||||
UsabilityTestOverview: withSiteIdUpdater(components.UsabilityTestOverviewPure),
|
||||
|
|
@ -192,12 +190,6 @@ function PrivateRoutes(props: Props) {
|
|||
path={withSiteId(RECORDINGS_PATH, siteIdList)}
|
||||
component={enhancedComponents.Assist}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
strict
|
||||
path={withSiteId(ASSIST_STATS_PATH, siteIdList)}
|
||||
component={enhancedComponents.AssistStats}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
strict
|
||||
|
|
|
|||
|
|
@ -1,37 +1,15 @@
|
|||
import React from 'react';
|
||||
import { Switch, Route } from 'react-router';
|
||||
import { withRouter, RouteComponentProps } from 'react-router-dom';
|
||||
|
||||
import {
|
||||
assist,
|
||||
recordings,
|
||||
withSiteId,
|
||||
} from 'App/routes';
|
||||
import AssistView from './AssistView'
|
||||
import Recordings from './RecordingsList/Recordings'
|
||||
|
||||
interface Props extends RouteComponentProps {
|
||||
match: any;
|
||||
}
|
||||
|
||||
function AssistRouter(props: Props) {
|
||||
const {
|
||||
match: {
|
||||
params: { siteId },
|
||||
},
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<Switch>
|
||||
<Route exact strict path={withSiteId(assist(), siteId)}>
|
||||
<AssistView />
|
||||
</Route>
|
||||
|
||||
<Route exact strict path={withSiteId(recordings(), siteId)}>
|
||||
<Recordings />
|
||||
</Route>
|
||||
</Switch>
|
||||
<AssistView />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,43 +1,68 @@
|
|||
import React from 'react';
|
||||
import { Button } from 'UI';
|
||||
import SessionSearchField from 'Shared/SessionSearchField';
|
||||
import { connect } from 'react-redux';
|
||||
import { edit as editFilter, addFilterByKeyAndValue, clearSearch, fetchFilterSearch } from 'Duck/liveSearch';
|
||||
|
||||
import {
|
||||
addFilterByKeyAndValue,
|
||||
clearSearch,
|
||||
edit as editFilter,
|
||||
fetchFilterSearch,
|
||||
} from 'Duck/liveSearch';
|
||||
import { Button } from 'UI';
|
||||
import { useModal } from 'App/components/Modal';
|
||||
import SessionSearchField from 'Shared/SessionSearchField';
|
||||
|
||||
import AssistStats from '../../AssistStats';
|
||||
import Recordings from '../RecordingsList/Recordings'
|
||||
|
||||
interface Props {
|
||||
appliedFilter: any;
|
||||
fetchFilterSearch: any;
|
||||
addFilterByKeyAndValue: any;
|
||||
clearSearch: any;
|
||||
appliedFilter: any;
|
||||
fetchFilterSearch: any;
|
||||
addFilterByKeyAndValue: any;
|
||||
clearSearch: any;
|
||||
}
|
||||
function AssistSearchField(props: Props) {
|
||||
const hasEvents = props.appliedFilter.filters.filter((i: any) => i.isEvent).size > 0;
|
||||
const hasFilters = props.appliedFilter.filters.filter((i: any) => !i.isEvent).size > 0;
|
||||
return (
|
||||
<div className="flex items-center w-full">
|
||||
<div style={{ width: '60%', marginRight: '10px' }}>
|
||||
<SessionSearchField />
|
||||
</div>
|
||||
<Button
|
||||
variant="text-primary"
|
||||
className="ml-auto font-medium"
|
||||
disabled={!hasFilters && !hasEvents}
|
||||
onClick={() => props.clearSearch()}
|
||||
>
|
||||
Clear Search
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
const hasEvents =
|
||||
props.appliedFilter.filters.filter((i: any) => i.isEvent).size > 0;
|
||||
const hasFilters =
|
||||
props.appliedFilter.filters.filter((i: any) => !i.isEvent).size > 0;
|
||||
const { showModal, hideModal } = useModal();
|
||||
|
||||
const showStats = () => {
|
||||
showModal(<AssistStats />, { right: true, width: 960 })
|
||||
}
|
||||
const showRecords = () => {
|
||||
showModal(<Recordings />, { right: true, width: 960 })
|
||||
}
|
||||
return (
|
||||
<div className="flex items-center w-full gap-2">
|
||||
<div style={{ width: '60%' }}>
|
||||
<SessionSearchField />
|
||||
</div>
|
||||
<Button variant="outline" onClick={showRecords}>Training Videos</Button>
|
||||
<Button variant="outline" onClick={showStats}>Co-Browsing Reports</Button>
|
||||
<Button
|
||||
variant="text-primary"
|
||||
className="ml-auto font-medium"
|
||||
disabled={!hasFilters && !hasEvents}
|
||||
onClick={() => props.clearSearch()}
|
||||
>
|
||||
Clear Search
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(
|
||||
(state: any) => ({
|
||||
appliedFilter: state.getIn(['liveSearch', 'instance']),
|
||||
}),
|
||||
{
|
||||
fetchFilterSearch,
|
||||
editFilter,
|
||||
addFilterByKeyAndValue,
|
||||
clearSearch,
|
||||
}
|
||||
(state: any) => ({
|
||||
appliedFilter: state.getIn(['liveSearch', 'instance']),
|
||||
isEnterprise:
|
||||
state.getIn(['user', 'account', 'edition']) === 'ee' ||
|
||||
state.getIn(['user', 'authDetails', 'edition']) === 'ee'
|
||||
}),
|
||||
{
|
||||
fetchFilterSearch,
|
||||
editFilter,
|
||||
addFilterByKeyAndValue,
|
||||
clearSearch,
|
||||
}
|
||||
)(AssistSearchField);
|
||||
|
|
|
|||
|
|
@ -6,12 +6,10 @@ import RecordingsList from './RecordingsList';
|
|||
import { useStore } from 'App/mstore';
|
||||
import { connect } from 'react-redux';
|
||||
import SelectDateRange from 'Shared/SelectDateRange/SelectDateRange';
|
||||
import Period from 'Types/app/period';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
interface Props {
|
||||
userId: string;
|
||||
filter: any;
|
||||
}
|
||||
|
||||
function Recordings(props: Props) {
|
||||
|
|
@ -28,10 +26,10 @@ function Recordings(props: Props) {
|
|||
};
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: '1360px', margin: 'auto' }} className='bg-white rounded py-4 border'>
|
||||
<div style={{ maxWidth: '1360px', margin: 'auto' }} className='bg-white rounded py-4 border h-screen overflow-y-scroll'>
|
||||
<div className='flex items-center mb-4 justify-between px-6'>
|
||||
<div className='flex items-baseline mr-3'>
|
||||
<PageTitle title='Recordings' />
|
||||
<PageTitle title='Training Videos' />
|
||||
</div>
|
||||
<div className='ml-auto flex items-center gap-4'>
|
||||
<SelectDateRange period={recordingsStore.period} onChange={onDateChange} right={true} />
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ function RecordingsList() {
|
|||
<div className="text-center mt-4">
|
||||
{recordsSearch !== ''
|
||||
? 'No matching results'
|
||||
: 'No recordings available'}
|
||||
: 'No videos have been recorded in your co-browsing sessions.'}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ import Period, { LAST_24_HOURS } from 'Types/app/period';
|
|||
import SelectDateRange from 'Shared/SelectDateRange/SelectDateRange';
|
||||
import TeamMembers from 'Components/AssistStats/components/TeamMembers';
|
||||
import { durationFromMsFormatted, formatTimeOrDate } from 'App/date'
|
||||
import withPageTitle from 'HOCs/withPageTitle';
|
||||
import { exportCSVFile } from 'App/utils';
|
||||
import { assistStatsService } from 'App/services';
|
||||
|
||||
|
|
@ -206,11 +205,11 @@ function AssistStats() {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className={'w-full'}>
|
||||
<div className={'w-full h-screen overflow-y-auto'}>
|
||||
<div className={'mx-auto p-4 bg-white rounded border'} style={{ maxWidth: 1360 }} id={'pdf-anchor'}>
|
||||
<div id={'pdf-ignore'} className={'w-full flex items-center mb-2'}>
|
||||
<Typography.Title style={{ marginBottom: 0 }} level={4}>
|
||||
Reports
|
||||
Co-browsing Reports
|
||||
</Typography.Title>
|
||||
<div className={'ml-auto flex items-center gap-2'}>
|
||||
<UserSearch onUserSelect={onUserSelect} />
|
||||
|
|
@ -227,8 +226,7 @@ function AssistStats() {
|
|||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div className={'w-full grid grid-cols-3 gap-2'}>
|
||||
<div className={'grid grid-cols-3 gap-2 flex-2 col-span-2'}>
|
||||
<div className={'w-full grid grid-cols-3 gap-2 flex-2 col-span-2'}>
|
||||
{Object.keys(graphs.currentPeriod).map((i: PeriodKeys) => (
|
||||
<div className={'bg-white rounded border'}>
|
||||
<div className={'pt-2 px-2'}>
|
||||
|
|
@ -238,15 +236,15 @@ function AssistStats() {
|
|||
<div className={'flex gap-1 items-center'}>
|
||||
<Typography.Title style={{ marginBottom: 0 }} level={5}>
|
||||
{graphs.currentPeriod[i]
|
||||
? durationFromMsFormatted(graphs.currentPeriod[i])
|
||||
: null}
|
||||
? durationFromMsFormatted(graphs.currentPeriod[i])
|
||||
: null}
|
||||
</Typography.Title>
|
||||
{graphs.previousPeriod[i] ? (
|
||||
<div
|
||||
className={
|
||||
graphs.currentPeriod[i] > graphs.previousPeriod[i]
|
||||
? 'flex items-center gap-1 text-green'
|
||||
: 'flex items-center gap-2 text-red'
|
||||
? 'flex items-center gap-1 text-green'
|
||||
: 'flex items-center gap-2 text-red'
|
||||
}
|
||||
>
|
||||
<ArrowUpOutlined
|
||||
|
|
@ -268,15 +266,14 @@ function AssistStats() {
|
|||
</Loader>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className={'flex-1 col-span-1'}>
|
||||
<TeamMembers
|
||||
isLoading={isLoading}
|
||||
topMembers={topMembers}
|
||||
onMembersSort={onMembersSort}
|
||||
membersSort={membersSort}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={'w-full mt-2'}>
|
||||
<TeamMembers
|
||||
isLoading={isLoading}
|
||||
topMembers={topMembers}
|
||||
onMembersSort={onMembersSort}
|
||||
membersSort={membersSort}
|
||||
/>
|
||||
</div>
|
||||
<div className={'w-full mt-2'}>
|
||||
<StatsTable
|
||||
|
|
@ -294,4 +291,4 @@ function AssistStats() {
|
|||
);
|
||||
}
|
||||
|
||||
export default withPageTitle('Reports - OpenReplay')(AssistStats);
|
||||
export default AssistStats;
|
||||
|
|
|
|||
|
|
@ -74,14 +74,12 @@ function StatsTable({ onSort, isLoading, onPageChange, page, sessions, exportCSV
|
|||
Export CSV
|
||||
</Button>
|
||||
</div>
|
||||
<div className={'bg-gray-lightest grid grid-cols-8 items-center font-semibold p-4'}>
|
||||
<Cell size={1}>Date</Cell>
|
||||
<div className={'bg-gray-lightest grid grid-cols-9 items-center font-semibold p-4'}>
|
||||
<Cell size={2}>Date</Cell>
|
||||
<Cell size={2}>Team Members</Cell>
|
||||
<Cell size={1}>Live Duration</Cell>
|
||||
<Cell size={1}>Call Duration</Cell>
|
||||
<Cell size={1}>Remote Duration</Cell>
|
||||
<Cell size={1} />
|
||||
{/* SPACER */}
|
||||
<Cell size={2}>Remote Duration</Cell>
|
||||
<Cell size={1}>{/* BUTTONS */}</Cell>
|
||||
</div>
|
||||
<div className={'bg-white'}>
|
||||
|
|
@ -125,10 +123,10 @@ function StatsTable({ onSort, isLoading, onPageChange, page, sessions, exportCSV
|
|||
|
||||
function Row({ session }: { session: AssistStatsSession }) {
|
||||
return (
|
||||
<div className={'grid grid-cols-8 p-4 border-b hover:bg-active-blue'}>
|
||||
<Cell size={1}>{checkForRecent(getDateFromMill(session.timestamp)!, 'LLL dd, hh:mm a')}</Cell>
|
||||
<div className={'grid grid-cols-9 p-4 border-b hover:bg-active-blue'}>
|
||||
<Cell size={2}>{checkForRecent(getDateFromMill(session.timestamp)!, 'LLL dd, hh:mm a')}</Cell>
|
||||
<Cell size={2}>
|
||||
<div className={'flex gap-2'}>
|
||||
<div className={'flex gap-2 flex-wrap'}>
|
||||
{session.teamMembers.map((member) => (
|
||||
<div className={'p-1 rounded border bg-gray-lightest w-fit'}>{member.name}</div>
|
||||
))}
|
||||
|
|
@ -136,8 +134,7 @@ function Row({ session }: { session: AssistStatsSession }) {
|
|||
</Cell>
|
||||
<Cell size={1}>{durationFromMsFormatted(session.assistDuration)}</Cell>
|
||||
<Cell size={1}>{durationFromMsFormatted(session.callDuration)}</Cell>
|
||||
<Cell size={1}>{durationFromMsFormatted(session.controlDuration)}</Cell>
|
||||
<Cell size={1} />
|
||||
<Cell size={2}>{durationFromMsFormatted(session.controlDuration)}</Cell>
|
||||
<Cell size={1}>
|
||||
<div className={'w-full flex justify-end gap-4'}>
|
||||
{session.recordings?.length > 0 ? (
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ export interface Module {
|
|||
|
||||
export const modules = [
|
||||
{
|
||||
label: 'Cobrowse',
|
||||
label: 'Co-Browse',
|
||||
description: 'Enable live session replay, remote control, annotations and webRTC call/video.',
|
||||
key: MODULES.ASSIST,
|
||||
icon: 'broadcast'
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import cn from 'classnames';
|
|||
|
||||
function ModalOverlay({ hideModal, children, left = false, right = false }: any) {
|
||||
return (
|
||||
<div className="fixed w-full h-screen" style={{ zIndex: 9999 }}>
|
||||
<div className="fixed w-full h-screen" style={{ zIndex: 999 }}>
|
||||
<div onClick={hideModal} className={stl.overlay} style={{ background: 'rgba(0,0,0,0.5)' }} />
|
||||
<div className={cn(stl.slide, { [stl.slideLeft]: left, [stl.slideRight]: right })}>{children}</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ function LiveSessionList(props: Props) {
|
|||
<div className="flex mb-6 justify-between items-center">
|
||||
<div className="flex items-center">
|
||||
<h3 className="text-2xl capitalize mr-2">
|
||||
<span>Cobrowse</span>
|
||||
<span>Co-Browse</span>
|
||||
{/* <span className="ml-2 font-normal color-gray-medium">{numberWithCommas(total)}</span> */}
|
||||
</h3>
|
||||
|
||||
|
|
|
|||
|
|
@ -132,6 +132,7 @@ function SelectDateRange(props: Props) {
|
|||
) {
|
||||
return false;
|
||||
}
|
||||
console.log('outside')
|
||||
setIsCustom(false);
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -65,11 +65,10 @@ function SideMenu(props: Props) {
|
|||
if (item.hidden) return item;
|
||||
|
||||
const isHidden = [
|
||||
(item.key === MENU.STATS && modules.includes(MODULES.ASSIST_STATS)),
|
||||
(item.key === MENU.RECOMMENDATIONS && modules.includes(MODULES.RECOMMENDATIONS)),
|
||||
(item.key === MENU.FEATURE_FLAGS && modules.includes(MODULES.FEATURE_FLAGS)),
|
||||
(item.key === MENU.NOTES && modules.includes(MODULES.NOTES)),
|
||||
(item.key === MENU.LIVE_SESSIONS || item.key === MENU.RECORDINGS || item.key === MENU.STATS) && modules.includes(MODULES.ASSIST),
|
||||
(item.key === MENU.LIVE_SESSIONS && modules.includes(MODULES.ASSIST)),
|
||||
(item.key === MENU.SESSIONS && modules.includes(MODULES.OFFLINE_RECORDINGS)),
|
||||
(item.key === MENU.ALERTS && modules.includes(MODULES.ALERTS)),
|
||||
(item.isAdmin && !isAdmin),
|
||||
|
|
@ -107,8 +106,6 @@ function SideMenu(props: Props) {
|
|||
[MENU.VAULT]: () => withSiteId(routes.bookmarks(), siteId),
|
||||
[MENU.NOTES]: () => withSiteId(routes.notes(), siteId),
|
||||
[MENU.LIVE_SESSIONS]: () => withSiteId(routes.assist(), siteId),
|
||||
[MENU.STATS]: () => withSiteId(routes.assistStats(), siteId),
|
||||
[MENU.RECORDINGS]: () => withSiteId(routes.recordings(), siteId),
|
||||
[MENU.DASHBOARDS]: () => withSiteId(routes.dashboard(), siteId),
|
||||
[MENU.CARDS]: () => withSiteId(routes.metrics(), siteId),
|
||||
[MENU.ALERTS]: () => withSiteId(routes.alerts(), siteId),
|
||||
|
|
|
|||
|
|
@ -42,8 +42,6 @@ export const enum MENU {
|
|||
BOOKMARKS = 'bookmarks',
|
||||
NOTES = 'notes',
|
||||
LIVE_SESSIONS = 'live-sessions',
|
||||
RECORDINGS = 'recordings',
|
||||
STATS = 'assist-stats',
|
||||
DASHBOARDS = 'dashboards',
|
||||
CARDS = 'cards',
|
||||
FUNNELS = 'funnels',
|
||||
|
|
@ -70,12 +68,10 @@ export const categories: Category[] = [
|
|||
]
|
||||
},
|
||||
{
|
||||
title: 'Assist',
|
||||
title: '',
|
||||
key: 'assist',
|
||||
items: [
|
||||
{ label: 'Cobrowse', key: MENU.LIVE_SESSIONS, icon: 'broadcast' },
|
||||
{ label: 'Recordings', key: MENU.RECORDINGS, icon: 'record-btn', isEnterprise: true },
|
||||
{ label: 'Reports', key: MENU.STATS, icon: 'file-bar-graph', isEnterprise: true }
|
||||
{ label: 'Co-Browse', key: MENU.LIVE_SESSIONS, icon: 'broadcast' },
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue