feat(ui): add RolePage
parent
fbb92b5dbc
commit
ca7af0fa11
|
@ -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))
|
||||
)
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue