diff --git a/frontend/app/assets/img/live-sessions.png b/frontend/app/assets/img/live-sessions.png new file mode 100644 index 000000000..85ecaae86 Binary files /dev/null and b/frontend/app/assets/img/live-sessions.png differ diff --git a/frontend/app/components/BugFinder/LiveSessionList/LiveSessionList.tsx b/frontend/app/components/BugFinder/LiveSessionList/LiveSessionList.tsx index b82e80139..7c60216c8 100644 --- a/frontend/app/components/BugFinder/LiveSessionList/LiveSessionList.tsx +++ b/frontend/app/components/BugFinder/LiveSessionList/LiveSessionList.tsx @@ -35,8 +35,8 @@ function LiveSessionList(props: Props) {
} show={ !loading && list && list.size === 0} > diff --git a/frontend/app/components/Client/Integrations/AssistDoc/AssistDoc.js b/frontend/app/components/Client/Integrations/AssistDoc/AssistDoc.js new file mode 100644 index 000000000..01d3e7ad2 --- /dev/null +++ b/frontend/app/components/Client/Integrations/AssistDoc/AssistDoc.js @@ -0,0 +1,66 @@ +import Highlight from 'react-highlight' +import ToggleContent from 'Shared/ToggleContent' +import DocLink from 'Shared/DocLink/DocLink'; + +const AssistDoc = (props) => { + return ( +
+
OpenReplay Assist allows you to support your users by seeing their live screen and instantly hopping on call (WebRTC) with them without requiring any 3rd-party screen sharing software.
+ +
Installation
+ + {`npm i @openreplay/tracker-assist`} + + +
Usage
+

Initialize the tracker then load the @openreplay/tracker-assist plugin.

+
+ +
Usage
+ + {`import Tracker from '@openreplay/tracker'; +import trackerAssist from '@openreplay/tracker-assist'; +const tracker = new Tracker({ + projectKey: PROJECT_KEY, +}); +tracker.start(); +tracker.use(trackerAssist(options)); // check the list of available options below`} + + } + second={ + + {`import OpenReplay from '@openreplay/tracker/cjs'; +import trackerFetch from '@openreplay/tracker-assist/cjs'; +const tracker = new OpenReplay({ + projectKey: PROJECT_KEY +}); +const trackerAssist = tracker.use(trackerAssist(options)); // check the list of available options below +//... +function MyApp() { + useEffect(() => { // use componentDidMount in case of React Class Component + tracker.start(); + }, []) +//... +}`} + + } + /> + +
Options
+ + {`trackerAssist({ + confirmText: string; +})`} + + + +
+ ) +}; + +AssistDoc.displayName = "AssistDoc"; + +export default AssistDoc; diff --git a/frontend/app/components/Client/Integrations/AssistDoc/index.js b/frontend/app/components/Client/Integrations/AssistDoc/index.js new file mode 100644 index 000000000..6086d5389 --- /dev/null +++ b/frontend/app/components/Client/Integrations/AssistDoc/index.js @@ -0,0 +1 @@ +export { default } from './AssistDoc' \ No newline at end of file diff --git a/frontend/app/components/Client/Integrations/Integrations.js b/frontend/app/components/Client/Integrations/Integrations.js index d90587e5f..05036788e 100644 --- a/frontend/app/components/Client/Integrations/Integrations.js +++ b/frontend/app/components/Client/Integrations/Integrations.js @@ -28,6 +28,7 @@ import SlackAddForm from './SlackAddForm'; import FetchDoc from './FetchDoc'; import MobxDoc from './MobxDoc'; import ProfilerDoc from './ProfilerDoc'; +import AssistDoc from './AssistDoc'; const NONE = -1; const SENTRY = 0; @@ -49,6 +50,7 @@ const SLACK = 15; const FETCH = 16; const MOBX = 17; const PROFILER = 18; +const ASSIST = 19; const TITLE = { [ SENTRY ]: 'Sentry', @@ -70,6 +72,7 @@ const TITLE = { [ FETCH ] : 'Fetch', [ MOBX ] : 'MobX', [ PROFILER ] : 'Profiler', + [ ASSIST ] : 'Assist', } const DOCS = [REDUX, VUE, GRAPHQL, NGRX, FETCH, MOBX, PROFILER] @@ -182,6 +185,8 @@ export default class Integrations extends React.PureComponent { return case PROFILER: return + case ASSIST: + return default: return null; } @@ -253,7 +258,7 @@ export default class Integrations extends React.PureComponent { {plugins && (
Use plugins to better debug your application's store, monitor queries and track performance issues.
-
+
this.showIntegrationConfig(PROFILER) } // integrated={ sentryIntegrated } /> + this.showIntegrationConfig(ASSIST) } + // integrated={ sentryIntegrated } + />
)} diff --git a/frontend/app/components/Client/ManageUsers/ManageUsers.js b/frontend/app/components/Client/ManageUsers/ManageUsers.js index 9f0a4244d..3e1182f41 100644 --- a/frontend/app/components/Client/ManageUsers/ManageUsers.js +++ b/frontend/app/components/Client/ManageUsers/ManageUsers.js @@ -1,8 +1,8 @@ import { connect } from 'react-redux'; import cn from 'classnames'; import withPageTitle from 'HOCs/withPageTitle'; -import { IconButton, SlideModal, Input, Button, Loader, NoContent, Popup } from 'UI'; -import { init, save, edit, remove as deleteMember, fetchList } from 'Duck/member'; +import { IconButton, SlideModal, Input, Button, Loader, NoContent, Popup, CopyButton } from 'UI'; +import { init, save, edit, remove as deleteMember, fetchList, generateInviteLink } from 'Duck/member'; import styles from './manageUsers.css'; import UserItem from './UserItem'; import { confirm } from 'UI/Confirmation'; @@ -24,11 +24,12 @@ const LIMIT_WARNING = 'You have reached users limit.'; save, edit, deleteMember, - fetchList + fetchList, + generateInviteLink }) @withPageTitle('Manage Users - OpenReplay Preferences') class ManageUsers extends React.PureComponent { - state = { showModal: false, remaining: this.props.account.limits.teamMember.remaining } + state = { showModal: false, remaining: this.props.account.limits.teamMember.remaining, invited: false } onChange = (e, { name, value }) => this.props.edit({ [ name ]: value }); onChangeCheckbox = ({ target: { checked, name } }) => this.props.edit({ [ name ]: checked }); @@ -70,11 +71,12 @@ class ManageUsers extends React.PureComponent { toast.error(e); }) } - this.closeModal() + this.setState({ invited: true }) + // this.closeModal() }); } - - formContent = member => ( + + formContent = (member, account) => (
@@ -99,7 +101,11 @@ class ManageUsers extends React.PureComponent { className={ styles.input } />
- + { !account.smtp && +
+ SMTP is not configured, setup SMTP +
+ }
-
{ 'Can manage Projects and Users.' }
+
{ 'Can manage Projects and team members.' }
- - +
+
+ + +
+ { !member.joined && member.invitationLink && + + } +
) @@ -144,7 +161,7 @@ class ManageUsers extends React.PureComponent { const { members, member, loading, account, hideHeader = false, } = this.props; - const { showModal, remaining } = this.state; + const { showModal, remaining, invited } = this.state; const isAdmin = account.admin || account.superAdmin; const canAddUsers = isAdmin && remaining !== 0; @@ -155,7 +172,7 @@ class ManageUsers extends React.PureComponent { title="Inivte People" size="small" isDisplayed={ showModal } - content={ this.formContent(member) } + content={ this.formContent(member, account) } onClose={ this.closeModal } />
@@ -202,6 +219,7 @@ class ManageUsers extends React.PureComponent { { members.map(user => ( ( +const UserItem = ({ user, adminLabel, deleteHandler, editHandler, generateInviteLink }) => (
{ user.name || user.email }
{ adminLabel &&
{ adminLabel }
}
+ { user.expiredInvitation && !user.joined && + generateInviteLink(user) } id="trash"> + +
+ } + content={ `Generate Invitation Link` } + size="tiny" + inverted + position="top center" + /> + } + { !user.expiredInvitation && !user.joined && user.invitationLink && + + } + /> +
+ } + content={ `Copy Invitation Link` } + size="tiny" + inverted + position="top center" + /> + } { !!deleteHandler &&
deleteHandler(user) } id="trash"> diff --git a/frontend/app/components/Client/ManageUsers/manageUsers.css b/frontend/app/components/Client/ManageUsers/manageUsers.css index 64e529d92..f62e7367a 100644 --- a/frontend/app/components/Client/ManageUsers/manageUsers.css +++ b/frontend/app/components/Client/ManageUsers/manageUsers.css @@ -34,4 +34,9 @@ .adminInfo { font-size: 12px; color: $gray-medium; +} + +.smtpMessage { + background-color: #faf6e0; + border-radius: 3px; } \ No newline at end of file diff --git a/frontend/app/components/Client/ManageUsers/userItem.css b/frontend/app/components/Client/ManageUsers/userItem.css index 599aa34f1..6de747561 100644 --- a/frontend/app/components/Client/ManageUsers/userItem.css +++ b/frontend/app/components/Client/ManageUsers/userItem.css @@ -35,6 +35,9 @@ padding: 5px; cursor: pointer; margin-left: 10px; + display: flex; + align-items: center; + justify-content: center; &:hover { & svg { fill: $teal-dark; diff --git a/frontend/app/components/ForgotPassword/ForgotPassword.js b/frontend/app/components/ForgotPassword/ForgotPassword.js index 5d9cc504f..6c4b0d476 100644 --- a/frontend/app/components/ForgotPassword/ForgotPassword.js +++ b/frontend/app/components/ForgotPassword/ForgotPassword.js @@ -4,6 +4,8 @@ import withPageTitle from 'HOCs/withPageTitle'; import { Loader, Button, Link, Icon, Message } from 'UI'; import { requestResetPassword, resetPassword } from 'Duck/user'; import { login as loginRoute } from 'App/routes'; +import { withRouter } from 'react-router-dom'; +import { validateEmail } from 'App/validate'; import cn from 'classnames'; import stl from './forgotPassword.css'; @@ -17,14 +19,16 @@ const checkDontMatch = (newPassword, newPasswordRepeat) => newPasswordRepeat.length > 0 && newPasswordRepeat !== newPassword; @connect( - state => ({ + (state, props) => ({ errors: state.getIn([ 'user', 'requestResetPassowrd', 'errors' ]), resetErrors: state.getIn([ 'user', 'resetPassword', 'errors' ]), loading: state.getIn([ 'user', 'requestResetPassowrd', 'loading' ]), + params: new URLSearchParams(props.location.search) }), { requestResetPassword, resetPassword }, ) @withPageTitle("Password Reset - OpenReplay") +@withRouter export default class ForgotPassword extends React.PureComponent { state = { email: '', @@ -37,15 +41,20 @@ export default class ForgotPassword extends React.PureComponent { handleSubmit = (token) => { const { email, requested, code, password } = this.state; + const { params } = this.props; - if (!requested) { + const pass = params.get('pass') + const invitation = params.get('invitation') + const resetting = pass && invitation + + if (!resetting) { this.props.requestResetPassword({ email: email.trim(), 'g-recaptcha-response': token }).then(() => { const { errors } = this.props; if (!errors) this.setState({ requested: true }); }); } else { if (this.isSubmitDisabled()) return; - this.props.resetPassword({ email: email.trim(), code, password }).then(() => { + this.props.resetPassword({ email: email.trim(), invitation, pass, password }).then(() => { const { resetErrors } = this.props; if (!resetErrors) this.setState({ updated: true }); }); @@ -78,9 +87,14 @@ export default class ForgotPassword extends React.PureComponent { } render() { - const { errors, loading } = this.props; - const { requested, updated, password, passwordRepeat, code } = this.state; - const dontMatch = checkDontMatch(password, passwordRepeat); + const { errors, loading, params } = this.props; + const { requested, updated, password, passwordRepeat, email } = this.state; + const dontMatch = checkDontMatch(password, passwordRepeat); + + const pass = params.get('pass') + const invitation = params.get('invitation') + const resetting = pass && invitation + const validEmail = validateEmail(email) return (
@@ -113,7 +127,7 @@ export default class ForgotPassword extends React.PureComponent {
)} - { !requested ? + { !resetting && !requested &&
-
- : - -
- - -
+
+ } -
- - -
-
- { PASSWORD_POLICY } -
-
- - -
- + { + requested && ( +
Reset password link has been sent to your email.
+ ) + } + + { + resetting && ( + + {/*
+ + +
*/} + +
+ + +
+
+ { PASSWORD_POLICY } +
+
+ + +
+
+ ) }
- +
diff --git a/frontend/app/components/ui/CopyButton/CopyButton.js b/frontend/app/components/ui/CopyButton/CopyButton.js index d080b1a02..2eeafd8d3 100644 --- a/frontend/app/components/ui/CopyButton/CopyButton.js +++ b/frontend/app/components/ui/CopyButton/CopyButton.js @@ -2,7 +2,7 @@ import React from 'react' import { useState } from 'react'; import copy from 'copy-to-clipboard'; -function CopyButton({ content, className }) { +function CopyButton({ content, className, btnText = 'copy' }) { const [copied, setCopied] = useState(false) const copyHandler = () => { @@ -17,7 +17,7 @@ function CopyButton({ content, className }) { className={ className } onClick={ copyHandler } > - { copied ? 'copied' : 'copy' } + { copied ? 'copied' : btnText } ) } diff --git a/frontend/app/components/ui/NoContent/NoContent.js b/frontend/app/components/ui/NoContent/NoContent.js index ee3e29b90..1561cc451 100644 --- a/frontend/app/components/ui/NoContent/NoContent.js +++ b/frontend/app/components/ui/NoContent/NoContent.js @@ -9,8 +9,12 @@ export default ({ show = true, children = null, empty = false, + image = null }) => (!show ? children :
+ { + image && image + } { icon &&
} diff --git a/frontend/app/duck/member.js b/frontend/app/duck/member.js index 94e2f4e2f..31cccb395 100644 --- a/frontend/app/duck/member.js +++ b/frontend/app/duck/member.js @@ -1,16 +1,48 @@ +import { Map } from 'immutable'; import Member from 'Types/member'; import crudDuckGenerator from './tools/crudDuck'; +import withRequestState, { RequestTypes } from 'Duck/requestStateCreator'; +import { reduceDucks } from 'Duck/tools'; + +const GENERATE_LINK = new RequestTypes('member/GENERATE_LINK'); const crudDuck = crudDuckGenerator('client/member', Member, { idKey: 'id' }); -export const { - fetchList, init, edit, remove, -} = crudDuck.actions; +export const { fetchList, init, edit, remove, } = crudDuck.actions; + +const initialState = Map({ + definedPercent: 0, +}); + +const reducer = (state = initialState, action = {}) => { + switch (action.type) { + case GENERATE_LINK.SUCCESS: + return state.update( + 'list', + list => list + .map(member => { + if(member.id === action.id) { + return Member({...member.toJS(), invitationLink: action.data.invitationLink }) + } + return member + }) + ); + } + return state; +}; export function save(instance) { return { types: crudDuck.actionTypes.SAVE.toArray(), - call: client => client.put( instance.id ? `/client/members/${ instance.id }` : '/client/members', instance.toData()), + call: client => client.put( instance.id ? `/client/members/${ instance.id }` : '/client/members', instance.toData()), }; } -export default crudDuck.reducer; +export function generateInviteLink(instance) { + return { + types: GENERATE_LINK.toArray(), + call: client => client.get(`/client/members/${ instance.id }/reset`), + id: instance.id + }; +} + +export default reduceDucks(crudDuck, { initialState, reducer }).reducer; diff --git a/frontend/app/duck/user.js b/frontend/app/duck/user.js index deb41e715..5395792c3 100644 --- a/frontend/app/duck/user.js +++ b/frontend/app/duck/user.js @@ -52,7 +52,7 @@ const reducer = (state = initialState, action = {}) => { case UPDATE_PASSWORD.SUCCESS: case LOGIN.SUCCESS: return setClient( - state.set('account', Account(action.data.user)), + state.set('account', Account({...action.data.user, smtp: action.data.client.smtp })), action.data.client, ); case SIGNUP.SUCCESS: diff --git a/frontend/app/player/MessageDistributor/managers/AssistManager.ts b/frontend/app/player/MessageDistributor/managers/AssistManager.ts index 26e683bd2..132193bd5 100644 --- a/frontend/app/player/MessageDistributor/managers/AssistManager.ts +++ b/frontend/app/player/MessageDistributor/managers/AssistManager.ts @@ -125,9 +125,9 @@ export default class AssistManager { this.md.setMessagesLoading(false); } if (status === ConnectionStatus.Connected) { - this.md.display(true); + // this.md.display(true); } else { - this.md.display(false); + // this.md.display(false); } update({ peerConnectionStatus: status }); } diff --git a/frontend/app/svg/icons/integrations/assist.svg b/frontend/app/svg/icons/integrations/assist.svg new file mode 100644 index 000000000..9563278c4 --- /dev/null +++ b/frontend/app/svg/icons/integrations/assist.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/app/svg/icons/link-45deg.svg b/frontend/app/svg/icons/link-45deg.svg new file mode 100644 index 000000000..616baf9ae --- /dev/null +++ b/frontend/app/svg/icons/link-45deg.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/frontend/app/types/member.js b/frontend/app/types/member.js index 2cbea16d4..f712c7347 100644 --- a/frontend/app/types/member.js +++ b/frontend/app/types/member.js @@ -9,6 +9,9 @@ export default Record({ createdAt: undefined, admin: false, superAdmin: false, + joined: false, + expiredInvitation: false, + invitationLink: '' }, { idKey: 'id', methods: {