* 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:
Shekar Siri 2021-11-27 03:27:33 +05:30 committed by GitHub
parent dd52556f14
commit 941c6c06fd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 648 additions and 93 deletions

View file

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

View file

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

View file

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

View file

@ -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 youd 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 youd 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 }>

View file

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

View 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)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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;
}
}

View file

@ -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' ]),

View file

@ -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" ]),

View file

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

View file

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

View 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>
}
}

View 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 dont have the necessary permissions to access this feature. Please check with your admin.
</div>
);
}
export default NoPermission;

View file

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

View 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;
}

View file

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

View file

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

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

View file

@ -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',

View 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

View file

@ -13,6 +13,7 @@ export default Member.extend({
smtp: false,
license: '',
expirationDate: undefined,
permissions: [],
}, {
fromJS: ({ current = {}, ...account})=> ({
...account,

View file

@ -10,7 +10,8 @@ export default Record({
tenantId: undefined,
name: undefined,
sites: List(),
optOut: true
optOut: true,
edition: '',
}, {
fromJS: ({
projects,

View file

@ -11,7 +11,8 @@ export default Record({
superAdmin: false,
joined: false,
expiredInvitation: false,
invitationLink: ''
roleId: undefined,
invitationLink: '',
}, {
idKey: 'id',
methods: {

View 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)
}
},
});

View file

@ -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"],