Roles UI (#223)
* feat(ui) - roles and permissions * feat(ui) - roles and permissions assist check * feat(ui) - roles and permissions dev tools * feat(ui) - roles and permissions logs * feat(ui) - roles and permissions logs * feat(ui) - roles and permissions cleanup
This commit is contained in:
parent
dd52556f14
commit
941c6c06fd
34 changed files with 648 additions and 93 deletions
|
|
@ -32,9 +32,11 @@ interface Props {
|
|||
calling: CallingState,
|
||||
peerConnectionStatus: ConnectionStatus,
|
||||
remoteControlEnabled: boolean,
|
||||
hasPermission: boolean,
|
||||
isEnterprise: boolean,
|
||||
}
|
||||
|
||||
function AssistActions({ toggleChatWindow, userId, calling, peerConnectionStatus, remoteControlEnabled }: Props) {
|
||||
function AssistActions({ toggleChatWindow, userId, calling, peerConnectionStatus, remoteControlEnabled, hasPermission, isEnterprise }: Props) {
|
||||
const [ incomeStream, setIncomeStream ] = useState<MediaStream | null>(null);
|
||||
const [ localStream, setLocalStream ] = useState<LocalStream | null>(null);
|
||||
const [ callObject, setCallObject ] = useState<{ end: ()=>void, toggleRemoteControl: ()=>void } | null >(null);
|
||||
|
|
@ -64,6 +66,7 @@ function AssistActions({ toggleChatWindow, userId, calling, peerConnectionStatus
|
|||
}
|
||||
|
||||
const inCall = calling !== CallingState.False;
|
||||
const cannotCall = (peerConnectionStatus !== ConnectionStatus.Connected) || (isEnterprise && !hasPermission)
|
||||
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
|
|
@ -73,8 +76,8 @@ function AssistActions({ toggleChatWindow, userId, calling, peerConnectionStatus
|
|||
className={
|
||||
cn(
|
||||
'cursor-pointer p-2 mr-2 flex items-center',
|
||||
{[stl.inCall] : inCall },
|
||||
{[stl.disabled]: peerConnectionStatus !== ConnectionStatus.Connected}
|
||||
// {[stl.inCall] : inCall },
|
||||
{[stl.disabled]: cannotCall}
|
||||
)
|
||||
}
|
||||
onClick={ inCall ? callObject?.end : call}
|
||||
|
|
@ -118,7 +121,13 @@ function AssistActions({ toggleChatWindow, userId, calling, peerConnectionStatus
|
|||
)
|
||||
}
|
||||
|
||||
const con = connect(null, { toggleChatWindow })
|
||||
const con = connect(state => {
|
||||
const permissions = state.getIn([ 'user', 'account', 'permissions' ]) || []
|
||||
return {
|
||||
hasPermission: permissions.includes('ASSIST_CALL'),
|
||||
isEnterprise: state.getIn([ 'user', 'client', 'edition' ]) === 'ee',
|
||||
}
|
||||
}, { toggleChatWindow })
|
||||
|
||||
export default con(connectPlayer(state => ({
|
||||
calling: state.calling,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { connect } from 'react-redux';
|
|||
import { NoContent, Loader } from 'UI';
|
||||
import { List, Map } from 'immutable';
|
||||
import SessionItem from 'Shared/SessionItem';
|
||||
import withPermissions from 'HOCs/withPermissions'
|
||||
|
||||
const AUTOREFRESH_INTERVAL = 1 * 60 * 1000
|
||||
|
||||
|
|
@ -60,8 +61,13 @@ function LiveSessionList(props: Props) {
|
|||
)
|
||||
}
|
||||
|
||||
export default connect(state => ({
|
||||
list: state.getIn(['sessions', 'liveSessions']),
|
||||
loading: state.getIn([ 'sessions', 'loading' ]),
|
||||
filters: state.getIn([ 'filters', 'appliedFilter' ]),
|
||||
}), { fetchList })(LiveSessionList)
|
||||
export default withPermissions(['ASSIST_LIVE'])(connect(
|
||||
(state) => ({
|
||||
list: state.getIn(['sessions', 'liveSessions']),
|
||||
loading: state.getIn([ 'sessions', 'loading' ]),
|
||||
filters: state.getIn([ 'filters', 'appliedFilter' ]),
|
||||
}),
|
||||
{
|
||||
fetchList
|
||||
}
|
||||
)(LiveSessionList));
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import styles from './client.css';
|
|||
import cn from 'classnames';
|
||||
import PreferencesMenu from './PreferencesMenu';
|
||||
import Notifications from './Notifications';
|
||||
import Roles from './Roles';
|
||||
|
||||
@connect((state) => ({
|
||||
appearance: state.getIn([ 'user', 'account', 'appearance' ]),
|
||||
|
|
@ -42,6 +43,7 @@ export default class Client extends React.PureComponent {
|
|||
<Route exact strict path={ clientRoute(CLIENT_TABS.CUSTOM_FIELDS) } component={ CustomFields } />
|
||||
<Route exact strict path={ clientRoute(CLIENT_TABS.WEBHOOKS) } component={ Webhooks } />
|
||||
<Route exact strict path={ clientRoute(CLIENT_TABS.NOTIFICATIONS) } component={ Notifications } />
|
||||
<Route exact strict path={ clientRoute(CLIENT_TABS.MANAGE_ROLES) } component={ Roles } />
|
||||
<Redirect to={ clientRoute(CLIENT_TABS.PROFILE) } />
|
||||
</Switch>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import { connect } from 'react-redux';
|
||||
import cn from 'classnames';
|
||||
import withPageTitle from 'HOCs/withPageTitle';
|
||||
import { IconButton, SlideModal, Input, Button, Loader, NoContent, Popup, CopyButton } from 'UI';
|
||||
import {
|
||||
IconButton, SlideModal, Input, Button, Loader,
|
||||
NoContent, Popup, CopyButton, Dropdown } from 'UI';
|
||||
import { init, save, edit, remove as deleteMember, fetchList, generateInviteLink } from 'Duck/member';
|
||||
import styles from './manageUsers.css';
|
||||
import UserItem from './UserItem';
|
||||
|
|
@ -19,6 +21,7 @@ const LIMIT_WARNING = 'You have reached users limit.';
|
|||
errors: state.getIn([ 'members', 'saveRequest', 'errors' ]),
|
||||
loading: state.getIn([ 'members', 'loading' ]),
|
||||
saving: state.getIn([ 'members', 'saveRequest', 'loading' ]),
|
||||
roles: state.getIn(['roles', 'list'])
|
||||
}), {
|
||||
init,
|
||||
save,
|
||||
|
|
@ -31,6 +34,7 @@ const LIMIT_WARNING = 'You have reached users limit.';
|
|||
class ManageUsers extends React.PureComponent {
|
||||
state = { showModal: false, remaining: this.props.account.limits.teamMember.remaining, invited: false }
|
||||
|
||||
// writeOption = (e, { name, value }) => this.props.edit({ [ name ]: value });
|
||||
onChange = (e, { name, value }) => this.props.edit({ [ name ]: value });
|
||||
onChangeCheckbox = ({ target: { checked, name } }) => this.props.edit({ [ name ]: checked });
|
||||
setFocus = () => this.focusElement.focus();
|
||||
|
|
@ -76,81 +80,97 @@ class ManageUsers extends React.PureComponent {
|
|||
});
|
||||
}
|
||||
|
||||
formContent = (member, account) => (
|
||||
<div className={ styles.form }>
|
||||
<form onSubmit={ this.save } >
|
||||
<div className={ styles.formGroup }>
|
||||
<label>{ 'Full Name' }</label>
|
||||
<Input
|
||||
ref={ (ref) => { this.focusElement = ref; } }
|
||||
name="name"
|
||||
value={ member.name }
|
||||
onChange={ this.onChange }
|
||||
className={ styles.input }
|
||||
id="name-field"
|
||||
/>
|
||||
</div>
|
||||
formContent = (member, account, roles) => {
|
||||
const options = roles.map(r => ({ text: r.name, value: r.roleId })).toJS();
|
||||
|
||||
<div className={ styles.formGroup }>
|
||||
<label>{ 'Email Address' }</label>
|
||||
<Input
|
||||
disabled={member.exists()}
|
||||
name="email"
|
||||
value={ member.email }
|
||||
onChange={ this.onChange }
|
||||
className={ styles.input }
|
||||
/>
|
||||
</div>
|
||||
{ !account.smtp &&
|
||||
<div className={cn("mb-4 p-2", styles.smtpMessage)}>
|
||||
SMTP is not configured. Please follow (see <a className="link" href="https://docs.openreplay.com/configuration/configure-smtp" target="_blank">here</a> how to set it up). You can still add new users, but you’d have to manually copy then send them the invitation link.
|
||||
</div>
|
||||
}
|
||||
<div className={ styles.formGroup }>
|
||||
<label className={ styles.checkbox }>
|
||||
<input
|
||||
name="admin"
|
||||
type="checkbox"
|
||||
value={ member.admin }
|
||||
checked={ !!member.admin }
|
||||
onChange={ this.onChangeCheckbox }
|
||||
disabled={member.superAdmin}
|
||||
return (
|
||||
<div className={ styles.form }>
|
||||
<form onSubmit={ this.save } >
|
||||
<div className={ styles.formGroup }>
|
||||
<label>{ 'Full Name' }</label>
|
||||
<Input
|
||||
ref={ (ref) => { this.focusElement = ref; } }
|
||||
name="name"
|
||||
value={ member.name }
|
||||
onChange={ this.onChange }
|
||||
className={ styles.input }
|
||||
id="name-field"
|
||||
/>
|
||||
<span>{ 'Admin' }</span>
|
||||
</label>
|
||||
<div className={ styles.adminInfo }>{ 'Can manage Projects and team members.' }</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<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 className={ styles.formGroup }>
|
||||
<label>{ 'Email Address' }</label>
|
||||
<Input
|
||||
disabled={member.exists()}
|
||||
name="email"
|
||||
value={ member.email }
|
||||
onChange={ this.onChange }
|
||||
className={ styles.input }
|
||||
/>
|
||||
</div>
|
||||
{ !account.smtp &&
|
||||
<div className={cn("mb-4 p-2", styles.smtpMessage)}>
|
||||
SMTP is not configured. Please follow (see <a className="link" href="https://docs.openreplay.com/configuration/configure-smtp" target="_blank">here</a> how to set it up). You can still add new users, but you’d have to manually copy then send them the invitation link.
|
||||
</div>
|
||||
}
|
||||
<div className={ styles.formGroup }>
|
||||
<label className={ styles.checkbox }>
|
||||
<input
|
||||
name="admin"
|
||||
type="checkbox"
|
||||
value={ member.admin }
|
||||
checked={ !!member.admin }
|
||||
onChange={ this.onChangeCheckbox }
|
||||
disabled={member.superAdmin}
|
||||
/>
|
||||
<span>{ 'Admin' }</span>
|
||||
</label>
|
||||
<div className={ styles.adminInfo }>{ 'Can manage Projects and team members.' }</div>
|
||||
</div>
|
||||
|
||||
<div className={ styles.formGroup }>
|
||||
<label htmlFor="role">{ 'Role' }</label>
|
||||
<Dropdown
|
||||
placeholder="Role"
|
||||
selection
|
||||
options={ options }
|
||||
name="roleId"
|
||||
value={ member.roleId }
|
||||
onChange={ this.onChange }
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<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>
|
||||
{ !member.joined && member.invitationLink &&
|
||||
<CopyButton
|
||||
content={member.invitationLink}
|
||||
className="link"
|
||||
btnText="Copy invite link"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
init = (v) => {
|
||||
this.props.init(v);
|
||||
|
|
@ -160,7 +180,7 @@ class ManageUsers extends React.PureComponent {
|
|||
|
||||
render() {
|
||||
const {
|
||||
members, member, loading, account, hideHeader = false,
|
||||
members, member, loading, account, hideHeader = false, roles
|
||||
} = this.props;
|
||||
const { showModal, remaining, invited } = this.state;
|
||||
const isAdmin = account.admin || account.superAdmin;
|
||||
|
|
@ -173,7 +193,7 @@ class ManageUsers extends React.PureComponent {
|
|||
title="Inivte People"
|
||||
size="small"
|
||||
isDisplayed={ showModal }
|
||||
content={ this.formContent(member, account) }
|
||||
content={ this.formContent(member, account, roles) }
|
||||
onClose={ this.closeModal }
|
||||
/>
|
||||
<div className={ styles.wrapper }>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import stl from './preferencesMenu.css';
|
|||
import { CLIENT_TABS, client as clientRoute } from 'App/routes';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
|
||||
function PreferencesMenu({ activeTab, appearance, history }) {
|
||||
function PreferencesMenu({ activeTab, appearance, history, isEnterprise }) {
|
||||
|
||||
const setTab = (tab) => {
|
||||
history.push(clientRoute(tab));
|
||||
|
|
@ -76,7 +76,18 @@ function PreferencesMenu({ activeTab, appearance, history }) {
|
|||
iconName="users"
|
||||
onClick={() => setTab(CLIENT_TABS.MANAGE_USERS) }
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{ isEnterprise && (
|
||||
<div className="mb-4">
|
||||
<SideMenuitem
|
||||
active={ activeTab === CLIENT_TABS.MANAGE_ROLES }
|
||||
title="Roles"
|
||||
iconName="shield-lock"
|
||||
onClick={() => setTab(CLIENT_TABS.MANAGE_ROLES) }
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mb-4">
|
||||
<SideMenuitem
|
||||
|
|
@ -92,4 +103,5 @@ function PreferencesMenu({ activeTab, appearance, history }) {
|
|||
|
||||
export default connect(state => ({
|
||||
appearance: state.getIn([ 'user', 'account', 'appearance' ]),
|
||||
isEnterprise: state.getIn([ 'user', 'client', 'edition' ]) === 'ee',
|
||||
}))(withRouter(PreferencesMenu));
|
||||
|
|
|
|||
109
frontend/app/components/Client/Roles/Roles.tsx
Normal file
109
frontend/app/components/Client/Roles/Roles.tsx
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
import React, { useState, useEffect } from 'react'
|
||||
import cn from 'classnames'
|
||||
import { Loader, IconButton, Popup, NoContent, SlideModal } from 'UI'
|
||||
import { connect } from 'react-redux'
|
||||
import stl from './roles.css'
|
||||
import RoleForm from './components/RoleForm'
|
||||
import { init, edit, fetchList, remove as deleteRole } from 'Duck/roles';
|
||||
import RoleItem from './components/RoleItem'
|
||||
import { confirm } from 'UI/Confirmation';
|
||||
|
||||
interface Props {
|
||||
loading: boolean
|
||||
init: (role?: any) => void,
|
||||
edit: (role: any) => void,
|
||||
instance: any,
|
||||
roles: any[],
|
||||
deleteRole: (id: any) => void,
|
||||
fetchList: () => Promise<void>,
|
||||
}
|
||||
|
||||
function Roles(props: Props) {
|
||||
const { loading, instance, roles, init, edit, deleteRole } = props
|
||||
const [showModal, setShowmModal] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
props.fetchList()
|
||||
}, [])
|
||||
|
||||
const closeModal = () => {
|
||||
setShowmModal(false)
|
||||
setTimeout(() => {
|
||||
init()
|
||||
}, 100)
|
||||
}
|
||||
|
||||
const editHandler = role => {
|
||||
init(role)
|
||||
setShowmModal(true)
|
||||
}
|
||||
|
||||
const deleteHandler = async (role) => {
|
||||
if (await confirm({
|
||||
header: 'Roles',
|
||||
confirmation: `Are you sure you want to remove this role?`
|
||||
})) {
|
||||
deleteRole(role.roleId)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Loader loading={ loading }>
|
||||
<SlideModal
|
||||
title={ instance.exists() ? "Edit Role" : "Add Role" }
|
||||
size="small"
|
||||
isDisplayed={showModal }
|
||||
content={ showModal && <RoleForm closeModal={closeModal}/> }
|
||||
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>
|
||||
<Popup
|
||||
trigger={
|
||||
<div>
|
||||
<IconButton
|
||||
id="add-button"
|
||||
circle
|
||||
icon="plus"
|
||||
outline
|
||||
onClick={ () => setShowmModal(true) }
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
size="tiny"
|
||||
inverted
|
||||
position="top left"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<NoContent
|
||||
title="No roles are available."
|
||||
size="small"
|
||||
show={ false }
|
||||
icon
|
||||
>
|
||||
<div className={''}>
|
||||
{roles.map(role => (
|
||||
<RoleItem
|
||||
role={role}
|
||||
editHandler={editHandler}
|
||||
deleteHandler={deleteHandler}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</NoContent>
|
||||
</div>
|
||||
</Loader>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
export default connect(state => ({
|
||||
instance: state.getIn(['roles', 'instance']) || null,
|
||||
roles: state.getIn(['roles', 'list']),
|
||||
loading: state.getIn(['roles', 'fetchRequest', 'loading']),
|
||||
}), { init, edit, fetchList, deleteRole })(Roles)
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
import React from 'react';
|
||||
import Role from 'Types/role'
|
||||
|
||||
interface Props {
|
||||
role: Role
|
||||
}
|
||||
function Permissions(props: Props) {
|
||||
return (
|
||||
<div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Permissions;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './Permissions';
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
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'
|
||||
|
||||
interface Permission {
|
||||
name: string,
|
||||
value: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
role: any,
|
||||
edit: (role: any) => void,
|
||||
save: (role: any) => Promise<void>,
|
||||
closeModal: () => void,
|
||||
saving: boolean,
|
||||
permissions: Array<Permission>[]
|
||||
}
|
||||
|
||||
const RoleForm = ({ role, closeModal, edit, save, saving, permissions }: Props) => {
|
||||
let focusElement = useRef<any>(null)
|
||||
const _save = () => {
|
||||
save(role).then(() => {
|
||||
closeModal()
|
||||
})
|
||||
}
|
||||
|
||||
const write = ({ target: { value, name } }) => edit({ [ name ]: value })
|
||||
|
||||
const onChangeOption = (e) => {
|
||||
const { permissions } = role
|
||||
const index = permissions.indexOf(e)
|
||||
const _perms = permissions.contains(e) ? permissions.remove(index) : permissions.push(e)
|
||||
edit({ permissions: _perms })
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
focusElement && focusElement.current && focusElement.current.focus()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className={ stl.form }>
|
||||
<form onSubmit={ _save } >
|
||||
<div className={ stl.formGroup }>
|
||||
<label>{ 'Name' }</label>
|
||||
<Input
|
||||
ref={ focusElement }
|
||||
name="name"
|
||||
value={ role.name }
|
||||
onChange={ write }
|
||||
className={ stl.input }
|
||||
id="name-field"
|
||||
/>
|
||||
</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>
|
||||
))}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div className="flex items-center">
|
||||
<div className="flex items-center mr-auto">
|
||||
<Button
|
||||
onClick={ _save }
|
||||
disabled={ !role.validate() }
|
||||
loading={ saving }
|
||||
primary
|
||||
marginRight
|
||||
>
|
||||
{ role.exists() ? 'Update' : 'Add' }
|
||||
</Button>
|
||||
<Button
|
||||
data-hidden={ !role.exists() }
|
||||
onClick={ closeModal }
|
||||
outline
|
||||
>
|
||||
{ 'Cancel' }
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(state => ({
|
||||
role: state.getIn(['roles', 'instance']),
|
||||
permissions: state.getIn(['roles', 'permissions']),
|
||||
}), { edit, save })(RoleForm);
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './RoleForm';
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
.form {
|
||||
padding: 0 20px;
|
||||
|
||||
& .formGroup {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
& label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
& .input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
& input[type=checkbox] {
|
||||
margin-right: 10px;
|
||||
height: 13px;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
import React from 'react';
|
||||
import { Icon } from 'UI'
|
||||
import stl from './roleItem.css'
|
||||
import cn from 'classnames'
|
||||
|
||||
interface Props {
|
||||
role: any,
|
||||
deleteHandler?: (role: any) => void,
|
||||
editHandler?: (role: any) => void,
|
||||
}
|
||||
function RoleItem({ role, deleteHandler, editHandler }: Props) {
|
||||
return (
|
||||
<div className={cn(stl.wrapper)}>
|
||||
<Icon name="user-alt" size="16" marginRight="10" />
|
||||
<span>{ role.name }</span>
|
||||
|
||||
<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>
|
||||
}
|
||||
{ !!editHandler &&
|
||||
<div className={ cn(stl.button, {[stl.disabled] : role.protected }) } onClick={ () => editHandler(role) }>
|
||||
<Icon name="edit" size="16" color="teal"/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default RoleItem;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './RoleItem'
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
.wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
border-bottom: solid thin #e6e6e6;
|
||||
padding: 10px 0px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-left: auto;
|
||||
/* opacity: 0; */
|
||||
transition: all 0.4s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
& .button {
|
||||
padding: 5px;
|
||||
cursor: pointer;
|
||||
margin-left: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
&:hover {
|
||||
& svg {
|
||||
fill: $teal-dark;
|
||||
}
|
||||
}
|
||||
&.disabled {
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
& .disabled {
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
1
frontend/app/components/Client/Roles/index.ts
Normal file
1
frontend/app/components/Client/Roles/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default } from './Roles';
|
||||
13
frontend/app/components/Client/Roles/roles.css
Normal file
13
frontend/app/components/Client/Roles/roles.css
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
.wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
.tabHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 25px;
|
||||
|
||||
& .tabTitle {
|
||||
margin: 0 15px 0 0;
|
||||
font-weight: 400 !important;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import { connect } from 'react-redux';
|
||||
import cn from 'classnames';
|
||||
import withPageTitle from 'HOCs/withPageTitle';
|
||||
import withPermissions from 'HOCs/withPermissions'
|
||||
import { setPeriod, setPlatform, fetchMetadataOptions } from 'Duck/dashboard';
|
||||
import { NoContent } from 'UI';
|
||||
import { WIDGET_KEYS } from 'Types/dashboard';
|
||||
|
|
@ -103,6 +104,7 @@ function isInViewport(el) {
|
|||
);
|
||||
}
|
||||
|
||||
@withPermissions(['METRICS'], 'page-margin container-90')
|
||||
@connect(state => ({
|
||||
period: state.getIn([ 'dashboard', 'period' ]),
|
||||
comparing: state.getIn([ 'dashboard', 'comparing' ]),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { connect } from 'react-redux';
|
||||
import withSiteIdRouter from 'HOCs/withSiteIdRouter';
|
||||
import withPermissions from 'HOCs/withPermissions'
|
||||
import { UNRESOLVED, RESOLVED, IGNORED } from "Types/errorInfo";
|
||||
import { getRE } from 'App/utils';
|
||||
import { fetchBookmarks } from "Duck/errors";
|
||||
|
|
@ -33,6 +34,7 @@ function getStatusLabel(status) {
|
|||
}
|
||||
}
|
||||
|
||||
@withPermissions(['ERRORS'], 'page-margin container-90')
|
||||
@withSiteIdRouter
|
||||
@connect(state => ({
|
||||
list: state.getIn([ "errors", "list" ]),
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ function FunnelDropdown(props) {
|
|||
|
||||
const writeOption = (e, { name, value }) => {
|
||||
const { siteId, history } = props;
|
||||
console.log(value)
|
||||
history.push(withSiteId(funnelRoute(parseInt(value)), siteId));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -97,12 +97,17 @@ function getStorageName(type) {
|
|||
showExceptions: state.exceptionsList.length > 0,
|
||||
showLongtasks: state.longtasksList.length > 0,
|
||||
}))
|
||||
@connect((state, props) => ({
|
||||
fullscreen: state.getIn([ 'components', 'player', 'fullscreen' ]),
|
||||
bottomBlock: state.getIn([ 'components', 'player', 'bottomBlock' ]),
|
||||
showStorage: props.showStorage || !state.getIn(['components', 'player', 'hiddenHints', 'storage']),
|
||||
showStack: props.showStack || !state.getIn(['components', 'player', 'hiddenHints', 'stack']),
|
||||
}), {
|
||||
@connect((state, props) => {
|
||||
const permissions = state.getIn([ 'user', 'account', 'permissions' ]) || [];
|
||||
const isEnterprise = state.getIn([ 'user', 'client', 'edition' ]) === 'ee';
|
||||
return {
|
||||
disabled: props.disabled || (isEnterprise && !permissions.includes('DEV_TOOLS')),
|
||||
fullscreen: state.getIn([ 'components', 'player', 'fullscreen' ]),
|
||||
bottomBlock: state.getIn([ 'components', 'player', 'bottomBlock' ]),
|
||||
showStorage: props.showStorage || !state.getIn(['components', 'player', 'hiddenHints', 'storage']),
|
||||
showStack: props.showStack || !state.getIn(['components', 'player', 'hiddenHints', 'stack']),
|
||||
}
|
||||
}, {
|
||||
fullscreenOn,
|
||||
fullscreenOff,
|
||||
toggleBottomBlock,
|
||||
|
|
|
|||
15
frontend/app/components/hocs/withPermissions.js
Normal file
15
frontend/app/components/hocs/withPermissions.js
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { NoPermission } from 'UI';
|
||||
|
||||
export default (requiredPermissions, className) => BaseComponent =>
|
||||
@connect((state, props) => ({
|
||||
permissions: state.getIn([ 'user', 'account', 'permissions' ]),
|
||||
isEnterprise: state.getIn([ 'user', 'client', 'edition' ]) === 'ee',
|
||||
}))
|
||||
class extends React.PureComponent {
|
||||
render() {
|
||||
const hasPermission = this.props.permissions.some(permission => requiredPermissions.includes(permission));
|
||||
|
||||
return !this.props.isEnterprise || hasPermission ? <BaseComponent {...this.props} /> : <div className={className}><NoPermission /></div>
|
||||
}
|
||||
}
|
||||
15
frontend/app/components/ui/NoPermission/NoPermission.tsx
Normal file
15
frontend/app/components/ui/NoPermission/NoPermission.tsx
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import React from 'react';
|
||||
import stl from './noPermission.css'
|
||||
import { Icon } from 'UI';
|
||||
|
||||
function NoPermission(props) {
|
||||
return (
|
||||
<div className={stl.wrapper}>
|
||||
<Icon name="shield-lock" size="50" className="py-16"/>
|
||||
<div className={ stl.title }>Not allowed</div>
|
||||
You don’t have the necessary permissions to access this feature. Please check with your admin.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default NoPermission;
|
||||
1
frontend/app/components/ui/NoPermission/index.ts
Normal file
1
frontend/app/components/ui/NoPermission/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default } from './NoPermission';
|
||||
59
frontend/app/components/ui/NoPermission/noPermission.css
Normal file
59
frontend/app/components/ui/NoPermission/noPermission.css
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
.wrapper {
|
||||
margin: auto;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
min-height: 100px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
color: $gray-medium;
|
||||
font-weight: 300;
|
||||
transition: all 0.2s;
|
||||
padding-top: 40px;
|
||||
|
||||
&.small {
|
||||
& .title {
|
||||
font-size: 20px !important;
|
||||
}
|
||||
|
||||
& .subtext {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 32px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.subtext {
|
||||
font-size: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
|
||||
.icon {
|
||||
display: block;
|
||||
margin: auto;
|
||||
background-image: svg-load(no-results.svg, fill=#CCC);
|
||||
background-repeat: no-repeat;
|
||||
background-size: contain;
|
||||
background-position: center center;
|
||||
width: 166px;
|
||||
height: 166px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.emptyIcon {
|
||||
display: block;
|
||||
margin: auto;
|
||||
background-image: svg-load(empty-state.svg, fill=#CCC);
|
||||
background-repeat: no-repeat;
|
||||
background-size: contain;
|
||||
background-position: center center;
|
||||
width: 166px;
|
||||
height: 166px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
|
@ -52,5 +52,6 @@ export { default as QuestionMarkHint } from './QuestionMarkHint';
|
|||
export { default as TimelinePointer } from './TimelinePointer';
|
||||
export { default as CopyButton } from './CopyButton';
|
||||
export { default as HighlightCode } from './HighlightCode';
|
||||
export { default as NoPermission } from './NoPermission';
|
||||
|
||||
export { Input, Modal, Form, Message, Card } from 'semantic-ui-react';
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ import announcements from './announcements';
|
|||
import errors from './errors';
|
||||
import funnels from './funnels';
|
||||
import config from './config';
|
||||
import roles from './roles';
|
||||
|
||||
export default combineReducers({
|
||||
jwt,
|
||||
|
|
@ -66,6 +67,7 @@ export default combineReducers({
|
|||
errors,
|
||||
funnels,
|
||||
config,
|
||||
roles,
|
||||
...integrations,
|
||||
...sources,
|
||||
});
|
||||
|
|
|
|||
32
frontend/app/duck/roles.js
Normal file
32
frontend/app/duck/roles.js
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import { List, Map } from 'immutable';
|
||||
import Role from 'Types/role';
|
||||
import crudDuckGenerator from './tools/crudDuck';
|
||||
import { reduceDucks } from 'Duck/tools';
|
||||
|
||||
const crudDuck = crudDuckGenerator('client/role', Role, { idKey: 'roleId' });
|
||||
export const { fetchList, init, edit, remove, } = crudDuck.actions;
|
||||
|
||||
const initialState = Map({
|
||||
list: List(),
|
||||
permissions: List([
|
||||
{ name: 'Session Replay', value: 'SESSION_REPLAY' },
|
||||
{ name: 'Develoepr Tools', value: 'DEV_TOOLS' },
|
||||
{ name: 'Errors', value: 'ERRORS' },
|
||||
{ name: 'Metrics', value: 'METRICS' },
|
||||
{ name: 'Assist Live', value: 'ASSIST_LIVE' },
|
||||
{ name: 'Assist Call', value: 'ASSIST_CALL' },
|
||||
])
|
||||
});
|
||||
|
||||
const reducer = (state = initialState, action = {}) => {
|
||||
return state;
|
||||
};
|
||||
|
||||
export function save(instance) {
|
||||
return {
|
||||
types: crudDuck.actionTypes.SAVE.toArray(),
|
||||
call: client => instance.roleId ? client.post(`/client/roles/${ instance.roleId }`, instance.toData()) : client.put(`/client/roles`, instance.toData()),
|
||||
};
|
||||
}
|
||||
|
||||
export default reduceDucks(crudDuck, { initialState, reducer }).reducer;
|
||||
|
|
@ -60,6 +60,7 @@ export const CLIENT_TABS = {
|
|||
INTEGRATIONS: 'integrations',
|
||||
PROFILE: 'account',
|
||||
MANAGE_USERS: 'manage-users',
|
||||
MANAGE_ROLES: 'manage-roles',
|
||||
SITES: 'projects',
|
||||
CUSTOM_FIELDS: 'metadata',
|
||||
WEBHOOKS: 'webhooks',
|
||||
|
|
|
|||
4
frontend/app/svg/icons/shield-lock.svg
Normal file
4
frontend/app/svg/icons/shield-lock.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" class="bi bi-shield-lock" viewBox="0 0 16 16">
|
||||
<path d="M5.338 1.59a61.44 61.44 0 0 0-2.837.856.481.481 0 0 0-.328.39c-.554 4.157.726 7.19 2.253 9.188a10.725 10.725 0 0 0 2.287 2.233c.346.244.652.42.893.533.12.057.218.095.293.118a.55.55 0 0 0 .101.025.615.615 0 0 0 .1-.025c.076-.023.174-.061.294-.118.24-.113.547-.29.893-.533a10.726 10.726 0 0 0 2.287-2.233c1.527-1.997 2.807-5.031 2.253-9.188a.48.48 0 0 0-.328-.39c-.651-.213-1.75-.56-2.837-.855C9.552 1.29 8.531 1.067 8 1.067c-.53 0-1.552.223-2.662.524zM5.072.56C6.157.265 7.31 0 8 0s1.843.265 2.928.56c1.11.3 2.229.655 2.887.87a1.54 1.54 0 0 1 1.044 1.262c.596 4.477-.787 7.795-2.465 9.99a11.775 11.775 0 0 1-2.517 2.453 7.159 7.159 0 0 1-1.048.625c-.28.132-.581.24-.829.24s-.548-.108-.829-.24a7.158 7.158 0 0 1-1.048-.625 11.777 11.777 0 0 1-2.517-2.453C1.928 10.487.545 7.169 1.141 2.692A1.54 1.54 0 0 1 2.185 1.43 62.456 62.456 0 0 1 5.072.56z"/>
|
||||
<path d="M9.5 6.5a1.5 1.5 0 0 1-1 1.415l.385 1.99a.5.5 0 0 1-.491.595h-.788a.5.5 0 0 1-.49-.595l.384-1.99a1.5 1.5 0 1 1 2-1.415z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
|
|
@ -13,6 +13,7 @@ export default Member.extend({
|
|||
smtp: false,
|
||||
license: '',
|
||||
expirationDate: undefined,
|
||||
permissions: [],
|
||||
}, {
|
||||
fromJS: ({ current = {}, ...account})=> ({
|
||||
...account,
|
||||
|
|
|
|||
|
|
@ -10,7 +10,8 @@ export default Record({
|
|||
tenantId: undefined,
|
||||
name: undefined,
|
||||
sites: List(),
|
||||
optOut: true
|
||||
optOut: true,
|
||||
edition: '',
|
||||
}, {
|
||||
fromJS: ({
|
||||
projects,
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@ export default Record({
|
|||
superAdmin: false,
|
||||
joined: false,
|
||||
expiredInvitation: false,
|
||||
invitationLink: ''
|
||||
roleId: undefined,
|
||||
invitationLink: '',
|
||||
}, {
|
||||
idKey: 'id',
|
||||
methods: {
|
||||
|
|
|
|||
30
frontend/app/types/role.js
Normal file
30
frontend/app/types/role.js
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import Record from 'Types/Record';
|
||||
import { validateName } from 'App/validate';
|
||||
import { List } from 'immutable';
|
||||
|
||||
export default Record({
|
||||
roleId: undefined,
|
||||
name: '',
|
||||
permissions: List(),
|
||||
protected: false,
|
||||
description: ''
|
||||
}, {
|
||||
idKey: 'roleId',
|
||||
methods: {
|
||||
validate() {
|
||||
return validateName(this.name, { diacritics: true });
|
||||
},
|
||||
toData() {
|
||||
const js = this.toJS();
|
||||
delete js.key;
|
||||
delete js.protected;
|
||||
return js;
|
||||
},
|
||||
},
|
||||
fromJS({ permissions, ...rest }) {
|
||||
return {
|
||||
...rest,
|
||||
permissions: List(permissions)
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
@ -19,8 +19,11 @@
|
|||
"Types": ["./app/types" ],
|
||||
"Types/*": ["./app/types/*"], // Sublime hack
|
||||
"UI": ["./app/components/ui"],
|
||||
"UI/*": ["./app/components/ui/*"],
|
||||
"Duck": ["./app/duck"],
|
||||
"Duck/*": ["./app/duck/*"],
|
||||
"HOCs": ["./app/components/hocs"],
|
||||
"HOCs/*": ["./app/components/hocs/*"],
|
||||
"Shared": ["./app/components/shared"],
|
||||
"Shared/*": ["./app/components/shared/*"],
|
||||
"Player": ["./app/player"],
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue