feat(ui): changes Roles page to show users and effective RW per DB
parent
3955daab17
commit
601fff5b40
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
|
|
Loading…
Reference in New Issue