Merge pull request #5956 from influxdata/5953/keep_tabs_in_detail_pages

feat(ui): Add InfluxDB admin tabs to user/role detail page
pull/5965/head
Pavel Závora 2022-06-24 10:11:52 +02:00 committed by GitHub
commit 38ee6bbad9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 229 additions and 44 deletions

View File

@ -10,6 +10,7 @@
1. [#5927](https://github.com/influxdata/chronograf/pull/5927): Show effective permissions on Users page.
1. [#5929](https://github.com/influxdata/chronograf/pull/5926): Add refresh button to InfluxDB Users/Roles/Databases page.
1. [#5940](https://github.com/influxdata/chronograf/pull/5940): Support InfluxDB behind proxy under subpath.
1. [#5956](https://github.com/influxdata/chronograf/pull/5956): Add InfluxDB admin tabs to user/role detail page.
### Bug Fixes

View File

@ -107,7 +107,8 @@ describe('InfluxDB', () => {
.contains('Create')
.should('not.be.disabled')
.click({force: true})
cy.getByTestID('exit--button').click({force: true})
cy.url().should('match', new RegExp(`${influxDB.user.name}$`))
cy.get('.subsection--tab.active').click({force: true})
cy.getByTestID(`user-row--${influxDB.user.name}`).should('exist')
cy.getByTestID('user-filter--input').type('Non existing user')
cy.getByTestID(`user-row--${influxDB.user.name}`).should('not.exist')
@ -133,6 +134,12 @@ describe('InfluxDB', () => {
})
cy.getByTestID('apply-changes--button').click({force: true})
cy.url().should('match', /users$/)
cy.getByTestID(`user-row--${influxDB.user.name}`)
.should('exist')
.within(() => {
cy.get('a').contains(influxDB.user.name).click({force: true})
})
cy.getByTestID(`${influxDB.db.name}-permissions--row`).within(() => {
influxDB.user.db[0].permissions.forEach((permission: any) => {
cy.getByTestID(
@ -149,7 +156,7 @@ describe('InfluxDB', () => {
cy.getByTestID('change-password--button').click({force: true})
cy.getByTestID('new-password--input').type(influxDB.user.password)
cy.getByTestID('confirm').click({force: true})
cy.getByTestID('exit--button').click({force: true})
cy.get('.subsection--tab.active').click({force: true})
cy.getByTestID(`user-row--${influxDB.user.name}`).within(() => {
cy.getByTestID('permissions--values').within(() => {
cy.getByTestID('read-permission').should('have.class', 'granted')
@ -176,7 +183,8 @@ describe('InfluxDB', () => {
.contains('Create')
.should('not.be.disabled')
.click({force: true})
cy.getByTestID('exit--button').click({force: true})
cy.url().should('match', new RegExp(`${influxDB.user.name}$`))
cy.get('.subsection--tab.active').click({force: true})
cy.get('.dropdown--selected').click({force: true})
cy.getByTestID('dropdown-menu').within(() => {
cy.getByTestID('dropdown--item')
@ -202,11 +210,17 @@ describe('InfluxDB', () => {
'value-changed'
)
cy.getByTestID('apply-changes--button').click({force: true})
cy.url().should('match', new RegExp(`users$`))
cy.getByTestID(`user-row--${influxDB.user.name}`)
.should('exist')
.within(() => {
cy.get('a').contains(influxDB.user.name).click({force: true})
})
cy.getByTestID(`role-${influxDB.role.name}--button`).should(
'not.have.class',
'value-changed'
)
cy.getByTestID('exit--button').click({force: true})
cy.get('.subsection--tab.active').click({force: true})
cy.getByTestID('roles-granted').within(() => {
cy.get('.role-value').contains(influxDB.role.name).should('exist')
})
@ -236,7 +250,8 @@ describe('InfluxDB', () => {
cy.getByTestID('form--create-role--button')
.should('not.be.disabled')
.click()
cy.getByTestID('exit--button').click({force: true})
cy.url().should('match', new RegExp(`${influxDB.role.name}$`))
cy.get('.subsection--tab.active').click({force: true})
cy.getByTestID(`role-${influxDB.role.name}--row`)
.should('exist')
.within(() => {
@ -260,6 +275,10 @@ describe('InfluxDB', () => {
})
cy.getByTestID('apply-changes--button').click({force: true})
cy.url().should('match',new RegExp(`roles$`))
cy.getByTestID(`role-${influxDB.role.name}--row`).within(() => {
cy.get('a').contains(influxDB.role.name).click({force: true})
})
cy.getByTestID(`${influxDB.db.name}-db-perm--row`).within(() => {
influxDB.role.permissions.forEach((perm: any) => {
@ -270,7 +289,8 @@ describe('InfluxDB', () => {
})
})
cy.getByTestID('exit--button').click({force: true})
cy.get('.subsection--tab.active').click({force: true})
cy.url().should('match',new RegExp(`roles$`))
cy.getByTestID('wizard-bucket-selected').click({force: true})
cy.getByTestID('dropdown-menu').within(() => {
cy.getByTestID('dropdown--item')

View File

@ -410,11 +410,13 @@ export const updateRoleUsersAsync = (role, users) => async dispatch => {
const {data} = await updateRoleAJAX(role.links.self, {users})
dispatch(notify(notifyRoleUsersUpdated()))
dispatch(syncRole(role, data))
return true
} catch (error) {
dispatch(
errorThrown(error, notifyRoleUsersUpdateFailed(error.data.message))
)
}
return false
}
export const updateRolePermissionsAsync = (
@ -425,11 +427,13 @@ export const updateRolePermissionsAsync = (
const {data} = await updateRoleAJAX(role.links.self, {permissions})
dispatch(notify(notifyRolePermissionsUpdated()))
dispatch(syncRole(role, data))
return true
} catch (error) {
dispatch(
errorThrown(error, notifyRolePermissionsUpdateFailed(error.data.message))
)
}
return false
}
export const updateUserPermissionsAsync = (
@ -440,6 +444,7 @@ export const updateUserPermissionsAsync = (
const {data} = await updateUserAJAX(user.links.self, {permissions})
dispatch(notify(notifyDBUserPermissionsUpdated()))
dispatch(syncUser(user, data))
return true
} catch (error) {
dispatch(
errorThrown(
@ -455,11 +460,13 @@ export const updateUserRolesAsync = (user, roles) => async dispatch => {
const {data} = await updateUserAJAX(user.links.self, {roles})
dispatch(notify(notifyDBUserRolesUpdated()))
dispatch(syncUser(user, data))
return true
} catch (error) {
dispatch(
errorThrown(error, notifyDBUserRolesUpdateFailed(error.data.message))
)
}
return false
}
export const updateUserPasswordAsync = (user, password) => async dispatch => {

View File

@ -0,0 +1,60 @@
import React from 'react'
import {
Form,
OverlayContainer,
OverlayHeading,
OverlayTechnology,
} from 'src/reusable_ui'
const minLen = 3
export function validateRoleName(name: string): boolean {
return name?.length >= minLen
}
interface Props {
onCancel: () => void
onOK: () => void
visible: boolean
}
const ConfirmDiscardDialog = ({visible, onOK, onCancel}: Props) => {
return (
<OverlayTechnology visible={visible}>
<OverlayContainer maxWidth={400}>
<OverlayHeading title="Discard unsaved changes?" />
<div
className="overlay--body"
style={{minHeight: '100px', padding: '0px'}}
>
<form>
<Form>
{[
<Form.Footer key={1}>
<div className="form-group text-center col-xs-12">
<button
className="btn btn-sm btn-warning"
type="button"
onClick={onOK}
data-test="confirm--ok--button"
>
OK
</button>
<button
className="btn btn-sm btn-default"
onClick={onCancel}
type="button"
data-test="confirm--cancel--button"
>
Cancel
</button>
</div>
</Form.Footer>,
]}
</Form>
</form>
</div>
</OverlayContainer>
</OverlayTechnology>
)
}
export default ConfirmDiscardDialog

View File

@ -2,12 +2,14 @@ import React from 'react'
import {useMemo} from 'react'
import SubSections from 'src/shared/components/SubSections'
import {Source, SourceAuthenticationMethod} from 'src/types'
import {PageSection} from 'src/types/shared'
import {WrapToPage} from './AdminInfluxDBScopedPage'
interface Props {
source: Source
activeTab: 'databases' | 'users' | 'roles' | 'queries'
children: JSX.Element | JSX.Element[]
onTabChange?: (section: PageSection, url: string) => void
}
export function hasRoleManagement(source: Source) {
return !!source?.links?.roles
@ -16,7 +18,12 @@ export function isConnectedToLDAP(source: Source) {
return source.authentication === SourceAuthenticationMethod.LDAP
}
const AdminInfluxDBTabbedPage = ({source, activeTab, children}: Props) => {
export const AdminTabs = ({
source,
activeTab,
children,
onTabChange,
}: Props) => {
const sections = useMemo(() => {
const hasRoles = hasRoleManagement(source)
const isLDAP = isConnectedToLDAP(source)
@ -44,16 +51,33 @@ const AdminInfluxDBTabbedPage = ({source, activeTab, children}: Props) => {
]
}, [source])
return (
<WrapToPage hideRefresh={activeTab === 'queries'}>
<SubSections
parentUrl="admin-influxdb"
sourceID={source.id}
activeSection={activeTab}
sections={sections}
position="top"
onTabChange={onTabChange}
>
{children}
</SubSections>
)
}
const AdminInfluxDBTabbedPage = ({
source,
activeTab,
children,
onTabChange,
}: Props) => {
return (
<WrapToPage hideRefresh={activeTab === 'queries'}>
<AdminTabs
source={source}
activeTab={activeTab}
onTabChange={onTabChange}
>
{children}
</AdminTabs>
</WrapToPage>
)
}

View File

@ -3,7 +3,11 @@ 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 {
AdminTabs,
hasRoleManagement,
isConnectedToLDAP,
} from './AdminInfluxDBTabbedPage'
import {withRouter, WithRouterProps} from 'react-router'
import {useMemo} from 'react'
import ConfirmButton from 'src/shared/components/ConfirmButton'
@ -22,6 +26,7 @@ import {
computePermissionsChange,
toUserPermissions,
} from '../../util/permissions'
import ConfirmDiscardDialog from 'src/admin/components/influxdb/ConfirmDiscardDialog'
const FAKE_ROLE: UserRole = {
name: '',
@ -148,7 +153,7 @@ const RolePage = ({
const changePermissions = useMemo(
() => async () => {
if (Object.entries(changedPermissions).length === 0) {
return
return true
}
setRunning(true)
try {
@ -157,7 +162,7 @@ const RolePage = ({
roleDBPermissions,
changedPermissions
)
await updatePermissionsAsync(role, permissions)
return await updatePermissionsAsync(role, permissions)
} finally {
setRunning(false)
}
@ -201,7 +206,7 @@ const RolePage = ({
const changeUsers = useMemo(
() => async () => {
if (Object.entries(changedUsersRecord).length === 0) {
return
return true
}
setRunning(true)
try {
@ -216,7 +221,7 @@ const RolePage = ({
}
return acc
}, [])
await updateUsersAsync(role, newUsers)
return await updateUsersAsync(role, newUsers)
} finally {
setRunning(false)
}
@ -228,13 +233,14 @@ const RolePage = ({
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 changeData = useCallback(async () => {
if ((await changeUsers()) && (await changePermissions())) {
exitHandler()
}
}, [changePermissions, changeUsers, exitHandler])
const databaseNames = useMemo<string[]>(
() =>
databases.reduce(
@ -246,6 +252,25 @@ const RolePage = ({
),
[databases]
)
const [exitUrl, setExitUrl] = useState('')
const onTabChange = useCallback(
(_section, url) => {
if (dataChanged) {
setExitUrl(url)
return
}
router.push(url)
},
[router, dataChanged]
)
const onExitCancel = useCallback(() => {
setExitUrl('')
}, [])
const onExitConfirm = useCallback(() => {
router.push(exitUrl)
}, [router, exitUrl])
const body =
role === FAKE_ROLE ? (
<div className="container-fluid">
@ -375,7 +400,7 @@ const RolePage = ({
<Page className="influxdb-admin">
<Page.Header fullWidth={true}>
<Page.Header.Left>
<Page.Title title="Manage Role" />
<Page.Title title="InfluxDB Role" />
</Page.Header.Left>
<Page.Header.Right showSourceIndicator={true}>
{dataChanged ? (
@ -402,7 +427,16 @@ const RolePage = ({
)}
</Page.Header.Right>
</Page.Header>
<div className="influxdb-admin--contents">{body}</div>
<div className="influxdb-admin--contents">
<AdminTabs activeTab="roles" source={source} onTabChange={onTabChange}>
<ConfirmDiscardDialog
onOK={onExitConfirm}
onCancel={onExitCancel}
visible={!!exitUrl}
/>
{body}
</AdminTabs>
</div>
</Page>
)
}

View File

@ -3,7 +3,11 @@ 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 {
AdminTabs,
hasRoleManagement,
isConnectedToLDAP,
} from './AdminInfluxDBTabbedPage'
import {withRouter, WithRouterProps} from 'react-router'
import {useMemo} from 'react'
import ConfirmButton from 'src/shared/components/ConfirmButton'
@ -24,6 +28,7 @@ import {
computePermissionsChange,
toUserPermissions,
} from '../../util/permissions'
import ConfirmDiscardDialog from 'src/admin/components/influxdb/ConfirmDiscardDialog'
const FAKE_USER: User = {
name: '',
@ -179,7 +184,7 @@ const UserPage = ({
const changePermissions = useMemo(
() => async () => {
if (Object.entries(changedPermissions).length === 0) {
return
return true
}
setRunning(true)
try {
@ -189,7 +194,7 @@ const UserPage = ({
changedPermissions,
isEnterprise ? [] : user.permissions.filter(x => x.scope === 'all')
)
await updatePermissionsAsync(user, permissions)
return await updatePermissionsAsync(user, permissions)
} finally {
setRunning(false)
}
@ -236,7 +241,7 @@ const UserPage = ({
const changeRoles = useMemo(
() => async () => {
if (Object.entries(changedRolesRecord).length === 0) {
return
return true
}
setRunning(true)
try {
@ -251,7 +256,7 @@ const UserPage = ({
}
return acc
}, [])
await updateRolesAsync(user, newRoles)
return await updateRolesAsync(user, newRoles)
} finally {
setRunning(false)
}
@ -263,13 +268,14 @@ const UserPage = ({
permissionsChanged,
rolesChanged,
])
const changeData = useCallback(async () => {
await changeRoles()
await changePermissions()
}, [changePermissions, changeRoles])
const exitHandler = useCallback(() => {
router.push(`/sources/${sourceID}/admin-influxdb/users`)
}, [router, source])
const changeData = useCallback(async () => {
if ((await changeRoles()) && (await changePermissions())) {
exitHandler()
}
}, [changePermissions, changeRoles, exitHandler])
const databaseNames = useMemo<string[]>(
() =>
databases.reduce(
@ -281,6 +287,24 @@ const UserPage = ({
),
[isEnterprise, databases]
)
const [exitUrl, setExitUrl] = useState('')
const onTabChange = useCallback(
(_section, url) => {
if (dataChanged) {
setExitUrl(url)
return
}
router.push(url)
},
[router, dataChanged]
)
const onExitCancel = useCallback(() => {
setExitUrl('')
}, [])
const onExitConfirm = useCallback(() => {
router.push(exitUrl)
}, [router, exitUrl])
const body =
user === FAKE_USER ? (
<div className="container-fluid">
@ -480,7 +504,7 @@ const UserPage = ({
<Page className="influxdb-admin">
<Page.Header fullWidth={true}>
<Page.Header.Left>
<Page.Title title="Manage User" />
<Page.Title title="InfluxDB User" />
</Page.Header.Left>
<Page.Header.Right showSourceIndicator={true}>
{dataChanged ? (
@ -489,7 +513,7 @@ const UserPage = ({
confirmText="Discard unsaved changes?"
confirmAction={exitHandler}
position="left"
testId="exit--button"
testId="discard-changes--exit--button"
/>
) : (
<Button text="Exit" onClick={exitHandler} testId="exit--button" />
@ -507,7 +531,16 @@ const UserPage = ({
)}
</Page.Header.Right>
</Page.Header>
<div className="influxdb-admin--contents">{body}</div>
<div className="influxdb-admin--contents">
<AdminTabs activeTab="users" source={source} onTabChange={onTabChange}>
<ConfirmDiscardDialog
onOK={onExitConfirm}
onCancel={onExitCancel}
visible={!!exitUrl}
/>
{body}
</AdminTabs>
</div>
</Page>
)
}

View File

@ -25,7 +25,7 @@ $overlay-min-height: 150px;
@extend %overlay-styles;
z-index: 1;
opacity: 0;
transition: opacity 0.25s ease;
// transition: opacity 1.25s ease; // some defect in Chrome causes to randomly go to opacity 1 (ConfirmDiscardDialog)
@include gradient-diag-down($c-pool,$c-comet);
}

View File

@ -33,6 +33,7 @@ interface Props extends WithRouterProps {
parentUrl: string
children?: ReactNode
position?: 'left' | 'top'
onTabChange?: (section: PageSection, url: string) => void
}
@ErrorHandling
@ -55,7 +56,7 @@ class SubSections extends Component<Props> {
<SubSectionsTab
key={i}
section={section}
handleClick={this.handleTabClick(section.url)}
handleClick={this.handleTabClick(section)}
activeSection={activeSection}
/>
)
@ -75,9 +76,14 @@ class SubSections extends Component<Props> {
return found?.component || children || <NotFound />
}
public handleTabClick = (url: string) => () => {
const {router, sourceID, parentUrl} = this.props
router.push(`/sources/${sourceID}/${parentUrl}/${url}`)
public handleTabClick = (section: PageSection) => () => {
const {router, sourceID, parentUrl, onTabChange} = this.props
const url = `/sources/${sourceID}/${parentUrl}/${section.url}`
if (onTabChange) {
onTabChange(section, url)
return
}
router.push(url)
}
}

View File

@ -265,7 +265,7 @@ pre.admin-table--query {
height: calc(100vh - 150px);
min-height: 10px;
&.influxdb-admin--detail{
height: calc(100vh - 120px);
height: calc(100vh - 150px);
padding: 10px 0 0;
background-color: $g1-raven;
}