changes(ui) - user invite link and assist changes (#116)

* change(ui) - assist installation link in onboarding

* change(ui) - invite link

* change(ui) - reset params

* change(ui) - unused component

* feature(ui) - user changes icon

* changes(ui) - invite link, and assist changes

* fix(ui) - smtp flag
This commit is contained in:
Shekar Siri 2021-08-04 21:09:29 +05:30 committed by GitHub
parent d158824c07
commit a98cbe883c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 300 additions and 90 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

View file

@ -35,8 +35,8 @@ function LiveSessionList(props: Props) {
<div>
<NoContent
title={"No live sessions!"}
subtext="Please try changing your search parameters."
icon="exclamation-circle"
// subtext="Please try changing your search parameters."
image={<img src="/img/live-sessions.png" style={{ width: '70%', marginBottom: '30px' }}/>}
show={ !loading && list && list.size === 0}
>
<Loader loading={ loading }>

View file

@ -0,0 +1,66 @@
import Highlight from 'react-highlight'
import ToggleContent from 'Shared/ToggleContent'
import DocLink from 'Shared/DocLink/DocLink';
const AssistDoc = (props) => {
return (
<div className="p-4">
<div>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.</div>
<div className="font-bold my-2">Installation</div>
<Highlight className="js">
{`npm i @openreplay/tracker-assist`}
</Highlight>
<div className="font-bold my-2">Usage</div>
<p>Initialize the tracker then load the @openreplay/tracker-assist plugin.</p>
<div className="py-3" />
<div className="font-bold my-2">Usage</div>
<ToggleContent
label="Is SSR?"
first={
<Highlight className="js">
{`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`}
</Highlight>
}
second={
<Highlight className="js">
{`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();
}, [])
//...
}`}
</Highlight>
}
/>
<div className="font-bold my-2">Options</div>
<Highlight className="js">
{`trackerAssist({
confirmText: string;
})`}
</Highlight>
<DocLink className="mt-4" label="Install Assist" url="https://docs.openreplay.com/installation/assist" />
</div>
)
};
AssistDoc.displayName = "AssistDoc";
export default AssistDoc;

View file

@ -0,0 +1 @@
export { default } from './AssistDoc'

View file

@ -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 <MobxDoc onClose={ this.closeModal } />
case PROFILER:
return <ProfilerDoc onClose={ this.closeModal } />
case ASSIST:
return <AssistDoc onClose={ this.closeModal } />
default:
return null;
}
@ -253,7 +258,7 @@ export default class Integrations extends React.PureComponent {
{plugins && (
<div className="" >
<div className="mb-4">Use plugins to better debug your application's store, monitor queries and track performance issues.</div>
<div className="flex">
<div className="flex flex-wrap">
<IntegrationItem
title="Redux"
icon="integrations/redux"
@ -313,6 +318,14 @@ export default class Integrations extends React.PureComponent {
onClick={ () => this.showIntegrationConfig(PROFILER) }
// integrated={ sentryIntegrated }
/>
<IntegrationItem
title="Assist"
icon="integrations/assist"
url={ null }
dockLink="https://docs.openreplay.com/installation/assist"
onClick={ () => this.showIntegrationConfig(ASSIST) }
// integrated={ sentryIntegrated }
/>
</div>
</div>
)}

View file

@ -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) => (
<div className={ styles.form }>
<form onSubmit={ this.save } >
<div className={ styles.formGroup }>
@ -99,7 +101,11 @@ class ManageUsers extends React.PureComponent {
className={ styles.input }
/>
</div>
{ !account.smtp &&
<div className={cn("mb-4 p-2", styles.smtpMessage)}>
SMTP is not configured, <a className="link" href="https://docs.openreplay.com/configuration/configure-smtp" target="_blank">setup SMTP</a>
</div>
}
<div className={ styles.formGroup }>
<label className={ styles.checkbox }>
<input
@ -111,26 +117,37 @@ class ManageUsers extends React.PureComponent {
/>
<span>{ 'Admin' }</span>
</label>
<div className={ styles.adminInfo }>{ 'Can manage Projects and Users.' }</div>
<div className={ styles.adminInfo }>{ 'Can manage Projects and team members.' }</div>
</div>
</form>
<Button
onClick={ this.save }
disabled={ !member.validate() }
loading={ this.props.saving }
primary
marginRight
>
{ member.exists() ? 'Update' : 'Invite' }
</Button>
<Button
data-hidden={ !member.exists() }
onClick={ this.closeModal }
outline
>
{ 'Cancel' }
</Button>
<div className="flex items-center">
<div className="flex items-center mr-auto">
<Button
onClick={ this.save }
disabled={ !member.validate() }
loading={ this.props.saving }
primary
marginRight
>
{ member.exists() ? 'Update' : 'Invite' }
</Button>
<Button
data-hidden={ !member.exists() }
onClick={ this.closeModal }
outline
>
{ 'Cancel' }
</Button>
</div>
{ !member.joined && member.invitationLink &&
<CopyButton
content={member.invitationLink}
className="link"
btnText="Copy invite link"
/>
}
</div>
</div>
)
@ -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 }
/>
<div className={ styles.wrapper }>
@ -202,6 +219,7 @@ class ManageUsers extends React.PureComponent {
{
members.map(user => (
<UserItem
generateInviteLink={this.props.generateInviteLink}
key={ user.id }
user={ user }
adminLabel={ this.adminLabel(user) }

View file

@ -1,13 +1,43 @@
import React from 'react';
import { Icon } from 'UI';
import { Icon, CopyButton, Popup } from 'UI';
import styles from './userItem.css';
const UserItem = ({ user, adminLabel, deleteHandler, editHandler }) => (
const UserItem = ({ user, adminLabel, deleteHandler, editHandler, generateInviteLink }) => (
<div className={ styles.wrapper } id="user-row">
<Icon name="user-alt" size="16" marginRight="10" />
<div id="user-name">{ user.name || user.email }</div>
{ adminLabel && <div className={ styles.adminLabel }>{ adminLabel }</div>}
<div className={ styles.actions }>
{ user.expiredInvitation && !user.joined &&
<Popup
trigger={
<div className={ styles.button } onClick={ () => generateInviteLink(user) } id="trash">
<Icon name="link-45deg" size="16" color="red"/>
</div>
}
content={ `Generate Invitation Link` }
size="tiny"
inverted
position="top center"
/>
}
{ !user.expiredInvitation && !user.joined && user.invitationLink &&
<Popup
trigger={
<div className={ styles.button }>
<CopyButton
content={user.invitationLink}
className="link"
btnText={<Icon name="link-45deg" size="16" color="teal"/>}
/>
</div>
}
content={ `Copy Invitation Link` }
size="tiny"
inverted
position="top center"
/>
}
{ !!deleteHandler &&
<div className={ styles.button } onClick={ () => deleteHandler(user) } id="trash">
<Icon name="trash" size="16" color="teal"/>

View file

@ -34,4 +34,9 @@
.adminInfo {
font-size: 12px;
color: $gray-medium;
}
.smtpMessage {
background-color: #faf6e0;
border-radius: 3px;
}

View file

@ -35,6 +35,9 @@
padding: 5px;
cursor: pointer;
margin-left: 10px;
display: flex;
align-items: center;
justify-content: center;
&:hover {
& svg {
fill: $teal-dark;

View file

@ -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 (
<div className="flex" style={{ height: '100vh'}}>
@ -113,7 +127,7 @@ export default class ForgotPassword extends React.PureComponent {
</div>
)}
{ !requested ?
{ !resetting && !requested &&
<div className={ stl.inputWithIcon }>
<i className={ stl.inputIconUser } />
<input
@ -125,47 +139,57 @@ export default class ForgotPassword extends React.PureComponent {
onChange={ this.write }
className={ stl.input }
/>
</div>
:
<React.Fragment>
<div className={ stl.inputWithIcon } >
<i className={ stl.inputIconPassword } />
<input
autocomplete="new-password"
type="text"
placeholder="Code"
name="code"
onChange={ this.write }
className={ stl.input }
/>
</div>
</div>
}
<div className={ stl.inputWithIcon } >
<i className={ stl.inputIconPassword } />
<input
autocomplete="new-password"
type="password"
placeholder="New Password"
name="password"
onChange={ this.write }
className={ stl.input }
/>
</div>
<div className={ stl.passwordPolicy } data-hidden={ !this.shouldShouwPolicy() }>
{ PASSWORD_POLICY }
</div>
<div className={ stl.inputWithIcon } >
<i className={ stl.inputIconPassword } />
<input
autocomplete="new-password"
type="password"
placeholder="Repeat New Password"
name="passwordRepeat"
onChange={ this.write }
className={ stl.input }
/>
</div>
</React.Fragment>
{
requested && (
<div>Reset password link has been sent to your email.</div>
)
}
{
resetting && (
<React.Fragment>
{/* <div className={ stl.inputWithIcon } >
<i className={ stl.inputIconPassword } />
<input
autocomplete="new-password"
type="text"
placeholder="Code"
name="code"
onChange={ this.write }
className={ stl.input }
/>
</div> */}
<div className={ stl.inputWithIcon } >
<i className={ stl.inputIconPassword } />
<input
autocomplete="new-password"
type="password"
placeholder="New Password"
name="password"
onChange={ this.write }
className={ stl.input }
/>
</div>
<div className={ stl.passwordPolicy } data-hidden={ !this.shouldShouwPolicy() }>
{ PASSWORD_POLICY }
</div>
<div className={ stl.inputWithIcon } >
<i className={ stl.inputIconPassword } />
<input
autocomplete="new-password"
type="password"
placeholder="Repeat New Password"
name="passwordRepeat"
onChange={ this.write }
className={ stl.input }
/>
</div>
</React.Fragment>
)
}
<Message error hidden={ !dontMatch }>
@ -185,7 +209,13 @@ export default class ForgotPassword extends React.PureComponent {
</div>
</div>
<div className={ stl.formFooter }>
<Button data-hidden={ updated } type="submit" primary >{ 'Reset' }</Button>
<Button
data-hidden={ updated || requested }
type="submit" primary
disabled={ (resetting && this.isSubmitDisabled()) || (!resetting && !validEmail)}
>
{ 'Reset' }
</Button>
<div className={ stl.links }>
<Link to={ LOGIN }>

View file

@ -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 }
</button>
)
}

View file

@ -9,8 +9,12 @@ export default ({
show = true,
children = null,
empty = false,
image = null
}) => (!show ? children :
<div className={ `${ styles.wrapper } ${ size && styles[ size ] }` }>
{
image && image
}
{
icon && <div className={ empty ? styles.emptyIcon : styles.icon } />
}

View file

@ -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;

View file

@ -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:

View file

@ -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 });
}

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120"><defs><style>.cls-1{fill:#fff;}.cls-2{fill:#d7e2e2;}.cls-3{fill:#9da0a0;}.cls-4{fill:#fab29a;}.cls-5{fill:#222;}</style></defs><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><path class="cls-1" d="M114,0H6A6,6,0,0,0,0,6V114a6,6,0,0,0,6,6H114a6,6,0,0,0,6-6V6A6,6,0,0,0,114,0Z"/><path class="cls-2" d="M28,108.75H91a8,8,0,0,0,8-8v-64a8,8,0,0,0-8-8H55.67L35,12V28.81H28a8,8,0,0,0-8,8v64a8,8,0,0,0,8,8Z"/><path class="cls-3" d="M54.11,79.63H64.89A13.25,13.25,0,0,1,78.15,92.88v7.2H40.85v-7.2A13.25,13.25,0,0,1,54.11,79.63Z"/><path class="cls-4" d="M46.18,53.82H72.82V66.3a13.32,13.32,0,1,1-26.64,0Z"/><path class="cls-5" d="M76.15,55v6.93a65,65,0,0,1-22.58-4.94,14.93,14.93,0,0,1-10.72,5V55a16.67,16.67,0,0,1,33.33,0Z"/><path d="M59.67,41.83A13.55,13.55,0,0,0,46.11,55.39V58.1h2.71a2.72,2.72,0,0,1,1.92.8,2.75,2.75,0,0,1,.79,1.91V69a2.75,2.75,0,0,1-.79,1.92,2.71,2.71,0,0,1-1.92.79H46.11A2.71,2.71,0,0,1,43.39,69V55.39a16.23,16.23,0,0,1,4.77-11.5,16.26,16.26,0,0,1,23,0,16.23,16.23,0,0,1,4.77,11.5V71.66a6.78,6.78,0,0,1-6.78,6.78H63.37A2.68,2.68,0,0,1,61,79.8H58.31a2.72,2.72,0,0,1-1.92-.8,2.67,2.67,0,0,1-.79-1.91,2.71,2.71,0,0,1,2.71-2.72H61a2.7,2.7,0,0,1,1.36.37,2.76,2.76,0,0,1,1,1h5.79a4.08,4.08,0,0,0,4.07-4.07H70.51a2.67,2.67,0,0,1-1.91-.79A2.72,2.72,0,0,1,67.8,69V60.81a2.71,2.71,0,0,1,.8-1.91,2.68,2.68,0,0,1,1.91-.8h2.72V55.39a13.61,13.61,0,0,0-4-9.59,13.44,13.44,0,0,0-4.39-2.94,13.61,13.61,0,0,0-5.19-1Z"/></g></g></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" class="bi bi-link-45deg" viewBox="0 0 16 16">
<path d="M4.715 6.542 3.343 7.914a3 3 0 1 0 4.243 4.243l1.828-1.829A3 3 0 0 0 8.586 5.5L8 6.086a1.002 1.002 0 0 0-.154.199 2 2 0 0 1 .861 3.337L6.88 11.45a2 2 0 1 1-2.83-2.83l.793-.792a4.018 4.018 0 0 1-.128-1.287z"/>
<path d="M6.586 4.672A3 3 0 0 0 7.414 9.5l.775-.776a2 2 0 0 1-.896-3.346L9.12 3.55a2 2 0 1 1 2.83 2.83l-.793.792c.112.42.155.855.128 1.287l1.372-1.372a3 3 0 1 0-4.243-4.243L6.586 4.672z"/>
</svg>

After

Width:  |  Height:  |  Size: 503 B

View file

@ -9,6 +9,9 @@ export default Record({
createdAt: undefined,
admin: false,
superAdmin: false,
joined: false,
expiredInvitation: false,
invitationLink: ''
}, {
idKey: 'id',
methods: {