feat(ui): add RolePage

pull/5923/head
Pavel Zavora 2022-06-01 06:23:17 +02:00
parent fbb92b5dbc
commit ca7af0fa11
3 changed files with 411 additions and 2 deletions

View File

@ -0,0 +1,407 @@
import React, {useState} from 'react'
import {connect, ResolveThunks} from 'react-redux'
import {withSource} from 'src/CheckSources'
import {Source} from 'src/types'
import {Database, User, UserPermission, UserRole} from 'src/types/influxAdmin'
import {hasRoleManagement, isConnectedToLDAP} from './AdminInfluxDBTabbedPage'
import {withRouter, WithRouterProps} from 'react-router'
import {useMemo} from 'react'
import ConfirmButton from 'src/shared/components/ConfirmButton'
import {
deleteRoleAsync,
updateRolePermissionsAsync,
updateRoleUsersAsync,
} from 'src/admin/actions/influxdb'
import {Button, ComponentColor, ComponentStatus, Page} from 'src/reusable_ui'
import FancyScrollbar from 'src/shared/components/FancyScrollbar'
import {useEffect} from 'react'
import {useCallback} from 'react'
import {PERMISSIONS} from 'src/shared/constants'
import {
computeUserPermissions,
computeUserPermissionsChange,
toUserPermissions,
} from './util/userPermissions'
const FAKE_ROLE: UserRole = {
name: '',
permissions: [],
users: [],
links: {self: ''},
}
const mapStateToProps = ({
adminInfluxDB: {databases, users, roles, permissions},
}) => ({
databases,
users,
roles,
permissions,
})
interface RouterParams {
sourceID: string
roleName: string
}
const mapDispatchToProps = {
deleteRoleAsync,
updateRolePermissionsAsync,
updateRoleUsersAsync,
}
interface OwnProps {
source: Source
}
interface ConnectedProps {
users: User[]
roles: UserRole[]
permissions: UserPermission[]
databases: Database[]
}
type ReduxDispatchProps = ResolveThunks<typeof mapDispatchToProps>
type Props = WithRouterProps<RouterParams> &
OwnProps &
ConnectedProps &
ReduxDispatchProps
const UserPage = ({
users,
databases,
permissions: serverPermissions,
roles,
source,
router,
params: {roleName, sourceID},
deleteRoleAsync: deleteAsync,
updateRolePermissionsAsync: updatePermissionsAsync,
updateRoleUsersAsync: updateUsersAsync,
}: Props) => {
if (!hasRoleManagement(source)) {
return (
<div className="container-fluid">
Role management is not available for the currently selected InfluxDB
Connection.
</div>
)
}
if (isConnectedToLDAP(source)) {
return (
<div className="container-fluid">
Users are managed via LDAP, roles management is not available.
</div>
)
}
const [running, setRunning] = useState(false)
const [role, deleteRole] = useMemo(() => {
const r = roles.find(x => x.name === roleName) || FAKE_ROLE
return [
r,
async () => {
setRunning(true)
try {
await deleteAsync(r)
router.push(`/sources/${sourceID}/admin-influxdb/roles`)
} finally {
setRunning(false)
}
},
]
}, [source, roles, roleName])
// permissions
const [
dbPermisssions,
clusterPermissions,
roleDBPermissions,
] = useMemo(
() => [
serverPermissions.find(x => x.scope === 'database')?.allowed || [],
serverPermissions.find(x => x.scope === 'all')?.allowed || [],
computeUserPermissions(role, true),
],
[serverPermissions, role]
)
const [changedPermissions, setChangedPermissions] = useState<
Record<string, Record<string, boolean | undefined>>
>({})
useEffect(() => {
setChangedPermissions({})
}, [role])
const onPermissionChange = useMemo(
() => (e: React.MouseEvent<HTMLElement>) => {
const db = (e.target as HTMLElement).dataset.db
const perm = (e.target as HTMLElement).dataset.perm
setChangedPermissions(
computeUserPermissionsChange(
db,
perm,
roleDBPermissions,
changedPermissions
)
)
},
[roleDBPermissions, changedPermissions, setChangedPermissions]
)
const permissionsChanged = !!Object.keys(changedPermissions).length
const changePermissions = useMemo(
() => async () => {
if (Object.entries(changedPermissions).length === 0) {
return
}
setRunning(true)
try {
// append to existing all-scoped permissions in OSS, they manage administrator status
const permissions = toUserPermissions(
roleDBPermissions,
changedPermissions
)
await updatePermissionsAsync(role, permissions)
} finally {
setRunning(false)
}
},
[role, changedPermissions, roleDBPermissions]
)
// users
const [allUserNames, usersRecord] = useMemo(() => {
const uNames = users.map(r => r.name).sort()
const ruRecord = role.users.reduce<Record<string, boolean>>((acc, r) => {
acc[r.name] = true
return acc
}, {})
return [uNames, ruRecord]
}, [role, users])
const [changedUsersRecord, setChangedUsersRecord] = useState<
Record<string, boolean>
>({})
useEffect(() => setChangedUsersRecord({}), [role])
const usersChanged = useMemo(() => !!Object.keys(changedUsersRecord).length, [
changedUsersRecord,
])
const onUserChange = useMemo(
() => (e: React.MouseEvent<HTMLElement>) => {
const user = (e.target as HTMLElement).dataset.user
const {[user]: roleChanged, ...restChanged} = changedUsersRecord
if (roleChanged === undefined) {
setChangedUsersRecord({
...changedUsersRecord,
[user]: !usersRecord[user],
})
} else {
// returning back to original state
setChangedUsersRecord(restChanged)
}
return
},
[usersRecord, changedUsersRecord]
)
const changeUsers = useMemo(
() => async () => {
if (Object.entries(changedUsersRecord).length === 0) {
return
}
setRunning(true)
try {
const newUsers = users.reduce<User[]>((acc, user) => {
const userName = user.name
if (
changedUsersRecord[userName] === true ||
(changedUsersRecord[userName] === undefined &&
usersRecord[roleName])
) {
acc.push(user)
}
return acc
}, [])
await updateUsersAsync(role, newUsers)
} finally {
setRunning(false)
}
},
[role, usersRecord, changedUsersRecord, users]
)
const dataChanged = useMemo(() => permissionsChanged || usersChanged, [
permissionsChanged,
usersChanged,
])
const changeData = useCallback(async () => {
await changeUsers()
await changePermissions()
}, [changePermissions, changeUsers])
const exitHandler = useCallback(() => {
router.push(`/sources/${sourceID}/admin-influxdb/roles`)
}, [router, source])
const databaseNames = useMemo<string[]>(
() =>
databases.reduce(
(acc, db) => {
acc.push(db.name)
return acc
},
['']
),
[databases]
)
const body =
role === FAKE_ROLE ? (
<div className="container-fluid">
User <span className="error-warning">{roleName}</span> not found!
</div>
) : (
<div className="panel panel-solid influxdb-admin">
<div className="panel-heading">
<h2 className="panel-title">
<span title={`Role: ${roleName}`}>{roleName}</span>
</h2>
<div className="panel-heading--right">
<ConfirmButton
type="btn-danger"
text="Delete User"
confirmAction={deleteRole}
disabled={running}
position="bottom"
></ConfirmButton>
</div>
</div>
<div className="panel-body influxdb-admin--detail">
<FancyScrollbar>
<div className="infludb-admin-section__header">
<h4>
Users
{usersChanged ? ' (unsaved)' : ''}
</h4>
</div>
<div className="infludb-admin-section__body">
{!allUserNames.length ? (
<p>No users are defined.</p>
) : (
<p>
{allUserNames.map((userName, i) => (
<div
key={i}
title="Click to change, click Apply Changes to save all changes"
data-user={userName}
className={`user-value ${
usersRecord[userName] ? 'granted' : 'denied'
} ${
changedUsersRecord[userName] !== undefined
? 'value-changed'
: ''
}`}
onClick={onUserChange}
>
{userName}
</div>
))}
</p>
)}
</div>
<div className="infludb-admin-section__header">
<h4>
Permissions
{permissionsChanged ? ' (unsaved)' : ''}
</h4>
</div>
<div className="infludb-admin-section__body">
<div>
<table className="table v-center table-highlight permission-table">
<thead>
<tr>
<th style={{minWidth: '100px', whiteSpace: 'nowrap'}}>
Database
</th>
<th style={{width: '99%', whiteSpace: 'nowrap'}}>
Permissions
</th>
</tr>
</thead>
<tbody>
{(databaseNames || []).map(db => (
<tr
key={db}
className={db ? '' : 'all-databases'}
title={
db
? db
: 'Cluster-Wide Permissions applies to all databases'
}
>
<td>{db || '*'}</td>
<td>
{(db ? dbPermisssions : clusterPermissions).map(
(perm, i) => (
<div
key={i}
title={
PERMISSIONS[perm]?.description ||
'Click to change, click Apply Changes to save all changes'
}
data-db={db}
data-perm={perm}
className={`permission-value ${
roleDBPermissions[db]?.[perm]
? 'granted'
: 'denied'
} ${
changedPermissions[db]?.[perm] !== undefined
? 'value-changed'
: ''
}`}
onClick={onPermissionChange}
>
{perm}
</div>
)
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</FancyScrollbar>
</div>
</div>
)
return (
<Page className="influxdb-admin">
<Page.Header fullWidth={true}>
<Page.Header.Left>
<Page.Title title="Manage Role" />
</Page.Header.Left>
<Page.Header.Right showSourceIndicator={true}>
{dataChanged ? (
<ConfirmButton
text="Exit"
confirmText="Discard unsaved changes?"
confirmAction={exitHandler}
position="left"
/>
) : (
<Button text="Exit" onClick={exitHandler} />
)}
{dataChanged && (
<Button
text="Apply Changes"
onClick={changeData}
color={ComponentColor.Primary}
status={
running ? ComponentStatus.Disabled : ComponentStatus.Default
}
/>
)}
</Page.Header.Right>
</Page.Header>
<div className="influxdb-admin--contents">{body}</div>
</Page>
)
}
export default withSource(
withRouter(connect(mapStateToProps, mapDispatchToProps)(UserPage))
)

View File

@ -66,6 +66,7 @@ import UsersPage from './admin/containers/influxdb/UsersPage'
import RolesPage from './admin/containers/influxdb/RolesPage'
import QueriesPage from './admin/containers/influxdb/QueriesPage'
import UserPage from './admin/containers/influxdb/UserPage'
import RolePage from './admin/containers/influxdb/RolePage'
const errorsQueue = []
@ -238,6 +239,7 @@ class Root extends PureComponent<Record<string, never>, State> {
<Route path="roles" component={RolesPage} />
<Route path="queries" component={QueriesPage} />
<Route path="users/:userName" component={UserPage} />
<Route path="roles/:roleName" component={RolePage} />
</Route>
<Route path="manage-sources" component={ManageSources} />
</Route>

View File

@ -156,7 +156,7 @@ pre.admin-table--query {
.influxdb-admin--contents{
height: calc(100%-60px);
}
div.permission-value,div.role-value{
div.permission-value,div.role-value,div.user-value{
@include btn-base-styles(
$g5-pepper,
$g6-smoke,
@ -203,7 +203,7 @@ pre.admin-table--query {
color: $g7-graphite;
}
}
span.role-value{
span.role-value, span.user-value{
padding: 0 2px;
display: inline-block;
white-space: nowrap;