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 RolesPage from './admin/containers/influxdb/RolesPage'
|
||||||
import QueriesPage from './admin/containers/influxdb/QueriesPage'
|
import QueriesPage from './admin/containers/influxdb/QueriesPage'
|
||||||
import UserPage from './admin/containers/influxdb/UserPage'
|
import UserPage from './admin/containers/influxdb/UserPage'
|
||||||
|
import RolePage from './admin/containers/influxdb/RolePage'
|
||||||
|
|
||||||
const errorsQueue = []
|
const errorsQueue = []
|
||||||
|
|
||||||
|
@ -238,6 +239,7 @@ class Root extends PureComponent<Record<string, never>, State> {
|
||||||
<Route path="roles" component={RolesPage} />
|
<Route path="roles" component={RolesPage} />
|
||||||
<Route path="queries" component={QueriesPage} />
|
<Route path="queries" component={QueriesPage} />
|
||||||
<Route path="users/:userName" component={UserPage} />
|
<Route path="users/:userName" component={UserPage} />
|
||||||
|
<Route path="roles/:roleName" component={RolePage} />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="manage-sources" component={ManageSources} />
|
<Route path="manage-sources" component={ManageSources} />
|
||||||
</Route>
|
</Route>
|
||||||
|
|
|
@ -156,7 +156,7 @@ pre.admin-table--query {
|
||||||
.influxdb-admin--contents{
|
.influxdb-admin--contents{
|
||||||
height: calc(100%-60px);
|
height: calc(100%-60px);
|
||||||
}
|
}
|
||||||
div.permission-value,div.role-value{
|
div.permission-value,div.role-value,div.user-value{
|
||||||
@include btn-base-styles(
|
@include btn-base-styles(
|
||||||
$g5-pepper,
|
$g5-pepper,
|
||||||
$g6-smoke,
|
$g6-smoke,
|
||||||
|
@ -203,7 +203,7 @@ pre.admin-table--query {
|
||||||
color: $g7-graphite;
|
color: $g7-graphite;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
span.role-value{
|
span.role-value, span.user-value{
|
||||||
padding: 0 2px;
|
padding: 0 2px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
Loading…
Reference in New Issue