diff --git a/CHANGELOG.md b/CHANGELOG.md index eb75530aba..2925fd380c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/ui/cypress/integration/admin_influxdb.test.ts b/ui/cypress/integration/admin_influxdb.test.ts index 53b357c236..da9513fe9f 100644 --- a/ui/cypress/integration/admin_influxdb.test.ts +++ b/ui/cypress/integration/admin_influxdb.test.ts @@ -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') @@ -296,7 +316,7 @@ describe('InfluxDB', () => { cy.getByTestID('show-users--toggle').click() cy.getByTestID('admin-table--head').within(() => { cy.get('th').contains('Users').should('exist') - }) + }) }) }) }) diff --git a/ui/src/admin/actions/influxdb.js b/ui/src/admin/actions/influxdb.js index fa2898664b..8ba2e153dd 100644 --- a/ui/src/admin/actions/influxdb.js +++ b/ui/src/admin/actions/influxdb.js @@ -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 => { diff --git a/ui/src/admin/components/influxdb/ConfirmDiscardDialog.tsx b/ui/src/admin/components/influxdb/ConfirmDiscardDialog.tsx new file mode 100644 index 0000000000..3671f9ccc2 --- /dev/null +++ b/ui/src/admin/components/influxdb/ConfirmDiscardDialog.tsx @@ -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 ( + + + +
+
+ + {[ + +
+ + +
+
, + ]} +
+ +
+
+
+ ) +} + +export default ConfirmDiscardDialog diff --git a/ui/src/admin/containers/influxdb/AdminInfluxDBTabbedPage.tsx b/ui/src/admin/containers/influxdb/AdminInfluxDBTabbedPage.tsx index 4cd738445d..45ed056ef9 100644 --- a/ui/src/admin/containers/influxdb/AdminInfluxDBTabbedPage.tsx +++ b/ui/src/admin/containers/influxdb/AdminInfluxDBTabbedPage.tsx @@ -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) @@ -43,17 +50,34 @@ const AdminInfluxDBTabbedPage = ({source, activeTab, children}: Props) => { }, ] }, [source]) + return ( + + {children} + + ) +} +const AdminInfluxDBTabbedPage = ({ + source, + activeTab, + children, + onTabChange, +}: Props) => { return ( - {children} - + ) } diff --git a/ui/src/admin/containers/influxdb/RolePage.tsx b/ui/src/admin/containers/influxdb/RolePage.tsx index cfaeffcf42..70f91f54c0 100644 --- a/ui/src/admin/containers/influxdb/RolePage.tsx +++ b/ui/src/admin/containers/influxdb/RolePage.tsx @@ -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( () => 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 ? (
@@ -375,7 +400,7 @@ const RolePage = ({ - + {dataChanged ? ( @@ -402,7 +427,16 @@ const RolePage = ({ )} -
{body}
+
+ + + {body} + +
) } diff --git a/ui/src/admin/containers/influxdb/UserPage.tsx b/ui/src/admin/containers/influxdb/UserPage.tsx index b84b4bec90..0febe2b023 100644 --- a/ui/src/admin/containers/influxdb/UserPage.tsx +++ b/ui/src/admin/containers/influxdb/UserPage.tsx @@ -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( () => 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 ? (
@@ -480,7 +504,7 @@ const UserPage = ({ - + {dataChanged ? ( @@ -489,7 +513,7 @@ const UserPage = ({ confirmText="Discard unsaved changes?" confirmAction={exitHandler} position="left" - testId="exit--button" + testId="discard-changes--exit--button" /> ) : (