feature(ui): assist - wip
This commit is contained in:
parent
5d12bc6259
commit
372b9c4b2e
17 changed files with 196 additions and 86 deletions
|
|
@ -1,5 +1,5 @@
|
|||
import React from 'react'
|
||||
import ChatWindow from './components/ChatWindow/ChatWindow'
|
||||
import ChatWindow from './ChatWindow/ChatWindow'
|
||||
import ScreenSharing from './ScreenSharing/ScreenSharing'
|
||||
|
||||
function Assist() {
|
||||
|
|
|
|||
17
frontend/app/components/Assist/ChatWindow/ChatWindow.tsx
Normal file
17
frontend/app/components/Assist/ChatWindow/ChatWindow.tsx
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import React from 'react'
|
||||
import VideoContainer from '../components/VideoContainer/VideoContainer'
|
||||
// import stl from './chatWindow.css';
|
||||
|
||||
function ChatWindow() {
|
||||
return (
|
||||
<div className="fixed border radius bg-white z-50 shadow-xl mt-16">
|
||||
<div className="p-2">
|
||||
<VideoContainer />
|
||||
<div className="py-1" />
|
||||
<VideoContainer />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ChatWindow
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
.wrapepr {
|
||||
background-color: white;
|
||||
border: solid thin $gray-medium
|
||||
border: solid thin #CCC;
|
||||
border-radius: 3px;
|
||||
position: fixed;
|
||||
height: 400px;
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
.btn {
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import React from 'react'
|
||||
import { Button, Icon } from 'UI'
|
||||
import cn from 'classnames'
|
||||
// import stl from './AassistActions.css'
|
||||
|
||||
interface Props {
|
||||
isLive: false;
|
||||
}
|
||||
|
||||
function AssistActions({ }: Props) {
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<div className={cn('cursor-pointer p-2 mr-2')}>
|
||||
<Icon name="telephone" size="20" />
|
||||
</div>
|
||||
<div className="flex items-center p-2 cursor-pointer">
|
||||
<Icon name="controller" size="20" />
|
||||
<span className="ml-2">Request Control</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AssistActions
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './AssistActions'
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
import React from 'react'
|
||||
import VideoContainer from '../VideoContainer/VideoContainer'
|
||||
// impdort stl from './chatWindow.css'
|
||||
|
||||
function ChatWindow() {
|
||||
return (
|
||||
<div className="fixed border radius bg-white z-50 shadow-md">
|
||||
<div className="p-2">
|
||||
<VideoContainer />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ChatWindow
|
||||
|
|
@ -1,50 +1,19 @@
|
|||
import React, { useEffect } from 'react'
|
||||
import { Button, Icon } from 'UI'
|
||||
|
||||
function VideoContainer() {
|
||||
const constraints = {
|
||||
'video': true,
|
||||
'audio': true
|
||||
}
|
||||
|
||||
async function playVideoFromCamera() {
|
||||
try {
|
||||
const constraints = {'video': true, 'audio': true};
|
||||
const stream = await navigator.mediaDevices.getUserMedia(constraints);
|
||||
const videoElement = document.querySelector('video#localVideo');
|
||||
// videoElement.srcObject = stream;
|
||||
} catch(error) {
|
||||
console.error('Error opening video camera.', error);
|
||||
}
|
||||
}
|
||||
|
||||
function getConnectedDevices(type, callback) {
|
||||
navigator.mediaDevices.enumerateDevices()
|
||||
.then(devices => {
|
||||
const filtered = devices.filter(device => device.kind === type);
|
||||
callback(filtered);
|
||||
});
|
||||
}
|
||||
|
||||
function VideoContainer() {
|
||||
useEffect(() => {
|
||||
getConnectedDevices('videoinput', cameras => console.log('Cameras found', cameras));
|
||||
navigator.mediaDevices.getUserMedia(constraints)
|
||||
.then(stream => {
|
||||
console.log('Got MediaStream:', stream);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error accessing media devices.', error);
|
||||
});
|
||||
// TODO check for video stream and display
|
||||
}, [])
|
||||
return (
|
||||
<div className="relative h-20 bg-gray-light-shade border p-1" style={{ height: '160px', width: '200px' }}>
|
||||
<div className="absolute left-0 right-0 bottom-0 flex justify-center border border-gray-300 p-1 bg-white radius">
|
||||
<div className="absolute left-0 right-0 bottom-0 flex justify-center border border-gray-300 p-1 bg-white radius bg-opacity-25">
|
||||
<Button plain size="small">
|
||||
<Icon name="mic" size="20" />
|
||||
<Icon name="mic" size="16" />
|
||||
</Button>
|
||||
|
||||
<Button plain size="small">
|
||||
<Icon name="camera-video" size="20" />
|
||||
<Icon name="camera-video" size="16" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ import { LAST_7_DAYS } from 'Types/app/period';
|
|||
import { resetFunnel } from 'Duck/funnels';
|
||||
import { resetFunnelFilters } from 'Duck/funnelFilters'
|
||||
import NoSessionsMessage from '../shared/NoSessionsMessage';
|
||||
import Assist from 'Components/Assist'
|
||||
import LiveSessionList from './LiveSessionList'
|
||||
|
||||
const AUTOREFRESH_INTERVAL = 10 * 60 * 1000;
|
||||
|
||||
|
|
@ -135,16 +135,16 @@ export default class BugFinder extends React.PureComponent {
|
|||
|
||||
setActiveTab = tab => {
|
||||
this.props.setActiveTab(tab);
|
||||
|
||||
}
|
||||
|
||||
render() {
|
||||
const { activeFlow, activeTab } = this.props;
|
||||
const { showRehydratePanel } = this.state;
|
||||
|
||||
console.log('activeTab', activeTab)
|
||||
|
||||
return (
|
||||
<div className="page-margin container-90 flex relative">
|
||||
<Assist />
|
||||
<div className="flex-1 flex">
|
||||
<div className="side-menu">
|
||||
<SessionsMenu
|
||||
|
|
@ -159,12 +159,10 @@ export default class BugFinder extends React.PureComponent {
|
|||
className="mb-5"
|
||||
>
|
||||
<EventFilter />
|
||||
</div>
|
||||
{activeFlow && activeFlow.type === 'flows' ?
|
||||
<FunnelList />
|
||||
:
|
||||
<SessionList onMenuItemClick={this.setActiveTab} />
|
||||
}
|
||||
</div>
|
||||
{ activeFlow && activeFlow.type === 'flows' && <FunnelList /> }
|
||||
{ activeTab.type !== 'live' && <SessionList onMenuItemClick={this.setActiveTab} /> }
|
||||
{ activeTab.type === 'live' && <LiveSessionList /> }
|
||||
</div>
|
||||
</div>
|
||||
<RehydrateSlidePanel
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
import React from 'react'
|
||||
// import { fetchLiveList } from 'Duck/sessions'
|
||||
import { connect } from 'react-redux';
|
||||
import { NoContent } from 'UI';
|
||||
import { List } from 'immutable';
|
||||
|
||||
interface Props {
|
||||
loading: Boolean,
|
||||
list?: List<any>
|
||||
}
|
||||
|
||||
function LiveSessionList({ loading, list }: Props ) {
|
||||
return (
|
||||
<div>
|
||||
<NoContent
|
||||
title={"No live sessions!"}
|
||||
subtext="Please try changing your search parameters."
|
||||
icon="exclamation-circle"
|
||||
show={ !loading && list && list.size === 0}
|
||||
>
|
||||
|
||||
</NoContent>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default connect(state => ({
|
||||
|
||||
}), { })(LiveSessionList)
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './LiveSessionList'
|
||||
|
|
@ -123,7 +123,7 @@ export default class SessionList extends React.PureComponent {
|
|||
const { activeTab, allList, total } = this.props;
|
||||
var filteredList;
|
||||
|
||||
if (activeTab.type !== ALL && activeTab.type !== 'bookmark') { // Watchdog sessions
|
||||
if (activeTab.type !== ALL && activeTab.type !== 'bookmark' && activeTab.type !== 'live') { // Watchdog sessions
|
||||
filteredList = allList.filter(session => activeTab.fits(session))
|
||||
} else {
|
||||
filteredList = allList
|
||||
|
|
|
|||
|
|
@ -81,6 +81,16 @@ function SessionsMenu(props) {
|
|||
onClick={() => onMenuItemClick({ name: 'Bookmarks', type: 'bookmark' })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="my-3">
|
||||
<SideMenuitem
|
||||
title="Assist"
|
||||
iconName="person"
|
||||
active={activeTab.type === 'live'}
|
||||
onClick={() => onMenuItemClick({ name: 'Assist', type: 'live' })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={cn(stl.divider, 'mb-4')} />
|
||||
<SavedSearchList />
|
||||
</div>
|
||||
|
|
|
|||
66
frontend/app/components/Session/LivePlayer.js
Normal file
66
frontend/app/components/Session/LivePlayer.js
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import { useEffect } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Loader } from 'UI';
|
||||
import { toggleFullscreen, closeBottomBlock } from 'Duck/components/player';
|
||||
import {
|
||||
PlayerProvider,
|
||||
connectPlayer,
|
||||
init as initPlayer,
|
||||
clean as cleanPlayer,
|
||||
} from 'Player';
|
||||
import { Controls as PlayerControls } from 'Player';
|
||||
import Assist from 'Components/Assist'
|
||||
|
||||
|
||||
import PlayerBlockHeader from '../Session_/PlayerBlockHeader';
|
||||
import EventsBlock from '../Session_/EventsBlock';
|
||||
import PlayerBlock from '../Session_/PlayerBlock';
|
||||
import styles from '../Session_/session.css';
|
||||
|
||||
|
||||
|
||||
const EventsBlockConnected = connectPlayer(state => ({
|
||||
currentTimeEventIndex: state.eventListNow.length > 0 ? state.eventListNow.length - 1 : 0,
|
||||
playing: state.playing,
|
||||
}))(EventsBlock)
|
||||
|
||||
|
||||
const InitLoader = connectPlayer(state => ({
|
||||
loading: !state.initialized
|
||||
}))(Loader);
|
||||
|
||||
|
||||
function WebPlayer ({ session, toggleFullscreen, closeBottomBlock, live, fullscreen, jwt }) {
|
||||
useEffect(() => {
|
||||
initPlayer(session, jwt);
|
||||
return () => cleanPlayer()
|
||||
}, [ session.sessionId ]);
|
||||
|
||||
// LAYOUT (TODO: local layout state - useContext or something..)
|
||||
useEffect(() => () => {
|
||||
toggleFullscreen(false);
|
||||
closeBottomBlock();
|
||||
}, [])
|
||||
return (
|
||||
<PlayerProvider>
|
||||
<InitLoader className="flex-1">
|
||||
<Assist />
|
||||
<PlayerBlockHeader fullscreen={fullscreen}/>
|
||||
<div className={ styles.session } data-fullscreen={fullscreen}>
|
||||
<PlayerBlock />
|
||||
</div>
|
||||
</InitLoader>
|
||||
</PlayerProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export default connect(state => ({
|
||||
session: state.getIn([ 'sessions', 'current' ]),
|
||||
jwt: state.get('jwt'),
|
||||
fullscreen: state.getIn([ 'components', 'player', 'fullscreen' ]),
|
||||
}), {
|
||||
toggleFullscreen,
|
||||
closeBottomBlock,
|
||||
})(WebPlayer)
|
||||
|
||||
|
|
@ -6,6 +6,7 @@ import { fetchList as fetchSlackList } from 'Duck/integrations/slack';
|
|||
import { Link, NoContent, Loader } from 'UI';
|
||||
import { sessions as sessionsRoute } from 'App/routes';
|
||||
|
||||
import LivePlayer from './LivePlayer';
|
||||
import WebPlayer from './WebPlayer';
|
||||
import IOSPlayer from './IOSPlayer';
|
||||
|
||||
|
|
@ -48,7 +49,7 @@ function Session({
|
|||
<Loader className="flex-1" loading={ loading || sessionId !== session.sessionId }>
|
||||
{ session.isIOS
|
||||
? <IOSPlayer session={session} />
|
||||
: <WebPlayer />
|
||||
: <LivePlayer />
|
||||
}
|
||||
</Loader>
|
||||
</NoContent>
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import { fetchList as fetchListIntegration } from 'Duck/integrations/actions';
|
|||
import cls from './playerBlockHeader.css';
|
||||
import Issues from './Issues/Issues';
|
||||
import Autoplay from './Autoplay';
|
||||
import AssistActions from '../Assist/components/AssistActions';
|
||||
|
||||
const SESSIONS_ROUTE = sessionsRoute();
|
||||
|
||||
|
|
@ -89,7 +90,7 @@ export default class PlayerBlockHeader extends React.PureComponent {
|
|||
live,
|
||||
disabled,
|
||||
jiraConfig,
|
||||
fullscreen
|
||||
fullscreen,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
|
|
@ -112,31 +113,36 @@ export default class PlayerBlockHeader extends React.PureComponent {
|
|||
<HeaderInfo icon={ osIcon(userOs) } label={ userOs } />
|
||||
|
||||
<div className='ml-auto flex items-center'>
|
||||
<Autoplay />
|
||||
<div className={ cls.divider } />
|
||||
<IconButton
|
||||
className="mr-2"
|
||||
tooltip="Bookmark"
|
||||
onClick={ this.toggleFavorite }
|
||||
loading={ loading }
|
||||
icon={ favorite ? 'star-solid' : 'star' }
|
||||
// label={ favorite ? 'Favourited' : 'Favourite' }
|
||||
plain
|
||||
/>
|
||||
<SharePopup
|
||||
entity="sessions"
|
||||
id={ sessionId }
|
||||
trigger={
|
||||
{ !live && <AssistActions isLive /> }
|
||||
{ live && (
|
||||
<>
|
||||
<Autoplay />
|
||||
<div className={ cls.divider } />
|
||||
<IconButton
|
||||
className="mr-2"
|
||||
tooltip="Share Session"
|
||||
disabled={ disabled }
|
||||
icon={ 'share-alt' }
|
||||
//label="Share"
|
||||
tooltip="Bookmark"
|
||||
onClick={ this.toggleFavorite }
|
||||
loading={ loading }
|
||||
icon={ favorite ? 'star-solid' : 'star' }
|
||||
// label={ favorite ? 'Favourited' : 'Favourite' }
|
||||
plain
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<SharePopup
|
||||
entity="sessions"
|
||||
id={ sessionId }
|
||||
trigger={
|
||||
<IconButton
|
||||
className="mr-2"
|
||||
tooltip="Share Session"
|
||||
disabled={ disabled }
|
||||
icon={ 'share-alt' }
|
||||
//label="Share"
|
||||
plain
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{ !live && jiraConfig && jiraConfig.token && <Issues sessionId={ sessionId } /> }
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue