Merge pull request #336 from openreplay/dev

v1.5.0: UI and tracker fixes
This commit is contained in:
Mehdi Osman 2022-02-16 18:40:20 +01:00 committed by GitHub
commit fc1d0634d4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 841 additions and 218 deletions

View file

@ -102,7 +102,7 @@ class Router extends React.Component {
this.props.fetchTenants();
}
if (!prevProps.isLoggedIn && this.props.isLoggedIn && this.state.destinationPath !== routes.login() && this.state.destinationPath !== '/') {
if (this.state.destinationPath && !prevProps.isLoggedIn && this.props.isLoggedIn && this.state.destinationPath !== routes.login() && this.state.destinationPath !== '/') {
this.props.history.push(this.state.destinationPath);
this.setState({ destinationPath: null });
}

View file

@ -33,7 +33,7 @@ const LIMIT_WARNING = 'You have reached users limit.';
generateInviteLink,
fetchRoles
})
@withPageTitle('Users - OpenReplay Preferences')
@withPageTitle('Team - OpenReplay Preferences')
class ManageUsers extends React.PureComponent {
state = { showModal: false, remaining: this.props.account.limits.teamMember.remaining, invited: false }

View file

@ -5,6 +5,7 @@ import { Checkbox } from 'UI'
import { connect } from 'react-redux'
import { withRequest } from 'HOCs'
import { fetch as fetchConfig, edit as editConfig, save as saveConfig } from 'Duck/config'
import withPageTitle from 'HOCs/withPageTitle';
function Notifications(props) {
const { config } = props;
@ -42,4 +43,4 @@ function Notifications(props) {
export default connect(state => ({
config: state.getIn(['config', 'options'])
}), { fetchConfig, editConfig, saveConfig })(Notifications)
}), { fetchConfig, editConfig, saveConfig })(withPageTitle('Notifications - OpenReplay Preferences')(Notifications));

View file

@ -68,25 +68,25 @@ function PreferencesMenu({ activeTab, appearance, history, isEnterprise }) {
/>
</div>
<div className="mb-4">
<SideMenuitem
active={ activeTab === CLIENT_TABS.MANAGE_USERS }
title="Users"
iconName="users"
onClick={() => setTab(CLIENT_TABS.MANAGE_USERS) }
/>
</div>
{ isEnterprise && (
<div className="mb-4">
<SideMenuitem
active={ activeTab === CLIENT_TABS.MANAGE_ROLES }
title="Roles"
iconName="shield-lock"
title="Roles & Access"
iconName="diagram-3"
onClick={() => setTab(CLIENT_TABS.MANAGE_ROLES) }
/>
</div>
)}
<div className="mb-4">
<SideMenuitem
active={ activeTab === CLIENT_TABS.MANAGE_USERS }
title="Team"
iconName="users"
onClick={() => setTab(CLIENT_TABS.MANAGE_USERS) }
/>
</div>
<div className="mb-4">
<SideMenuitem

View file

@ -8,6 +8,7 @@ import { init, edit, fetchList, remove as deleteRole, resetErrors } from 'Duck/r
import RoleItem from './components/RoleItem'
import { confirm } from 'UI/Confirmation';
import { toast } from 'react-toastify';
import withPageTitle from 'HOCs/withPageTitle';
interface Props {
loading: boolean
@ -20,11 +21,12 @@ interface Props {
account: any,
permissionsMap: any,
removeErrors: any,
resetErrors: () => void
resetErrors: () => void,
projectsMap: any,
}
function Roles(props: Props) {
const { loading, instance, roles, init, edit, deleteRole, account, permissionsMap, removeErrors } = props
const { loading, instance, roles, init, edit, deleteRole, account, permissionsMap, projectsMap, removeErrors } = props
const [showModal, setShowmModal] = useState(false)
const isAdmin = account.admin || account.superAdmin;
@ -72,16 +74,16 @@ function Roles(props: Props) {
<React.Fragment>
<Loader loading={ loading }>
<SlideModal
title={ instance.exists() ? "Edit Role" : "Add Role" }
title={ instance.exists() ? "Edit Role" : "Create Role" }
size="small"
isDisplayed={showModal }
content={ showModal && <RoleForm closeModal={closeModal}/> }
content={ showModal && <RoleForm closeModal={closeModal} permissionsMap={permissionsMap} deleteHandler={deleteHandler} /> }
onClose={ closeModal }
/>
<div className={ stl.wrapper }>
<div className={ cn(stl.tabHeader, 'flex items-center') }>
<div className="flex items-center mr-auto">
<h3 className={ cn(stl.tabTitle, "text-2xl") }>Manage Roles and Permissions</h3>
<h3 className={ cn(stl.tabTitle, "text-2xl") }>Roles and Access</h3>
<Popup
trigger={
<div>
@ -111,11 +113,17 @@ function Roles(props: Props) {
icon
>
<div className={''}>
<div className={cn(stl.wrapper, 'flex items-start py-3 border-b px-3 pr-20')}>
<div className="flex" style={{ width: '20%'}}>Title</div>
<div className="flex" style={{ width: '30%'}}>Project Access</div>
<div className="flex" style={{ width: '50%'}}>Feature Access</div>
</div>
{roles.map(role => (
<RoleItem
role={role}
isAdmin={isAdmin}
permissions={permissionsMap}
projects={projectsMap}
editHandler={editHandler}
deleteHandler={deleteHandler}
/>
@ -132,14 +140,20 @@ export default connect(state => {
const permissions = state.getIn(['roles', 'permissions'])
const permissionsMap = {}
permissions.forEach(p => {
permissionsMap[p.value] = p.name
permissionsMap[p.value] = p.text
});
const projects = state.getIn([ 'site', 'list' ])
return {
instance: state.getIn(['roles', 'instance']) || null,
permissionsMap: permissionsMap,
roles: state.getIn(['roles', 'list']),
removeErrors: state.getIn(['roles', 'removeRequest', 'errors']),
loading: state.getIn(['roles', 'fetchRequest', 'loading']),
account: state.getIn([ 'user', 'account' ])
account: state.getIn([ 'user', 'account' ]),
projectsMap: projects.reduce((acc, p) => {
acc[ p.get('id') ] = p.get('name')
return acc
}
, {}),
}
}, { init, edit, fetchList, deleteRole, resetErrors })(Roles)
}, { init, edit, fetchList, deleteRole, resetErrors })(withPageTitle('Roles & Access - OpenReplay Preferences')(Roles))

View file

@ -2,7 +2,7 @@ import React, { useRef, useEffect } from 'react'
import { connect } from 'react-redux'
import stl from './roleForm.css'
import { save, edit } from 'Duck/roles'
import { Input, Button, Checkbox } from 'UI'
import { Input, Button, Checkbox, Dropdown, Icon } from 'UI'
interface Permission {
name: string,
@ -16,9 +16,14 @@ interface Props {
closeModal: (toastMessage?: string) => void,
saving: boolean,
permissions: Array<Permission>[]
projectOptions: Array<any>[],
permissionsMap: any,
projectsMap: any,
deleteHandler: (id: any) => Promise<void>,
}
const RoleForm = ({ role, closeModal, edit, save, saving, permissions }: Props) => {
const RoleForm = (props: Props) => {
const { role, edit, save, closeModal, saving, permissions, projectOptions, permissionsMap, projectsMap } = props
let focusElement = useRef<any>(null)
const _save = () => {
save(role).then(() => {
@ -28,13 +33,33 @@ const RoleForm = ({ role, closeModal, edit, save, saving, permissions }: Props)
const write = ({ target: { value, name } }) => edit({ [ name ]: value })
const onChangeOption = (e) => {
const onChangePermissions = (e) => {
const { permissions } = role
const index = permissions.indexOf(e)
const _perms = permissions.contains(e) ? permissions.remove(index) : permissions.push(e)
edit({ permissions: _perms })
}
const onChangeProjects = (e) => {
const { projects } = role
const index = projects.indexOf(e)
const _projects = index === -1 ? projects.push(e) : projects.remove(index)
edit({ projects: _projects })
}
const writeOption = (e, { name, value }) => {
if (name === 'permissions') {
onChangePermissions(value)
} else if (name === 'projects') {
onChangeProjects(value)
}
}
const toggleAllProjects = () => {
const { allProjects } = role
edit({ allProjects: !allProjects })
}
useEffect(() => {
focusElement && focusElement.current && focusElement.current.focus()
}, [])
@ -42,8 +67,8 @@ const RoleForm = ({ role, closeModal, edit, save, saving, permissions }: Props)
return (
<div className={ stl.form }>
<form onSubmit={ _save } >
<div className={ stl.formGroup }>
<label>{ 'Name' }</label>
<div className="form-group">
<label>{ 'Title' }</label>
<Input
ref={ focusElement }
name="name"
@ -51,22 +76,77 @@ const RoleForm = ({ role, closeModal, edit, save, saving, permissions }: Props)
onChange={ write }
className={ stl.input }
id="name-field"
placeholder="Ex. Admin"
/>
</div>
<div>
{ permissions.map((permission: any, index) => (
<div key={ index } className={ stl.formGroup }>
<Checkbox
name="permissions"
className="font-medium"
type="checkbox"
checked={ role.permissions.contains(permission.value) }
onClick={ () => onChangeOption(permission.value) }
label={permission.name}
/>
<div className="form-group flex flex-col">
<label>{ 'Project Access' }</label>
<div className="flex my-3">
<Checkbox
name="allProjects"
className="font-medium"
type="checkbox"
checked={ role.allProjects }
onClick={toggleAllProjects}
label={''}
/>
<div className="cursor-pointer" onClick={toggleAllProjects}>
<div>All Projects</div>
<span className="text-xs text-gray-600">
(Uncheck to select specific projects)
</span>
</div>
))}
</div>
{ !role.allProjects && (
<>
<Dropdown
search
className="fluid"
placeholder="Select"
selection
options={ projectOptions }
name="projects"
value={null}
onChange={ writeOption }
id="change-dropdown"
selectOnBlur={false}
selectOnNavigation={false}
/>
{ role.projects.size > 0 && (
<div className="flex flex-row items-start flex-wrap mt-4">
{ role.projects.map(p => (
OptionLabel(projectsMap, p, onChangeProjects)
)) }
</div>
)}
</>
)}
</div>
<div className="form-group flex flex-col">
<label>{ 'Capability Access' }</label>
<Dropdown
search
className="fluid"
placeholder="Select"
selection
options={ permissions }
name="permissions"
value={null}
onChange={ writeOption }
id="change-dropdown"
selectOnBlur={false}
selectOnNavigation={false}
/>
{ role.permissions.size > 0 && (
<div className="flex flex-row items-start flex-wrap mt-4">
{ role.permissions.map(p => (
OptionLabel(permissionsMap, p, onChangePermissions)
)) }
</div>
)}
</div>
</form>
@ -89,13 +169,50 @@ const RoleForm = ({ role, closeModal, edit, save, saving, permissions }: Props)
{ 'Cancel' }
</Button>
</div>
{ role.exists() && (
<div>
<Button
data-hidden={ !role.exists() }
onClick={ () => props.deleteHandler(role) }
hover
noPadding
>
<Icon name="trash" size="18"/>
</Button>
</div>
)}
</div>
</div>
);
}
export default connect(state => ({
role: state.getIn(['roles', 'instance']),
permissions: state.getIn(['roles', 'permissions']),
saving: state.getIn([ 'roles', 'saveRequest', 'loading' ]),
}), { edit, save })(RoleForm);
export default connect(state => {
const role = state.getIn(['roles', 'instance'])
const projects = state.getIn([ 'site', 'list' ])
return {
role,
projectOptions: projects.map(p => ({
key: p.get('id'),
value: p.get('id'),
text: p.get('name'),
disabled: role.projects.includes(p.get('id')),
})).toJS(),
permissions: state.getIn(['roles', 'permissions'])
.map(({ text, value }) => ({ text, value, disabled: role.permissions.includes(value) })).toJS(),
saving: state.getIn([ 'roles', 'saveRequest', 'loading' ]),
projectsMap: projects.reduce((acc, p) => {
acc[ p.get('id') ] = p.get('name')
return acc
}
, {}),
}
}, { edit, save })(RoleForm);
function OptionLabel(nameMap: any, p: any, onChangeOption: (e: any) => void) {
return <div className="px-2 py-1 rounded bg-gray-lightest mr-2 mb-2 border flex items-center justify-between">
<div>{nameMap[p]}</div>
<div className="cursor-pointer ml-2" onClick={() => onChangeOption(p)}>
<Icon name="close" size="12" />
</div>
</div>
}

View file

@ -1,9 +1,5 @@
.form {
padding: 0 20px;
& .formGroup {
margin-bottom: 15px;
}
& label {
display: block;
margin-bottom: 5px;

View file

@ -1,11 +1,19 @@
import React from 'react'
import { Icon } from 'UI'
import { Icon, Link } from 'UI'
import stl from './roleItem.css'
import cn from 'classnames'
import { CLIENT_TABS, client as clientRoute } from 'App/routes';
function PermisionLabel({ permission }: any) {
function PermisionLabel({ label }: any) {
return (
<div className={cn(stl.label)}>{ permission }</div>
<div className={cn(stl.label, 'mb-2')}>{ label }</div>
);
}
function PermisionLabelLinked({ label, route }: any) {
return (
<Link to={route}><div className={cn(stl.label, 'mb-2 bg-active-blue color-teal')}>{ label }</div></Link>
);
}
@ -14,28 +22,33 @@ interface Props {
deleteHandler?: (role: any) => void,
editHandler?: (role: any) => void,
permissions: any,
isAdmin: boolean
isAdmin: boolean,
projects: any,
}
function RoleItem({ role, deleteHandler, editHandler, isAdmin, permissions }: Props) {
function RoleItem({ role, deleteHandler, editHandler, isAdmin, permissions, projects }: Props) {
return (
<div className={cn(stl.wrapper)}>
<Icon name="user-alt" size="16" marginRight="10" />
<div className="flex items-center">
<div className="mr-4">{ role.name }</div>
<div className="grid grid-flow-col auto-cols-max">
{role.permissions.map((permission: any) => (
<PermisionLabel permission={permissions[permission]} key={permission.id} />
// <span key={permission.id} className={cn(stl.permission)}>{ permissions[permission].name }</span>
))}
</div>
<div className={cn('flex items-start relative py-4 hover border-b px-3 pr-20')}>
<div className="flex" style={{ width: '20%'}}>
<Icon name="user-alt" size="16" marginRight="10" />
{ role.name }
</div>
<div className="flex items-start flex-wrap" style={{ width: '30%'}}>
{role.allProjects ? (
<PermisionLabelLinked label="All projects" route={clientRoute(CLIENT_TABS.SITES)}/>
) : (
role.projects.map(p => (
<PermisionLabel label={projects[p]} />
))
)}
</div>
<div className="flex items-start flex-wrap" style={{ width: '50%'}}>
{role.permissions.map((permission: any) => (
<PermisionLabel label={permissions[permission]} key={permission.id} />
))}
</div>
{ isAdmin && (
<div className={ stl.actions }>
{ !!deleteHandler &&
<div className={ cn(stl.button, {[stl.disabled] : role.protected }) } onClick={ () => deleteHandler(role) } id="trash">
<Icon name="trash" size="16" color="teal"/>
</div>
}
<div className={ cn(stl.actions, 'absolute right-0 top-0 bottom-0 mr-8') }>
{ !!editHandler &&
<div className={ cn(stl.button, {[stl.disabled] : role.protected }) } onClick={ () => editHandler(role) }>
<Icon name="edit" size="16" color="teal"/>
@ -43,7 +56,6 @@ function RoleItem({ role, deleteHandler, editHandler, isAdmin, permissions }: Pr
}
</div>
)}
</div>
);
}

View file

@ -1,13 +1,5 @@
.wrapper {
display: flex;
align-items: center;
width: 100%;
border-bottom: solid thin #e6e6e6;
padding: 10px 0px;
}
.actions {
margin-left: auto;
/* margin-left: auto; */
/* opacity: 0; */
transition: all 0.4s;
display: flex;
@ -37,11 +29,11 @@
}
.label {
margin-left: 10px;
margin-right: 10px;
padding: 0 5px;
border-radius: 3px;
background-color: $gray-lightest;
font-size: 10px;
font-size: 12px;
border: solid thin $gray-light;
width: fit-content;
}

View file

@ -37,7 +37,7 @@ const GDPR_FORM = 'GDPR_FORM';
remove,
fetchGDPR
})
@withPageTitle('Sites - OpenReplay Preferences')
@withPageTitle('Projects - OpenReplay Preferences')
class Sites extends React.PureComponent {
state = {
showTrackingCode: false,

View file

@ -9,6 +9,7 @@ import { fetchList as fetchSlackList } from 'Duck/integrations/slack';
import { errors as errorsRoute, isRoute } from "App/routes";
import EventFilter from 'Components/BugFinder/EventFilter';
import DateRange from 'Components/BugFinder/DateRange';
import withPageTitle from 'HOCs/withPageTitle';
import { SavedSearchList } from 'UI';
@ -43,6 +44,7 @@ function getStatusLabel(status) {
applyFilter,
fetchSlackList,
})
@withPageTitle("Errors - OpenReplay")
export default class Errors extends React.PureComponent {
state = {
status: UNRESOLVED,

View file

@ -127,10 +127,6 @@ export default class Fetch extends React.PureComponent {
<BottomBlock.Header>
<h4 className="text-lg">Fetch</h4>
<div className="flex items-center">
{/* <div className="flex items-center mr-3 text-sm uppercase">
<div className="p-2 cursor-pointer" onClick={this.goToPrevError}>Prev</div>
<div className="p-2 cursor-pointer" onClick={this.goToNextError}>Next</div>
</div> */}
<Input
className="input-small"
placeholder="Filter"

View file

@ -1,4 +1,4 @@
import { JSONTree } from 'UI'
import { JSONTree, Button } from 'UI'
import cn from 'classnames';
export default class GQLDetails extends React.PureComponent {
@ -7,7 +7,14 @@ export default class GQLDetails extends React.PureComponent {
gql: {
variables,
response,
duration,
operationKind,
operationName,
},
nextClick,
prevClick,
first = false,
last = false,
} = this.props;
let jsonVars = undefined;
@ -19,28 +26,52 @@ export default class GQLDetails extends React.PureComponent {
jsonResponse = JSON.parse(response);
} catch (e) {}
return (
<div className="ph-20" >
<div className="divider"/>
{ variables && variables !== "{}" &&
<div>
<div className="mt-6">
<h5>{ 'Variables'}</h5>
{ jsonVars === undefined
? <div className="ml-3"> { variables } </div>
: <JSONTree src={ jsonVars } />
}
<div className="px-4 pb-16">
<h5 className="mb-2">{ 'Operation Name'}</h5>
<div className={ cn('p-2 bg-gray-lightest rounded color-gray-darkest')}>{ operationName }</div>
<div className="flex items-center mt-4">
<div className="w-4/12">
<div className="font-medium mb-2">Operation Kind</div>
<div>{operationKind}</div>
</div>
<div className="w-4/12">
<div className="font-medium mb-2">Duration</div>
<div>{parseInt(duration)} ms</div>
</div>
</div>
<div className="flex justify-between items-start mt-6">
<h5 className="mt-1 mr-1">{ 'Response' }</h5>
</div>
<div style={{ height: 'calc(100vh - 314px)', overflowY: 'auto' }}>
{ variables && variables !== "{}" &&
<div>
<div className="mt-2">
<h5>{ 'Variables'}</h5>
{ jsonVars === undefined
? <div className="ml-3"> { variables } </div>
: <JSONTree src={ jsonVars } />
}
</div>
<div className="divider"/>
</div>
<div className="divider"/>
</div>
}
<div className="mt-6">
<div className="flex justify-between items-start">
<h5 className="mt-1 mr-1">{ 'Response' }</h5>
</div>
{ jsonResponse === undefined
? <div className="ml-3"> { response } </div>
: <JSONTree src={ jsonResponse } />
}
<div className="mt-3">
{ jsonResponse === undefined
? <div className="ml-3"> { response } </div>
: <JSONTree src={ jsonResponse } />
}
</div>
</div>
<div className="flex justify-between absolute bottom-0 left-0 right-0 p-3 border-t bg-white">
<Button primary plain onClick={prevClick} disabled={first}>
Prev
</Button>
<Button primary plain onClick={nextClick} disabled={last}>
Next
</Button>
</div>
</div>
);

View file

@ -1,8 +1,6 @@
//import cn from 'classnames';
import { Icon, NoContent, Input, SlideModal } from 'UI';
import { Label, Icon, NoContent, Input, SlideModal, CloseButton } from 'UI';
import { getRE } from 'App/utils';
import { connectPlayer } from 'Player';
import Autoscroll from '../Autoscroll';
import { connectPlayer, pause, jump } from 'Player';
import BottomBlock from '../BottomBlock';
import TimeTable from '../TimeTable';
import GQLDetails from './GQLDetails';
@ -10,60 +8,105 @@ import GQLDetails from './GQLDetails';
function renderDefaultStatus() {
return "2xx-3xx";
}
@connectPlayer(state => ({
list: state.graphqlListNow,
livePlay: state.livePlay,
}))
export default class GraphQL extends React.PureComponent {
state = {
filter: "",
filter: "",
filteredList: this.props.list,
current: null,
currentIndex: 0,
showFetchDetails: false,
hasNextError: false,
hasPreviousError: false,
}
onFilterChange = (e, { value }) => this.setState({ filter: value })
setCurrent = (item) => {
this.setState({ current: item });
onFilterChange = (e, { value }) => {
const { list } = this.props;
const filterRE = getRE(value, 'i');
const filtered = list
.filter((r) => filterRE.test(r.name) || filterRE.test(r.url) || filterRE.test(r.method) || filterRE.test(r.status));
this.setState({ filter: value, filteredList: value ? filtered : list, currentIndex: 0 });
}
setCurrent = (item, index) => {
if (!this.props.livePlay) {
pause();
jump(item.time)
}
this.setState({ current: item, currentIndex: index });
}
closeModal = () => this.setState({ current: null, showFetchDetails: false });
static getDerivedStateFromProps(nextProps, prevState) {
const { filteredList } = prevState;
if (nextProps.timelinePointer) {
let activeItem = filteredList.find((r) => r.time >= nextProps.timelinePointer.time);
activeItem = activeItem || filteredList[filteredList.length - 1];
return {
current: activeItem,
currentIndex: filteredList.indexOf(activeItem),
};
}
}
closeModal = () => this.setState({ current: null})
render() {
const { list } = this.props;
const { filter, current } = this.state;
const filterRE = getRE(filter, 'i');
const filtered = list
.filter(({ operationName = "", operationKind = "" }) => filterRE.test(operationName) || filterRE.test(operationKind));
const { current, currentIndex, filteredList } = this.state;
return (
<React.Fragment>
<SlideModal
size="middle"
title={ current && <span><i className="color-gray-medium">{current.operationKind}</i> {current.operationName}</span> }
right
title = {
<div className="flex justify-between">
<h1>GraphQL</h1>
<div className="flex items-center">
<CloseButton onClick={ this.closeModal } size="18" className="ml-2" />
</div>
</div>
}
isDisplayed={ current != null }
content={ current &&
<GQLDetails gql={ current }/>
<GQLDetails
gql={ current }
nextClick={this.nextClickHander}
prevClick={this.prevClickHander}
first={currentIndex === 0}
last={currentIndex === filteredList.length - 1}
/>
}
onClose={ this.closeModal }
/>
<BottomBlock>
<BottomBlock.Header>
<Input
className="input-small"
placeholder="Filter by Name or Type"
icon="search"
iconPosition="left"
name="filter"
onChange={ this.onFilterChange }
/>
<h4 className="text-lg">GraphQL</h4>
<div className="flex items-center">
<Input
className="input-small"
placeholder="Filter by Name or Type"
icon="search"
iconPosition="left"
name="filter"
onChange={ this.onFilterChange }
/>
</div>
</BottomBlock.Header>
<BottomBlock.Content>
<NoContent
size="small"
show={ filtered.length === 0}
show={ filteredList.length === 0}
>
<TimeTable
rows={ filtered }
rows={ filteredList }
onRowClick={ this.setCurrent }
hoverable
navigation
activeIndex={currentIndex}
>
{[
{

View file

@ -2,6 +2,7 @@ import { List, Map } from 'immutable';
import Role from 'Types/role';
import crudDuckGenerator from './tools/crudDuck';
import { reduceDucks } from 'Duck/tools';
import { array, request, success, failure, createListUpdater, mergeReducers } from './funcTools/tools';
const crudDuck = crudDuckGenerator('client/role', Role, { idKey: 'roleId' });
export const { fetchList, init, edit, remove, } = crudDuck.actions;
@ -11,19 +12,29 @@ const RESET_ERRORS = 'roles/RESET_ERRORS';
const initialState = Map({
list: List(),
permissions: List([
{ name: 'Session Replay', value: 'SESSION_REPLAY' },
{ name: 'Developer Tools', value: 'DEV_TOOLS' },
{ name: 'Errors', value: 'ERRORS' },
{ name: 'Metrics', value: 'METRICS' },
{ name: 'Assist (Live)', value: 'ASSIST_LIVE' },
{ name: 'Assist (Call)', value: 'ASSIST_CALL' },
{ text: 'Session Replay', value: 'SESSION_REPLAY' },
{ text: 'Developer Tools', value: 'DEV_TOOLS' },
{ text: 'Errors', value: 'ERRORS' },
{ text: 'Metrics', value: 'METRICS' },
{ text: 'Assist (Live)', value: 'ASSIST_LIVE' },
{ text: 'Assist (Call)', value: 'ASSIST_CALL' },
])
});
const name = "role";
const idKey = "roleId";
const updateItemInList = createListUpdater(idKey);
const updateInstance = (state, instance) => state.getIn([ "instance", idKey ]) === instance[ idKey ]
? state.mergeIn([ "instance" ], instance)
: state;
const reducer = (state = initialState, action = {}) => {
switch (action.type) {
case RESET_ERRORS:
return state.setIn(['removeRequest', 'errors'], null);
case crudDuck.actionTypes.SAVE.SUCCESS:
return updateItemInList(updateInstance(state, action.data), action.data);
}
return state;
};

View file

@ -59,8 +59,8 @@ export const forgotPassword = () => '/reset-password';
export const CLIENT_TABS = {
INTEGRATIONS: 'integrations',
PROFILE: 'account',
MANAGE_USERS: 'manage-users',
MANAGE_ROLES: 'manage-roles',
MANAGE_USERS: 'team',
MANAGE_ROLES: 'roles',
SITES: 'projects',
CUSTOM_FIELDS: 'metadata',
WEBHOOKS: 'webhooks',
@ -73,7 +73,7 @@ export const client = (tab = routerClientTabString) => `/client/${ tab }`;
export const OB_TABS = {
INSTALLING: 'installing',
IDENTIFY_USERS: 'identify-users',
MANAGE_USERS: 'manage-users',
MANAGE_USERS: 'team',
INTEGRATIONS: 'integrations',
};
export const OB_DEFAULT_TAB = OB_TABS.INSTALLING;

View file

@ -117,4 +117,10 @@
.disabled {
opacity: 0.4;
pointer-events: none;
}
.hover {
&:hover {
background-color: $active-blue;
}
}

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" class="bi bi-diagram-3" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M6 3.5A1.5 1.5 0 0 1 7.5 2h1A1.5 1.5 0 0 1 10 3.5v1A1.5 1.5 0 0 1 8.5 6v1H14a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-1 0V8h-5v.5a.5.5 0 0 1-1 0V8h-5v.5a.5.5 0 0 1-1 0v-1A.5.5 0 0 1 2 7h5.5V6A1.5 1.5 0 0 1 6 4.5v-1zM8.5 5a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1zM0 11.5A1.5 1.5 0 0 1 1.5 10h1A1.5 1.5 0 0 1 4 11.5v1A1.5 1.5 0 0 1 2.5 14h-1A1.5 1.5 0 0 1 0 12.5v-1zm1.5-.5a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5h-1zm4.5.5A1.5 1.5 0 0 1 7.5 10h1a1.5 1.5 0 0 1 1.5 1.5v1A1.5 1.5 0 0 1 8.5 14h-1A1.5 1.5 0 0 1 6 12.5v-1zm1.5-.5a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5h-1zm4.5.5a1.5 1.5 0 0 1 1.5-1.5h1a1.5 1.5 0 0 1 1.5 1.5v1a1.5 1.5 0 0 1-1.5 1.5h-1a1.5 1.5 0 0 1-1.5-1.5v-1zm1.5-.5a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5h-1z"/>
</svg>

After

Width:  |  Height:  |  Size: 973 B

View file

@ -1,18 +1,21 @@
import Record from 'Types/Record';
import { validateName } from 'App/validate';
import { notEmptyString, validateName } from 'App/validate';
import { List } from 'immutable';
export default Record({
roleId: undefined,
name: '',
allProjects: true,
permissions: List(),
projects: List(),
protected: false,
description: ''
description: '',
permissionOptions: List(),
}, {
idKey: 'roleId',
methods: {
validate() {
return validateName(this.name, { diacritics: true });
return notEmptyString(this.name) && validateName(this.name, { diacritics: true }) && (this.allProjects || this.projects.size > 0);
},
toData() {
const js = this.toJS();
@ -21,10 +24,11 @@ export default Record({
return js;
},
},
fromJS({ permissions, ...rest }) {
fromJS({ projects = [], permissions = [], ...rest }) {
return {
...rest,
permissions: List(permissions)
permissions: List(permissions),
projects: List(projects),
}
},
});

View file

@ -1,7 +1,7 @@
{
"name": "@openreplay/tracker",
"description": "The OpenReplay tracker main package",
"version": "3.5.0",
"version": "3.5.1",
"keywords": [
"logging",
"replay"
@ -41,6 +41,6 @@
"error-stack-parser": "^2.0.6"
},
"engines": {
"node": ">=12"
"node": ">=14"
}
}

View file

@ -171,10 +171,18 @@ export default class App {
this.debug.error("OpenReplay error: ", context, e)
}
private readonly preStartMessages: Message[] = []
send(message: Message, urgent = false): void {
if (this.activityState !== ActivityState.Active) {
if (this.activityState === ActivityState.NotActive) {
return;
}
if (this.activityState === ActivityState.Starting) {
this.preStartMessages.push(message);
}
if (this.preStartMessages.length) {
this.messages.push(...this.preStartMessages);
this.preStartMessages.length = 0
}
this.messages.push(message);
if (urgent) {
this.commit();

View file

@ -1,4 +1,4 @@
FROM node:12.22-stretch
FROM node:17-stretch
WORKDIR /work
COPY . .
RUN npm install

View file

@ -15,7 +15,8 @@
"peer": "^0.6.1",
"socket.io": "^4.4.1",
"source-map": "^0.7.3",
"ua-parser-js": "^1.0.2"
"ua-parser-js": "^1.0.2",
"uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.6.0"
}
},
"node_modules/@maxmind/geoip2-node": {
@ -1287,6 +1288,10 @@
"uuid": "bin/uuid"
}
},
"node_modules/uWebSockets.js": {
"version": "20.6.0",
"resolved": "git+ssh://git@github.com/uNetworking/uWebSockets.js.git#a58e810e47a23696410f6073c8c905dc38f75da5"
},
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
@ -2350,6 +2355,10 @@
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
"integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA=="
},
"uWebSockets.js": {
"version": "git+ssh://git@github.com/uNetworking/uWebSockets.js.git#a58e810e47a23696410f6073c8c905dc38f75da5",
"from": "uWebSockets.js@github:uNetworking/uWebSockets.js#v20.6.0"
},
"vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",

View file

@ -24,6 +24,7 @@
"peer": "^0.6.1",
"socket.io": "^4.4.1",
"source-map": "^0.7.3",
"ua-parser-js": "^1.0.2"
"ua-parser-js": "^1.0.2",
"uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.6.0"
}
}

View file

@ -8,32 +8,29 @@ const HOST = '0.0.0.0';
const PORT = 9000;
var app = express();
var wsapp = express();
let debug = process.env.debug === "1" || false;
const request_logger = (identity) => {
return (req, res, next) => {
console.log(identity,new Date().toTimeString(), 'REQUEST', req.method, req.originalUrl);
debug && console.log(identity, new Date().toTimeString(), 'REQUEST', req.method, req.originalUrl);
res.on('finish', function () {
console.log(new Date().toTimeString(), 'RESPONSE', req.method, req.originalUrl, this.statusCode);
if (this.statusCode !== 200 || debug) {
console.log(new Date().toTimeString(), 'RESPONSE', req.method, req.originalUrl, this.statusCode);
}
})
next();
}
};
app.use(request_logger("[app]"));
wsapp.use(request_logger("[wsapp]"));
app.use('/sourcemaps', sourcemapsReaderServer);
app.use('/assist', peerRouter);
wsapp.use('/assist', socket.wsRouter);
const server = app.listen(PORT, HOST, () => {
console.log(`App listening on http://${HOST}:${PORT}`);
console.log('Press Ctrl+C to quit.');
});
const wsserver = wsapp.listen(PORT + 1, HOST, () => {
console.log(`WS App listening on http://${HOST}:${PORT + 1}`);
console.log('Press Ctrl+C to quit.');
});
const peerServer = ExpressPeerServer(server, {
debug: true,
path: '/',
@ -45,6 +42,38 @@ peerServer.on('disconnect', peerDisconnect);
peerServer.on('error', peerError);
app.use('/', peerServer);
app.enable('trust proxy');
wsapp.enable('trust proxy');
socket.start(wsserver);
module.exports = {wsserver, server};
const {App} = require("uWebSockets.js");
const PREFIX = process.env.prefix || '/assist'
const uapp = new App();
const healthFn = (res, req) => {
res.writeStatus('200 OK').end('ok!');
}
uapp.get(PREFIX, healthFn);
uapp.get(`${PREFIX}/`, healthFn);
const uWrapper = function (fn) {
return (res, req) => fn(req, res);
}
uapp.get(`${PREFIX}/${process.env.S3_KEY}/sockets-list`, uWrapper(socket.handlers.socketsList));
uapp.get(`${PREFIX}/${process.env.S3_KEY}/sockets-list/:projectKey`, uWrapper(socket.handlers.socketsListByProject));
uapp.get(`${PREFIX}/${process.env.S3_KEY}/sockets-live`, uWrapper(socket.handlers.socketsLive));
uapp.get(`${PREFIX}/${process.env.S3_KEY}/sockets-live/:projectKey`, uWrapper(socket.handlers.socketsLiveByProject));
socket.start(uapp);
uapp.listen(HOST, PORT + 1, (token) => {
if (!token) {
console.warn("port already in use");
}
console.log(`WS App listening on http://${HOST}:${PORT + 1}`);
console.log('Press Ctrl+C to quit.');
});
module.exports = {uapp, server};

53
utilities/server_back.js Normal file
View file

@ -0,0 +1,53 @@
var sourcemapsReaderServer = require('./servers/sourcemaps-server');
var {peerRouter, peerConnection, peerDisconnect, peerError} = require('./servers/peerjs-server');
var express = require('express');
const {ExpressPeerServer} = require('peer');
const socket = require("./servers/websocket");
const HOST = '0.0.0.0';
const PORT = 9000;
var app = express();
var wsapp = express();
let debug = process.env.debug === "1" || false;
const request_logger = (identity) => {
return (req, res, next) => {
debug && console.log(identity, new Date().toTimeString(), 'REQUEST', req.method, req.originalUrl);
res.on('finish', function () {
if (this.statusCode !== 200 || debug) {
console.log(new Date().toTimeString(), 'RESPONSE', req.method, req.originalUrl, this.statusCode);
}
})
next();
}
};
app.use(request_logger("[app]"));
wsapp.use(request_logger("[wsapp]"));
app.use('/sourcemaps', sourcemapsReaderServer);
app.use('/assist', peerRouter);
wsapp.use('/assist', socket.wsRouter);
const server = app.listen(PORT, HOST, () => {
console.log(`App listening on http://${HOST}:${PORT}`);
console.log('Press Ctrl+C to quit.');
});
const wsserver = wsapp.listen(PORT + 1, HOST, () => {
console.log(`WS App listening on http://${HOST}:${PORT + 1}`);
console.log('Press Ctrl+C to quit.');
});
const peerServer = ExpressPeerServer(server, {
debug: true,
path: '/',
proxied: true,
allow_discovery: false
});
peerServer.on('connection', peerConnection);
peerServer.on('disconnect', peerDisconnect);
peerServer.on('error', peerError);
app.use('/', peerServer);
app.enable('trust proxy');
wsapp.enable('trust proxy');
socket.start(wsserver);
module.exports = {wsserver, server};

View file

@ -1,9 +1,7 @@
const _io = require('socket.io');
const express = require('express');
const uaParser = require('ua-parser-js');
const geoip2Reader = require('@maxmind/geoip2-node').Reader;
var {extractPeerId} = require('./peerjs-server');
var wsRouter = express.Router();
const IDENTITIES = {agent: 'agent', session: 'session'};
const NEW_AGENT = "NEW_AGENT";
const NO_AGENTS = "NO_AGENT";
@ -11,12 +9,12 @@ const AGENT_DISCONNECT = "AGENT_DISCONNECTED";
const AGENTS_CONNECTED = "AGENTS_CONNECTED";
const NO_SESSIONS = "SESSION_DISCONNECTED";
const SESSION_ALREADY_CONNECTED = "SESSION_ALREADY_CONNECTED";
// const wsReconnectionTimeout = process.env.wsReconnectionTimeout | 10 * 1000;
let io;
wsRouter.get(`/${process.env.S3_KEY}/sockets-list`, function (req, res) {
console.log("[WS]looking for all available sessions");
let debug = process.env.debug === "1" || false;
const socketsList = function (req, res) {
debug && console.log("[WS]looking for all available sessions");
let liveSessions = {};
for (let peerId of io.sockets.adapter.rooms.keys()) {
let {projectKey, sessionId} = extractPeerId(peerId);
@ -25,12 +23,11 @@ wsRouter.get(`/${process.env.S3_KEY}/sockets-list`, function (req, res) {
liveSessions[projectKey].push(sessionId);
}
}
res.statusCode = 200;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({"data": liveSessions}));
});
wsRouter.get(`/${process.env.S3_KEY}/sockets-list/:projectKey`, function (req, res) {
console.log(`[WS]looking for available sessions for ${req.params.projectKey}`);
res.writeStatus('200 OK').writeHeader('Content-Type', 'application/json').end(JSON.stringify({"data": liveSessions}));
}
const socketsListByProject = function (req, res) {
req.params = {projectKey: req.getParameter(0)};
debug && console.log(`[WS]looking for available sessions for ${req.params.projectKey}`);
let liveSessions = {};
for (let peerId of io.sockets.adapter.rooms.keys()) {
let {projectKey, sessionId} = extractPeerId(peerId);
@ -39,13 +36,10 @@ wsRouter.get(`/${process.env.S3_KEY}/sockets-list/:projectKey`, function (req, r
liveSessions[projectKey].push(sessionId);
}
}
res.statusCode = 200;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({"data": liveSessions[req.params.projectKey] || []}));
});
wsRouter.get(`/${process.env.S3_KEY}/sockets-live`, async function (req, res) {
console.log("[WS]looking for all available LIVE sessions");
res.writeStatus('200 OK').writeHeader('Content-Type', 'application/json').end(JSON.stringify({"data": liveSessions[req.params.projectKey] || []}));
}
const socketsLive = async function (req, res) {
debug && console.log("[WS]looking for all available LIVE sessions");
let liveSessions = {};
for (let peerId of io.sockets.adapter.rooms.keys()) {
let {projectKey, sessionId} = extractPeerId(peerId);
@ -59,13 +53,11 @@ wsRouter.get(`/${process.env.S3_KEY}/sockets-live`, async function (req, res) {
}
}
}
res.statusCode = 200;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({"data": liveSessions}));
});
wsRouter.get(`/${process.env.S3_KEY}/sockets-live/:projectKey`, async function (req, res) {
console.log(`[WS]looking for available LIVE sessions for ${req.params.projectKey}`);
res.writeStatus('200 OK').writeHeader('Content-Type', 'application/json').end(JSON.stringify({"data": liveSessions}));
}
const socketsLiveByProject = async function (req, res) {
req.params = {projectKey: req.getParameter(0)};
debug && console.log(`[WS]looking for available LIVE sessions for ${req.params.projectKey}`);
let liveSessions = {};
for (let peerId of io.sockets.adapter.rooms.keys()) {
let {projectKey, sessionId} = extractPeerId(peerId);
@ -79,10 +71,8 @@ wsRouter.get(`/${process.env.S3_KEY}/sockets-live/:projectKey`, async function (
}
}
}
res.statusCode = 200;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({"data": liveSessions[req.params.projectKey] || []}));
});
res.writeStatus('200 OK').writeHeader('Content-Type', 'application/json').end(JSON.stringify({"data": liveSessions[req.params.projectKey] || []}));
}
const findSessionSocketId = async (io, peerId) => {
const connected_sockets = await io.in(peerId).fetchSockets();
@ -128,8 +118,8 @@ async function get_all_agents_ids(io, socket) {
function extractSessionInfo(socket) {
if (socket.handshake.query.sessionInfo !== undefined) {
console.log("received headers");
console.log(socket.handshake.headers);
debug && console.log("received headers");
debug && console.log(socket.handshake.headers);
socket.handshake.query.sessionInfo = JSON.parse(socket.handshake.query.sessionInfo);
let ua = uaParser(socket.handshake.headers['user-agent']);
@ -146,8 +136,8 @@ function extractSessionInfo(socket) {
// console.log("Looking for MMDB file in " + process.env.MAXMINDDB_FILE);
geoip2Reader.open(process.env.MAXMINDDB_FILE, options)
.then(reader => {
console.log("looking for location of ");
console.log(socket.handshake.headers['x-forwarded-for'] || socket.handshake.address);
debug && console.log("looking for location of ");
debug && console.log(socket.handshake.headers['x-forwarded-for'] || socket.handshake.address);
let country = reader.country(socket.handshake.headers['x-forwarded-for'] || socket.handshake.address);
socket.handshake.query.sessionInfo.userCountry = country.country.isoCode;
})
@ -158,19 +148,22 @@ function extractSessionInfo(socket) {
}
module.exports = {
wsRouter,
start: (server) => {
io = _io(server, {
maxHttpBufferSize: 7e6,
io = new _io.Server({
maxHttpBufferSize: 1e6,
cors: {
origin: "*",
methods: ["GET", "POST", "PUT"]
},
path: '/socket'
path: '/ws-assist/socket',
transports: ['websocket'],
// upgrade: false
});
io.attachApp(server);
io.on('connection', async (socket) => {
console.log(`WS started:${socket.id}, Query:${JSON.stringify(socket.handshake.query)}`);
debug && console.log(`WS started:${socket.id}, Query:${JSON.stringify(socket.handshake.query)}`);
socket.peerId = socket.handshake.query.peerId;
socket.identity = socket.handshake.query.identity;
const {projectKey, sessionId} = extractPeerId(socket.peerId);
@ -180,24 +173,24 @@ module.exports = {
let {c_sessions, c_agents} = await sessions_agents_count(io, socket);
if (socket.identity === IDENTITIES.session) {
if (c_sessions > 0) {
console.log(`session already connected, refusing new connexion`);
debug && console.log(`session already connected, refusing new connexion`);
io.to(socket.id).emit(SESSION_ALREADY_CONNECTED);
return socket.disconnect();
}
extractSessionInfo(socket);
if (c_agents > 0) {
console.log(`notifying new session about agent-existence`);
debug && console.log(`notifying new session about agent-existence`);
let agents_ids = await get_all_agents_ids(io, socket);
io.to(socket.id).emit(AGENTS_CONNECTED, agents_ids);
}
} else if (c_sessions <= 0) {
console.log(`notifying new agent about no SESSIONS`);
debug && console.log(`notifying new agent about no SESSIONS`);
io.to(socket.id).emit(NO_SESSIONS);
}
socket.join(socket.peerId);
if (io.sockets.adapter.rooms.get(socket.peerId)) {
console.log(`${socket.id} joined room:${socket.peerId}, as:${socket.identity}, members:${io.sockets.adapter.rooms.get(socket.peerId).size}`);
debug && console.log(`${socket.id} joined room:${socket.peerId}, as:${socket.identity}, members:${io.sockets.adapter.rooms.get(socket.peerId).size}`);
}
if (socket.identity === IDENTITIES.agent) {
if (socket.handshake.query.agentInfo !== undefined) {
@ -208,23 +201,23 @@ module.exports = {
socket.on('disconnect', async () => {
// console.log(`${socket.id} disconnected from ${socket.peerId}, waiting ${wsReconnectionTimeout / 1000}s before checking remaining`);
console.log(`${socket.id} disconnected from ${socket.peerId}`);
debug && console.log(`${socket.id} disconnected from ${socket.peerId}`);
if (socket.identity === IDENTITIES.agent) {
socket.to(socket.peerId).emit(AGENT_DISCONNECT, socket.id);
}
// wait a little bit before notifying everyone
// setTimeout(async () => {
console.log("checking for number of connected agents and sessions");
debug && console.log("checking for number of connected agents and sessions");
let {c_sessions, c_agents} = await sessions_agents_count(io, socket);
if (c_sessions === -1 && c_agents === -1) {
console.log(`room not found: ${socket.peerId}`);
debug && console.log(`room not found: ${socket.peerId}`);
}
if (c_sessions === 0) {
console.log(`notifying everyone in ${socket.peerId} about no SESSIONS`);
debug && console.log(`notifying everyone in ${socket.peerId} about no SESSIONS`);
socket.to(socket.peerId).emit(NO_SESSIONS);
}
if (c_agents === 0) {
console.log(`notifying everyone in ${socket.peerId} about no AGENTS`);
debug && console.log(`notifying everyone in ${socket.peerId} about no AGENTS`);
socket.to(socket.peerId).emit(NO_AGENTS);
}
@ -235,22 +228,52 @@ module.exports = {
socket.onAny(async (eventName, ...args) => {
socket.lastMessageReceivedAt = Date.now();
if (socket.identity === IDENTITIES.session) {
console.log(`received event:${eventName}, from:${socket.identity}, sending message to room:${socket.peerId}, members: ${io.sockets.adapter.rooms.get(socket.peerId).size}`);
debug && console.log(`received event:${eventName}, from:${socket.identity}, sending message to room:${socket.peerId}, members: ${io.sockets.adapter.rooms.get(socket.peerId).size}`);
socket.to(socket.peerId).emit(eventName, args[0]);
} else {
console.log(`received event:${eventName}, from:${socket.identity}, sending message to session of room:${socket.peerId}, members:${io.sockets.adapter.rooms.get(socket.peerId).size}`);
debug && console.log(`received event:${eventName}, from:${socket.identity}, sending message to session of room:${socket.peerId}, members:${io.sockets.adapter.rooms.get(socket.peerId).size}`);
let socketId = await findSessionSocketId(io, socket.peerId);
if (socketId === null) {
console.log(`session not found for:${socket.peerId}`);
debug && console.log(`session not found for:${socket.peerId}`);
io.to(socket.id).emit(NO_SESSIONS);
} else {
console.log("message sent");
debug && console.log("message sent");
io.to(socketId).emit(eventName, socket.id, args[0]);
}
}
});
});
console.log("WS server started")
console.log("WS server started");
debug ? console.log("Debugging enabled.") : console.log("Debugging disabled, set debug=\"1\" to enable debugging.");
setInterval((io) => {
try {
let count = 0;
console.log(` ====== Rooms: ${io.sockets.adapter.rooms.size} ====== `);
const arr = Array.from(io.sockets.adapter.rooms)
const filtered = arr.filter(room => !room[1].has(room[0]))
for (let i of filtered) {
let {projectKey, sessionId} = extractPeerId(i[0]);
if (projectKey !== null && sessionId !== null) {
count++;
}
}
console.log(` ====== Valid Rooms: ${count} ====== `);
if (debug) {
for (let item of filtered) {
console.log(`Room: ${item[0]} connected: ${item[1].size}`)
}
}
} catch (e) {
console.error(e);
}
}, 20000, io);
},
handlers: {
socketsList,
socketsListByProject,
socketsLive,
socketsLiveByProject
}
};

View file

@ -0,0 +1,272 @@
const _io = require('socket.io');
const express = require('express');
const uaParser = require('ua-parser-js');
const geoip2Reader = require('@maxmind/geoip2-node').Reader;
var {extractPeerId} = require('./peerjs-server');
var wsRouter = express.Router();
const IDENTITIES = {agent: 'agent', session: 'session'};
const NEW_AGENT = "NEW_AGENT";
const NO_AGENTS = "NO_AGENT";
const AGENT_DISCONNECT = "AGENT_DISCONNECTED";
const AGENTS_CONNECTED = "AGENTS_CONNECTED";
const NO_SESSIONS = "SESSION_DISCONNECTED";
const SESSION_ALREADY_CONNECTED = "SESSION_ALREADY_CONNECTED";
// const wsReconnectionTimeout = process.env.wsReconnectionTimeout | 10 * 1000;
let io;
let debug = process.env.debug === "1" || false;
wsRouter.get(`/${process.env.S3_KEY}/sockets-list`, function (req, res) {
debug && console.log("[WS]looking for all available sessions");
let liveSessions = {};
for (let peerId of io.sockets.adapter.rooms.keys()) {
let {projectKey, sessionId} = extractPeerId(peerId);
if (projectKey !== undefined) {
liveSessions[projectKey] = liveSessions[projectKey] || [];
liveSessions[projectKey].push(sessionId);
}
}
res.statusCode = 200;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({"data": liveSessions}));
});
wsRouter.get(`/${process.env.S3_KEY}/sockets-list/:projectKey`, function (req, res) {
debug && console.log(`[WS]looking for available sessions for ${req.params.projectKey}`);
let liveSessions = {};
for (let peerId of io.sockets.adapter.rooms.keys()) {
let {projectKey, sessionId} = extractPeerId(peerId);
if (projectKey === req.params.projectKey) {
liveSessions[projectKey] = liveSessions[projectKey] || [];
liveSessions[projectKey].push(sessionId);
}
}
res.statusCode = 200;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({"data": liveSessions[req.params.projectKey] || []}));
});
wsRouter.get(`/${process.env.S3_KEY}/sockets-live`, async function (req, res) {
debug && console.log("[WS]looking for all available LIVE sessions");
let liveSessions = {};
for (let peerId of io.sockets.adapter.rooms.keys()) {
let {projectKey, sessionId} = extractPeerId(peerId);
if (projectKey !== undefined) {
let connected_sockets = await io.in(peerId).fetchSockets();
for (let item of connected_sockets) {
if (item.handshake.query.identity === IDENTITIES.session) {
liveSessions[projectKey] = liveSessions[projectKey] || [];
liveSessions[projectKey].push(item.handshake.query.sessionInfo);
}
}
}
}
res.statusCode = 200;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({"data": liveSessions}));
});
wsRouter.get(`/${process.env.S3_KEY}/sockets-live/:projectKey`, async function (req, res) {
debug && console.log(`[WS]looking for available LIVE sessions for ${req.params.projectKey}`);
let liveSessions = {};
for (let peerId of io.sockets.adapter.rooms.keys()) {
let {projectKey, sessionId} = extractPeerId(peerId);
if (projectKey === req.params.projectKey) {
let connected_sockets = await io.in(peerId).fetchSockets();
for (let item of connected_sockets) {
if (item.handshake.query.identity === IDENTITIES.session) {
liveSessions[projectKey] = liveSessions[projectKey] || [];
liveSessions[projectKey].push(item.handshake.query.sessionInfo);
}
}
}
}
res.statusCode = 200;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({"data": liveSessions[req.params.projectKey] || []}));
});
const findSessionSocketId = async (io, peerId) => {
const connected_sockets = await io.in(peerId).fetchSockets();
for (let item of connected_sockets) {
if (item.handshake.query.identity === IDENTITIES.session) {
return item.id;
}
}
return null;
};
async function sessions_agents_count(io, socket) {
let c_sessions = 0, c_agents = 0;
if (io.sockets.adapter.rooms.get(socket.peerId)) {
const connected_sockets = await io.in(socket.peerId).fetchSockets();
for (let item of connected_sockets) {
if (item.handshake.query.identity === IDENTITIES.session) {
c_sessions++;
} else {
c_agents++;
}
}
} else {
c_agents = -1;
c_sessions = -1;
}
return {c_sessions, c_agents};
}
async function get_all_agents_ids(io, socket) {
let agents = [];
if (io.sockets.adapter.rooms.get(socket.peerId)) {
const connected_sockets = await io.in(socket.peerId).fetchSockets();
for (let item of connected_sockets) {
if (item.handshake.query.identity === IDENTITIES.agent) {
agents.push(item.id);
}
}
}
return agents;
}
function extractSessionInfo(socket) {
if (socket.handshake.query.sessionInfo !== undefined) {
debug && console.log("received headers");
debug && console.log(socket.handshake.headers);
socket.handshake.query.sessionInfo = JSON.parse(socket.handshake.query.sessionInfo);
let ua = uaParser(socket.handshake.headers['user-agent']);
socket.handshake.query.sessionInfo.userOs = ua.os.name || null;
socket.handshake.query.sessionInfo.userBrowser = ua.browser.name || null;
socket.handshake.query.sessionInfo.userBrowserVersion = ua.browser.version || null;
socket.handshake.query.sessionInfo.userDevice = ua.device.model || null;
socket.handshake.query.sessionInfo.userDeviceType = ua.device.type || 'desktop';
socket.handshake.query.sessionInfo.userCountry = null;
const options = {
// you can use options like `cache` or `watchForUpdates`
};
// console.log("Looking for MMDB file in " + process.env.MAXMINDDB_FILE);
geoip2Reader.open(process.env.MAXMINDDB_FILE, options)
.then(reader => {
debug && console.log("looking for location of ");
debug && console.log(socket.handshake.headers['x-forwarded-for'] || socket.handshake.address);
let country = reader.country(socket.handshake.headers['x-forwarded-for'] || socket.handshake.address);
socket.handshake.query.sessionInfo.userCountry = country.country.isoCode;
})
.catch(error => {
console.error(error);
});
}
}
module.exports = {
wsRouter,
start: (server) => {
io = _io(server, {
maxHttpBufferSize: 1e6,
cors: {
origin: "*",
methods: ["GET", "POST", "PUT"]
},
path: '/socket'
});
io.on('connection', async (socket) => {
debug && console.log(`WS started:${socket.id}, Query:${JSON.stringify(socket.handshake.query)}`);
socket.peerId = socket.handshake.query.peerId;
socket.identity = socket.handshake.query.identity;
const {projectKey, sessionId} = extractPeerId(socket.peerId);
socket.sessionId = sessionId;
socket.projectKey = projectKey;
socket.lastMessageReceivedAt = Date.now();
let {c_sessions, c_agents} = await sessions_agents_count(io, socket);
if (socket.identity === IDENTITIES.session) {
if (c_sessions > 0) {
debug && console.log(`session already connected, refusing new connexion`);
io.to(socket.id).emit(SESSION_ALREADY_CONNECTED);
return socket.disconnect();
}
extractSessionInfo(socket);
if (c_agents > 0) {
debug && console.log(`notifying new session about agent-existence`);
let agents_ids = await get_all_agents_ids(io, socket);
io.to(socket.id).emit(AGENTS_CONNECTED, agents_ids);
}
} else if (c_sessions <= 0) {
debug && console.log(`notifying new agent about no SESSIONS`);
io.to(socket.id).emit(NO_SESSIONS);
}
socket.join(socket.peerId);
if (io.sockets.adapter.rooms.get(socket.peerId)) {
debug && console.log(`${socket.id} joined room:${socket.peerId}, as:${socket.identity}, members:${io.sockets.adapter.rooms.get(socket.peerId).size}`);
}
if (socket.identity === IDENTITIES.agent) {
if (socket.handshake.query.agentInfo !== undefined) {
socket.handshake.query.agentInfo = JSON.parse(socket.handshake.query.agentInfo);
}
socket.to(socket.peerId).emit(NEW_AGENT, socket.id, socket.handshake.query.agentInfo);
}
socket.on('disconnect', async () => {
debug && console.log(`${socket.id} disconnected from ${socket.peerId}`);
if (socket.identity === IDENTITIES.agent) {
socket.to(socket.peerId).emit(AGENT_DISCONNECT, socket.id);
}
debug && console.log("checking for number of connected agents and sessions");
let {c_sessions, c_agents} = await sessions_agents_count(io, socket);
if (c_sessions === -1 && c_agents === -1) {
debug && console.log(`room not found: ${socket.peerId}`);
}
if (c_sessions === 0) {
debug && console.log(`notifying everyone in ${socket.peerId} about no SESSIONS`);
socket.to(socket.peerId).emit(NO_SESSIONS);
}
if (c_agents === 0) {
debug && console.log(`notifying everyone in ${socket.peerId} about no AGENTS`);
socket.to(socket.peerId).emit(NO_AGENTS);
}
});
socket.onAny(async (eventName, ...args) => {
socket.lastMessageReceivedAt = Date.now();
if (socket.identity === IDENTITIES.session) {
debug && console.log(`received event:${eventName}, from:${socket.identity}, sending message to room:${socket.peerId}, members: ${io.sockets.adapter.rooms.get(socket.peerId).size}`);
socket.to(socket.peerId).emit(eventName, args[0]);
} else {
debug && console.log(`received event:${eventName}, from:${socket.identity}, sending message to session of room:${socket.peerId}, members:${io.sockets.adapter.rooms.get(socket.peerId).size}`);
let socketId = await findSessionSocketId(io, socket.peerId);
if (socketId === null) {
debug && console.log(`session not found for:${socket.peerId}`);
io.to(socket.id).emit(NO_SESSIONS);
} else {
debug && console.log("message sent");
io.to(socketId).emit(eventName, socket.id, args[0]);
}
}
});
});
console.log("WS server started")
setInterval((io) => {
try {
let count = 0;
console.log(` ====== Rooms: ${io.sockets.adapter.rooms.size} ====== `);
const arr = Array.from(io.sockets.adapter.rooms)
const filtered = arr.filter(room => !room[1].has(room[0]))
for (let i of filtered) {
let {projectKey, sessionId} = extractPeerId(i[0]);
if (projectKey !== null && sessionId !== null) {
count++;
}
}
console.log(` ====== Valid Rooms: ${count} ====== `);
if (debug) {
for (let item of filtered) {
console.log(`Room: ${item[0]} connected: ${item[1].size}`)
}
}
} catch (e) {
console.error(e);
}
}, 20000, io);
}
};