feat(ui): changes Roles page to show users and effective RW per DB

pull/5923/head
Pavel Zavora 2022-06-01 09:03:34 +02:00
parent 3955daab17
commit 601fff5b40
3 changed files with 226 additions and 163 deletions

View File

@ -1,115 +1,81 @@
import React, {useCallback, useMemo} from 'react'
import React from 'react'
import RoleRowEdit from 'src/admin/components/RoleRowEdit'
import MultiSelectDropdown from 'src/shared/components/MultiSelectDropdown'
import ConfirmButton from 'src/shared/components/ConfirmButton'
import {ROLES_TABLE} from 'src/admin/constants/tableSizing'
import {UserPermission, UserRole, User} from 'src/types/influxAdmin'
import classnames from 'classnames'
import {UserRole} from 'src/types/influxAdmin'
import {Link} from 'react-router'
import {PERMISSIONS} from 'src/shared/constants'
interface Props {
role: UserRole
allUsers: User[]
allPermissions: string[]
allUsers: any[]
page: string
perDBPermissions: Array<Record<string, boolean>>
showUsers: boolean
onCancel: (role: UserRole) => void
onEdit: (role: UserRole, updates: Partial<UserRole>) => void
onSave: (role: UserRole) => Promise<void>
onDelete: (role: UserRole) => Promise<void>
onUpdateRoleUsers: (role: UserRole, users: User[]) => void
onUpdateRolePermissions: (
role: UserRole,
permissions: UserPermission[]
) => void
}
const RoleRow = ({
role: {name: roleName, permissions, users = [], isEditing},
role,
allUsers,
allPermissions,
page,
perDBPermissions,
showUsers,
onEdit,
onSave,
onCancel,
onDelete,
onUpdateRoleUsers,
onUpdateRolePermissions,
}: Props) => {
const handleUpdateUsers = useCallback(
(usrs: User[]) => onUpdateRoleUsers(role, usrs),
[role]
)
const handleUpdatePermissions = useCallback(
(allowed: Array<{name: string}>) =>
onUpdateRolePermissions(role, [
{scope: 'all', allowed: allowed.map(({name}) => name)},
]),
[role]
)
const selectedPerms = useMemo(() => {
const allPerm = permissions?.find(x => x.scope === 'all')
return allPerm?.allowed.map((name: string) => ({name})) || []
}, [permissions])
if (isEditing) {
if (role.isEditing) {
return (
<RoleRowEdit
role={role}
onEdit={onEdit}
onSave={onSave}
onCancel={onCancel}
colSpan={3}
colSpan={1 + +showUsers + perDBPermissions.length}
/>
)
}
const wrappedDelete = () => {
onDelete(role)
}
return (
<tr data-test={`role-${roleName}--row`}>
<td style={{width: `${ROLES_TABLE.colName}px`}}>{roleName}</td>
<td>
{allPermissions && allPermissions.length ? (
<MultiSelectDropdown
items={allPermissions.map(name => ({name}))}
selectedItems={selectedPerms}
label={selectedPerms.length ? '' : 'Select Permissions'}
onApply={handleUpdatePermissions}
buttonSize="btn-xs"
buttonColor="btn-primary"
customClass={classnames(`dropdown-${ROLES_TABLE.colPermissions}`, {
'admin-table--multi-select-empty': !permissions.length,
})}
resetStateOnReceiveProps={false}
/>
) : null}
</td>
<td>
{allUsers && allUsers.length ? (
<MultiSelectDropdown
items={allUsers}
selectedItems={users}
label={users.length ? '' : 'Select Users'}
onApply={handleUpdateUsers}
buttonSize="btn-xs"
buttonColor="btn-primary"
customClass={classnames(`dropdown-${ROLES_TABLE.colUsers}`, {
'admin-table--multi-select-empty': !users.length,
})}
resetStateOnReceiveProps={false}
/>
) : null}
</td>
<td className="text-right">
<ConfirmButton
customClass="table--show-on-row-hover"
size="btn-xs"
type="btn-danger"
text="Delete Role"
confirmAction={wrappedDelete}
/>
<tr>
<td style={{width: `${ROLES_TABLE.colName}px`}}>
<Link to={page}>{role.name}</Link>
</td>
{showUsers && (
<td
className="admin-table--left-offset"
title={!allUsers.length ? 'No users are defined' : ''}
>
{role.users.map((user, i) => (
<span key={i} className="user-value granted">
{user.name}
</span>
))}
</td>
)}
{perDBPermissions.map((perms, i) => (
<td className="admin-table__dbperm" key={i}>
<span
className={`permission-value ${
perms.ReadData ? 'granted' : 'denied'
}`}
title={PERMISSIONS.ReadData.description}
>
{PERMISSIONS.ReadData.displayName}
</span>
<span
className={`permission-value ${
perms.WriteData ? 'granted' : 'denied'
}`}
title={PERMISSIONS.WriteData.description}
>
{PERMISSIONS.WriteData.displayName}
</span>
</td>
))}
</tr>
)
}

View File

@ -1,16 +1,9 @@
export const USERS_TABLE = {
colUsername: 240,
colAdministrator: 70,
colPassword: 186,
colRoles: 190,
colPermissions: 190,
colDelete: 110,
}
export const ROLES_TABLE = {
colName: 280,
colUsers: 200,
colPermissions: 200,
colDelete: 110,
colName: 240,
}
export const QUERIES_TABLE = {
colDatabase: 160,

View File

@ -1,17 +1,14 @@
import React, {useMemo} from 'react'
import React, {useMemo, useState} from 'react'
import {connect, ResolveThunks} from 'react-redux'
import {withSource} from 'src/CheckSources'
import {Source} from 'src/types'
import {UserPermission, UserRole, User} from 'src/types/influxAdmin'
import {UserRole, User, Database} from 'src/types/influxAdmin'
import {notify as notifyAction} from 'src/shared/actions/notifications'
import {
addRole as addRoleActionCreator,
editRole as editRoleActionCreator,
deleteRole as deleteRoleAction,
createRoleAsync,
deleteRoleAsync,
updateRoleUsersAsync,
updateRolePermissionsAsync,
filterRoles as filterRolesAction,
} from 'src/admin/actions/influxdb'
import {notifyRoleNameInvalid} from 'src/shared/copy/notifications'
@ -19,32 +16,33 @@ import AdminInfluxDBTabbedPage, {
hasRoleManagement,
isConnectedToLDAP,
} from './AdminInfluxDBTabbedPage'
import FilterBar from 'src/admin/components/FilterBar'
import FancyScrollbar from 'src/shared/components/FancyScrollbar'
import EmptyRow from 'src/admin/components/EmptyRow'
import RoleRow from 'src/admin/components/RoleRow'
import {useCallback} from 'react'
import allOrParticularSelection from './util/allOrParticularSelection'
import computeEffectiveDBPermissions from './util/computeEffectiveDBPermissions'
import useDebounce from 'src/utils/useDebounce'
import useChangeEffect from 'src/utils/useChangeEffect'
import {ComponentSize, MultiSelectDropdown, SlideToggle} from 'src/reusable_ui'
const isValidRole = (role: UserRole): boolean => {
const minLen = 3
return role.name.length >= minLen
}
const mapStateToProps = ({adminInfluxDB: {users, roles, permissions}}) => ({
const mapStateToProps = ({adminInfluxDB: {databases, users, roles}}) => ({
databases,
users,
roles,
permissions,
})
const mapDispatchToProps = {
addRole: addRoleActionCreator,
removeRole: deleteRoleAction,
editRole: editRoleActionCreator,
createRole: createRoleAsync,
deleteRole: deleteRoleAsync,
filterRoles: filterRolesAction,
updateRoleUsers: updateRoleUsersAsync,
updateRolePermissions: updateRolePermissionsAsync,
createRole: createRoleAsync,
removeRole: deleteRoleAction,
addRole: addRoleActionCreator,
editRole: editRoleActionCreator,
notify: notifyAction,
}
@ -52,9 +50,9 @@ interface OwnProps {
source: Source
}
interface ConnectedProps {
databases: Database[]
users: User[]
roles: UserRole[]
permissions: UserPermission[]
}
type ReduxDispatchProps = ResolveThunks<typeof mapDispatchToProps>
@ -63,23 +61,16 @@ type Props = OwnProps & ConnectedProps & ReduxDispatchProps
const RolesPage = ({
source,
permissions,
users,
roles,
databases,
addRole,
filterRoles,
editRole,
removeRole,
deleteRole,
updateRoleUsers,
updateRolePermissions,
createRole,
notify,
}: Props) => {
const allPermissions = useMemo(() => {
const globalPermissions = permissions.find(p => p.scope === 'all')
return globalPermissions ? globalPermissions.allowed : []
}, [permissions])
const handleSaveRole = useCallback(
async (role: UserRole) => {
if (!isValidRole(role)) {
@ -94,65 +85,155 @@ const RolesPage = ({
)
const isEditing = useMemo(() => roles.some(r => r.isEditing), [roles])
if (!hasRoleManagement(source)) {
return (
<AdminInfluxDBTabbedPage activeTab="roles" source={source}>
<div className="container-fluid">
Roles management is not available for the currently selected InfluxDB
Connection.
</div>
</AdminInfluxDBTabbedPage>
)
}
if (isConnectedToLDAP(source)) {
return (
<AdminInfluxDBTabbedPage activeTab="roles" source={source}>
<div className="container-fluid">
Users are managed via LDAP, roles management is not available.
</div>
</AdminInfluxDBTabbedPage>
)
}
const rolesPage = useMemo(
() => `/sources/${source.id}/admin-influxdb/roles`,
[source]
)
// filter databases
const [selectedDBs, setSelectedDBs] = useState<string[]>(['*'])
const visibleDBNames = useMemo<string[]>(() => {
if (selectedDBs.includes('*')) {
return databases.map(db => db.name)
}
return selectedDBs
}, [databases, selectedDBs])
const changeSelectedDBs = useCallback(
(newDBs: string[]) =>
setSelectedDBs((oldDBs: string[]) => {
return allOrParticularSelection(oldDBs, newDBs)
}),
[setSelectedDBs]
)
// effective permissions
const visibleRoles = useMemo(() => roles.filter(x => !x.hidden), [roles])
const perDBPermissions = useMemo(
() => computeEffectiveDBPermissions(visibleRoles, visibleDBNames),
[visibleDBNames, visibleRoles]
)
// filter users
const [filterText, setFilterText] = useState('')
const changeFilterText = useCallback(e => setFilterText(e.target.value), [
setFilterText,
])
const debouncedFilterText = useDebounce(filterText, 200)
useChangeEffect(() => {
filterRoles(debouncedFilterText)
}, [debouncedFilterText])
// hide users
const [showUsers, setShowUsers] = useState(true)
const changeHideUsers = useCallback(() => setShowUsers(!showUsers), [
showUsers,
setShowUsers,
])
return (
<AdminInfluxDBTabbedPage activeTab="roles" source={source}>
<div className="panel panel-solid influxdb-admin">
<FilterBar
type="roles"
onFilter={filterRoles}
isEditing={isEditing}
onClickCreate={addRole}
/>
<div className="panel-heading">
<div className="search-widget">
<input
type="text"
className="form-control input-sm"
placeholder={`Filter Roles...`}
value={filterText}
onChange={changeFilterText}
/>
<span className="icon search" />
</div>
<div className="db-selector">
<MultiSelectDropdown
onChange={changeSelectedDBs}
selectedIDs={selectedDBs}
emptyText="<no database>"
>
{databases.reduce(
(acc, db) => {
acc.push(
<MultiSelectDropdown.Item
key={db.name}
id={db.name}
value={{id: db.name}}
>
{db.name}
</MultiSelectDropdown.Item>
)
return acc
},
[
<MultiSelectDropdown.Item id="*" key="*" value={{id: '*'}}>
All Databases
</MultiSelectDropdown.Item>,
<MultiSelectDropdown.Divider id="" key="" />,
]
)}
</MultiSelectDropdown>
</div>
<div className="hide-roles-toggle">
<SlideToggle
active={showUsers}
onChange={changeHideUsers}
size={ComponentSize.ExtraSmall}
/>
Show Users
</div>
<div className="panel-heading--right">
<button
className="btn btn-sm btn-primary"
disabled={isEditing}
onClick={addRole}
>
<span className="icon plus" /> Create Role
</button>
</div>
</div>
<div className="panel-body">
<FancyScrollbar>
<table className="table v-center admin-table table-highlight">
<table className="table v-center admin-table table-highlight admin-table--compact">
<thead>
<tr>
<th>Name</th>
<th className="admin-table--left-offset">Permissions</th>
<th className="admin-table--left-offset">Users</th>
<th />
<th>Role</th>
{showUsers && (
<th className="admin-table--left-offset">Users</th>
)}
{visibleRoles.length && visibleDBNames.length
? visibleDBNames.map(name => (
<th
className="admin-table__dbheader"
title={`effective permissions for db: ${name}`}
key={name}
>
{name}
</th>
))
: null}
</tr>
</thead>
<tbody>
{roles.length ? (
roles
.filter(r => !r.hidden)
.map(role => (
<RoleRow
key={role.links.self}
allUsers={users}
allPermissions={allPermissions}
role={role}
onEdit={editRole}
onSave={handleSaveRole}
onCancel={removeRole}
onDelete={deleteRole}
onUpdateRoleUsers={updateRoleUsers}
onUpdateRolePermissions={updateRolePermissions}
/>
))
{visibleRoles.length ? (
visibleRoles.map((role, roleIndex) => (
<RoleRow
key={role.name}
role={role}
page={`${rolesPage}/${encodeURIComponent(
role.name || ''
)}`}
perDBPermissions={perDBPermissions[roleIndex]}
allUsers={users}
showUsers={showUsers}
onEdit={editRole}
onSave={handleSaveRole}
onCancel={removeRole}
/>
))
) : (
<EmptyRow entities="Roles" colSpan={4} />
<EmptyRow
entities="Roles"
colSpan={1 + +showUsers}
filtered={!!filterText}
/>
)}
</tbody>
</table>
@ -163,6 +244,29 @@ const RolesPage = ({
)
}
const RolesPageAvailable = (props: Props) => {
if (!hasRoleManagement(props.source)) {
return (
<AdminInfluxDBTabbedPage activeTab="roles" source={props.source}>
<div className="container-fluid">
Roles management is not available for the currently selected InfluxDB
Connection.
</div>
</AdminInfluxDBTabbedPage>
)
}
if (isConnectedToLDAP(props.source)) {
return (
<AdminInfluxDBTabbedPage activeTab="roles" source={props.source}>
<div className="container-fluid">
Users are managed via LDAP, roles management is not available.
</div>
</AdminInfluxDBTabbedPage>
)
}
return <RolesPage {...props} />
}
export default withSource(
connect(mapStateToProps, mapDispatchToProps)(RolesPage)
connect(mapStateToProps, mapDispatchToProps)(RolesPageAvailable)
)