Merge pull request #5944 from influxdata/5942/influxdb_admin_filter_redux

feat(ui/admin): remember state of influxdb users/roles filters
pull/5949/head
Pavel Závora 2022-06-21 11:06:20 +02:00 committed by GitHub
commit cbbd0ea829
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 243 additions and 133 deletions

View File

@ -289,10 +289,14 @@ describe('InfluxDB', () => {
cy.get('th').contains('Users').should('exist')
})
cy.getByTestID('hide-users--toggle').click()
cy.getByTestID('show-users--toggle').click()
cy.getByTestID('admin-table--head').within(() => {
cy.get('th').contains('Users').should('not.exist')
})
cy.getByTestID('show-users--toggle').click()
cy.getByTestID('admin-table--head').within(() => {
cy.get('th').contains('Users').should('exist')
})
})
})
})

View File

@ -473,3 +473,18 @@ export const updateUserPasswordAsync = (user, password) => async dispatch => {
)
}
}
export const changeSelectedDBs = (selectedDBs /* : string[] */) => ({
type: 'INFLUXDB_CHANGE_SELECTED_DBS',
payload: {
selectedDBs,
},
})
export const changeShowUsers = () => ({
type: 'INFLUXDB_CHANGE_SHOW_USERS',
})
export const changeShowRoles = () => ({
type: 'INFLUXDB_CHANGE_SHOW_ROLES',
})

View File

@ -0,0 +1,53 @@
import React from 'react'
import {connect, ResolveThunks} from 'react-redux'
import {changeSelectedDBs} from 'src/admin/actions/influxdb'
import {MultiSelectDropdown} from 'src/reusable_ui'
import {Database} from 'src/types/influxAdmin'
interface ConnectedProps {
databases: Database[]
selectedDBs: string[]
}
const mapStateToProps = ({adminInfluxDB: {databases, selectedDBs}}) => ({
databases,
selectedDBs,
})
const mapDispatchToProps = {
setSelectedDBs: changeSelectedDBs,
}
type ReduxDispatchProps = ResolveThunks<typeof mapDispatchToProps>
type Props = ConnectedProps & ReduxDispatchProps
const MultiDBSelector = ({databases, selectedDBs, setSelectedDBs}: Props) => {
return (
<div className="db-selector">
<MultiSelectDropdown
onChange={setSelectedDBs}
selectedIDs={selectedDBs}
emptyText="<no database>"
>
{databases.reduce(
(acc, db) => {
acc.push(
<MultiSelectDropdown.Item
key={db.name}
id={db.name}
value={{id: db.name}}
>
{db.name}
</MultiSelectDropdown.Item>
)
return acc
},
[
<MultiSelectDropdown.Item id="*" key="*" value={{id: '*'}}>
All Databases
</MultiSelectDropdown.Item>,
<MultiSelectDropdown.Divider id="" key="" />,
]
)}
</MultiSelectDropdown>
</div>
)
}
export default connect(mapStateToProps, mapDispatchToProps)(MultiDBSelector)

View File

@ -142,7 +142,7 @@ const RolePage = ({
)
)
},
[roleDBPermissions, changedPermissions, setChangedPermissions]
[roleDBPermissions, changedPermissions]
)
const permissionsChanged = !!Object.keys(changedPermissions).length
const changePermissions = useMemo(

View File

@ -6,6 +6,7 @@ import {Source, NotificationAction} from 'src/types'
import {UserRole, User, Database} from 'src/types/influxAdmin'
import {notify as notifyAction} from 'src/shared/actions/notifications'
import {
changeShowUsers,
createRoleAsync,
filterRoles as filterRolesAction,
} from 'src/admin/actions/influxdb'
@ -21,14 +22,14 @@ import FancyScrollbar from 'src/shared/components/FancyScrollbar'
import NoEntities from 'src/admin/components/influxdb/NoEntities'
import RoleRow from 'src/admin/components/RoleRow'
import {useCallback} from 'react'
import allOrParticularSelection from '../../util/allOrParticularSelection'
import {computeEntitiesDBPermissions} from '../../util/computeEffectiveDBPermissions'
import useDebounce from 'src/utils/useDebounce'
import useChangeEffect from 'src/utils/useChangeEffect'
import {ComponentSize, MultiSelectDropdown, SlideToggle} from 'src/reusable_ui'
import {ComponentSize, SlideToggle} from 'src/reusable_ui'
import CreateRoleDialog, {
validateRoleName,
} from 'src/admin/components/influxdb/CreateRoleDialog'
import MultiDBSelector from 'src/admin/components/influxdb/MultiDBSelector'
const validateRole = (
role: Pick<UserRole, 'name'>,
@ -41,16 +42,22 @@ const validateRole = (
return true
}
const mapStateToProps = ({adminInfluxDB: {databases, users, roles}}) => ({
const mapStateToProps = ({
adminInfluxDB: {databases, users, roles, selectedDBs, showUsers, rolesFilter},
}) => ({
databases,
users,
roles,
selectedDBs,
showUsers,
rolesFilter,
})
const mapDispatchToProps = {
filterRoles: filterRolesAction,
createRole: createRoleAsync,
notify: notifyAction,
toggleShowUsers: changeShowUsers,
}
interface OwnProps {
@ -60,6 +67,9 @@ interface ConnectedProps {
databases: Database[]
users: User[]
roles: UserRole[]
selectedDBs: string[]
showUsers: boolean
rolesFilter: string
}
type ReduxDispatchProps = ResolveThunks<typeof mapDispatchToProps>
@ -71,30 +81,26 @@ const RolesPage = ({
users,
roles,
databases,
selectedDBs,
showUsers,
rolesFilter,
router,
filterRoles,
createRole,
toggleShowUsers,
notify,
}: Props) => {
const rolesPage = useMemo(
() => `/sources/${source.id}/admin-influxdb/roles`,
[source]
)
// filter databases
const [selectedDBs, setSelectedDBs] = useState<string[]>(['*'])
// database columns
const visibleDBNames = useMemo<string[]>(() => {
if (selectedDBs.includes('*')) {
return databases.map(db => db.name)
}
return selectedDBs
}, [databases, selectedDBs])
const changeSelectedDBs = useCallback(
(newDBs: string[]) =>
setSelectedDBs((oldDBs: string[]) => {
return allOrParticularSelection(oldDBs, newDBs)
}),
[setSelectedDBs]
)
// effective permissions
const visibleRoles = useMemo(() => roles.filter(x => !x.hidden), [roles])
@ -103,23 +109,14 @@ const RolesPage = ({
[visibleDBNames, visibleRoles]
)
// filter users
const [filterText, setFilterText] = useState('')
const changeFilterText = useCallback(e => setFilterText(e.target.value), [
setFilterText,
])
// filter roles
const [filterText, setFilterText] = useState(rolesFilter)
const changeFilterText = useCallback(e => setFilterText(e.target.value), [])
const debouncedFilterText = useDebounce(filterText, 200)
useChangeEffect(() => {
filterRoles(debouncedFilterText)
}, [debouncedFilterText])
// hide users
const [showUsers, setShowUsers] = useState(true)
const changeHideUsers = useCallback(() => setShowUsers(!showUsers), [
showUsers,
setShowUsers,
])
const [createVisible, setCreateVisible] = useState(false)
const createNew = useCallback(
async (role: {name: string}) => {
@ -159,40 +156,13 @@ const RolesPage = ({
/>
<span className="icon search" />
</div>
<div className="db-selector">
<MultiSelectDropdown
onChange={changeSelectedDBs}
selectedIDs={selectedDBs}
emptyText="<no database>"
>
{databases.reduce(
(acc, db) => {
acc.push(
<MultiSelectDropdown.Item
key={db.name}
id={db.name}
value={{id: db.name}}
>
{db.name}
</MultiSelectDropdown.Item>
)
return acc
},
[
<MultiSelectDropdown.Item id="*" key="*" value={{id: '*'}}>
All Databases
</MultiSelectDropdown.Item>,
<MultiSelectDropdown.Divider id="" key="" />,
]
)}
</MultiSelectDropdown>
</div>
<MultiDBSelector />
<div className="hide-roles-toggle">
<SlideToggle
active={showUsers}
onChange={changeHideUsers}
onChange={toggleShowUsers}
size={ComponentSize.ExtraSmall}
entity="users"
dataTest="show-users--toggle"
/>
Show Users
</div>

View File

@ -173,7 +173,7 @@ const UserPage = ({
)
)
},
[userDBPermissions, changedPermissions, setChangedPermissions]
[userDBPermissions, changedPermissions]
)
const permissionsChanged = !!Object.keys(changedPermissions).length
const changePermissions = useMemo(

View File

@ -5,6 +5,7 @@ import {Source, NotificationAction} from 'src/types'
import {UserRole, User, Database} from 'src/types/influxAdmin'
import {notify as notifyAction} from 'src/shared/actions/notifications'
import {
changeShowRoles,
createUserAsync,
filterUsers as filterUsersAction,
} from 'src/admin/actions/influxdb'
@ -22,15 +23,14 @@ import NoEntities from 'src/admin/components/influxdb/NoEntities'
import UserRow from 'src/admin/components/UserRow'
import useDebounce from 'src/utils/useDebounce'
import useChangeEffect from 'src/utils/useChangeEffect'
import MultiSelectDropdown from 'src/reusable_ui/components/dropdowns/MultiSelectDropdown'
import {ComponentSize, SlideToggle} from 'src/reusable_ui'
import {computeEffectiveUserDBPermissions} from '../../util/computeEffectiveDBPermissions'
import allOrParticularSelection from '../../util/allOrParticularSelection'
import CreateUserDialog, {
validatePassword,
validateUserName,
} from '../../components/influxdb/CreateUserDialog'
import {withRouter, WithRouterProps} from 'react-router'
import MultiDBSelector from 'src/admin/components/influxdb/MultiDBSelector'
const validateUser = (
user: Pick<User, 'name' | 'password'>,
@ -47,15 +47,21 @@ const validateUser = (
return true
}
const mapStateToProps = ({adminInfluxDB: {databases, users, roles}}) => ({
const mapStateToProps = ({
adminInfluxDB: {databases, users, roles, selectedDBs, showRoles, usersFilter},
}) => ({
databases,
users,
roles,
selectedDBs,
showRoles,
usersFilter,
})
const mapDispatchToProps = {
filterUsers: filterUsersAction,
createUser: createUserAsync,
toggleShowRoles: changeShowRoles,
notify: notifyAction,
}
@ -66,6 +72,9 @@ interface ConnectedProps {
databases: Database[]
users: User[]
roles: UserRole[]
selectedDBs: string[]
showRoles: boolean
usersFilter: string
}
type ReduxDispatchProps = ResolveThunks<typeof mapDispatchToProps>
@ -77,9 +86,13 @@ const UsersPage = ({
databases,
users,
roles,
selectedDBs,
showRoles,
usersFilter,
notify,
createUser,
filterUsers,
toggleShowRoles,
}: Props) => {
const [isEnterprise, usersPage] = useMemo(
() => [
@ -88,21 +101,13 @@ const UsersPage = ({
],
[source]
)
// filter databases
const [selectedDBs, setSelectedDBs] = useState<string[]>(['*'])
// database columns
const visibleDBNames = useMemo<string[]>(() => {
if (selectedDBs.includes('*')) {
return databases.map(db => db.name)
}
return selectedDBs
}, [databases, selectedDBs])
const changeSelectedDBs = useCallback(
(newDBs: string[]) =>
setSelectedDBs((oldDBs: string[]) => {
return allOrParticularSelection(oldDBs, newDBs)
}),
[setSelectedDBs]
)
// effective permissions
const visibleUsers = useMemo(() => users.filter(x => !x.hidden), [users])
@ -113,22 +118,13 @@ const UsersPage = ({
)
// filter users
const [filterText, setFilterText] = useState('')
const changeFilterText = useCallback(e => setFilterText(e.target.value), [
setFilterText,
])
const [filterText, setFilterText] = useState(usersFilter)
const changeFilterText = useCallback(e => setFilterText(e.target.value), [])
const debouncedFilterText = useDebounce(filterText, 200)
useChangeEffect(() => {
filterUsers(debouncedFilterText)
}, [debouncedFilterText])
// hide role
const [showRoles, setShowRoles] = useState(true)
const changeHideRoles = useCallback(() => setShowRoles(!showRoles), [
showRoles,
setShowRoles,
])
const [createVisible, setCreateVisible] = useState(false)
const createNew = useCallback(
async (user: {name: string; password: string}) => {
@ -169,39 +165,12 @@ const UsersPage = ({
/>
<span className="icon search" />
</div>
<div className="db-selector" data-test="db-selector">
<MultiSelectDropdown
onChange={changeSelectedDBs}
selectedIDs={selectedDBs}
emptyText="<no database>"
>
{databases.reduce(
(acc, db) => {
acc.push(
<MultiSelectDropdown.Item
key={db.name}
id={db.name}
value={{id: db.name}}
>
{db.name}
</MultiSelectDropdown.Item>
)
return acc
},
[
<MultiSelectDropdown.Item id="*" key="*" value={{id: '*'}}>
All Databases
</MultiSelectDropdown.Item>,
<MultiSelectDropdown.Divider id="" key="" />,
]
)}
</MultiSelectDropdown>
</div>
<MultiDBSelector />
{isEnterprise && (
<div className="hide-roles-toggle">
<SlideToggle
active={showRoles}
onChange={changeHideRoles}
onChange={toggleShowRoles}
size={ComponentSize.ExtraSmall}
/>
Show Roles

View File

@ -6,6 +6,7 @@ import {
changeNamedCollection,
computeNamedChanges,
} from '../util/changeNamedCollection'
import allOrParticularSelection from '../util/allOrParticularSelection'
const querySorters = {
'+time'(queries) {
@ -37,8 +38,7 @@ const identity = x => x
function sortQueries(queries, queriesSort) {
return (querySorters[queriesSort] || identity)(queries)
}
const initialState = {
export const initialState = {
users: [],
roles: [],
permissions: [],
@ -46,16 +46,21 @@ const initialState = {
queriesSort: '-time',
queryIDToKill: null,
databases: [],
selectedDBs: ['*'],
showUsers: true,
showRoles: true,
usersFilter: '',
rolesFilter: '',
}
const adminInfluxDB = (state = initialState, action) => {
switch (action.type) {
case 'INFLUXDB_LOAD_USERS': {
return {...state, ...action.payload}
return {...state, ...action.payload, usersFilter: ''}
}
case 'INFLUXDB_LOAD_ROLES': {
return {...state, ...action.payload}
return {...state, ...action.payload, rolesFilter: ''}
}
case 'INFLUXDB_LOAD_PERMISSIONS': {
@ -63,7 +68,9 @@ const adminInfluxDB = (state = initialState, action) => {
}
case 'INFLUXDB_LOAD_DATABASES': {
return {...state, ...action.payload}
const databases = action.payload.databases
const selectedDBs = initialState.selectedDBs
return {...state, databases, selectedDBs}
}
case 'INFLUXDB_ADD_DATABASE': {
@ -333,7 +340,7 @@ const adminInfluxDB = (state = initialState, action) => {
return u
}),
}
return {...state, ...newState}
return {...state, ...newState, usersFilter: text}
}
case 'INFLUXDB_FILTER_ROLES': {
@ -344,7 +351,7 @@ const adminInfluxDB = (state = initialState, action) => {
return r
}),
}
return {...state, ...newState}
return {...state, ...newState, rolesFilter: text}
}
case 'INFLUXDB_KILL_QUERY': {
@ -359,6 +366,18 @@ const adminInfluxDB = (state = initialState, action) => {
case 'INFLUXDB_SET_QUERY_TO_KILL': {
return {...state, ...action.payload}
}
case 'INFLUXDB_CHANGE_SELECTED_DBS': {
const newDBs = action.payload.selectedDBs
const oldDBs = state.selectedDBs || ['*']
const selectedDBs = allOrParticularSelection(oldDBs, newDBs)
return {...state, selectedDBs}
}
case 'INFLUXDB_CHANGE_SHOW_USERS': {
return {...state, showUsers: !state.showUsers}
}
case 'INFLUXDB_CHANGE_SHOW_ROLES': {
return {...state, showRoles: !state.showRoles}
}
}
return state

View File

@ -10,6 +10,7 @@ import {defaultTableData} from 'src/logs/constants'
import {VERSION, GIT_SHA} from 'src/shared/constants'
import {LocalStorage} from 'src/types/localStorage'
import {initialState as adminInfluxDBInitialState} from './admin/reducers/influxdb'
export const loadLocalStorage = (
errorsQueue: any[]
@ -39,7 +40,7 @@ export const loadLocalStorage = (
delete state.VERSION
delete state.GIT_SHA
state.adminInfluxDB = {...adminInfluxDBInitialState, ...state.adminInfluxDB}
return state
} catch (error) {
console.error(notifyLoadLocalSettingsFailed(error).message)
@ -55,6 +56,7 @@ export const saveToLocalStorage = ({
dashTimeV1: {ranges, refreshes},
logs,
script,
adminInfluxDB: {showUsers, showRoles},
}: LocalStorage): void => {
try {
const dashTimeV1 = {
@ -104,6 +106,7 @@ export const saveToLocalStorage = ({
},
tableTime: minimalLogs.tableTime || {},
},
adminInfluxDB: {showRoles, showUsers},
})
)
} catch (err) {

View File

@ -14,7 +14,7 @@ interface Props {
color?: ComponentColor
disabled?: boolean
tooltipText?: string
entity?: string
dataTest?: string
}
@ErrorHandling
@ -27,14 +27,14 @@ class SlideToggle extends Component<Props> {
}
public render() {
const {tooltipText} = this.props
const {tooltipText, dataTest} = this.props
return (
<div
className={this.className}
onClick={this.handleClick}
title={tooltipText}
data-test={this.dataTest}
data-test={dataTest}
>
<div className="slide-toggle--knob" />
</div>
@ -59,12 +59,6 @@ class SlideToggle extends Component<Props> {
{active, disabled}
)
}
private get dataTest(): string {
const {active, entity} = this.props
return active ? `hide-${entity}--toggle` : `show-${entity}--toggle`
}
}
export default SlideToggle

View File

@ -10,6 +10,10 @@ export interface LocalStorage {
logs: LogsState
telegrafSystemInterval: string
hostPageDisabled: boolean
adminInfluxDB: {
showUsers: boolean
showRoles: boolean
}
}
export type VERSION = string

View File

@ -7,6 +7,7 @@ import {
syncRole,
editDatabase,
editRetentionPolicyRequested,
loadUsers,
loadRoles,
loadPermissions,
deleteRole,
@ -19,6 +20,10 @@ import {
removeDatabaseDeleteCode,
loadQueries,
setQueriesSort,
loadDatabases,
changeSelectedDBs,
changeShowUsers,
changeShowRoles,
} from 'src/admin/actions/influxdb'
import {NEW_DEFAULT_DATABASE, NEW_EMPTY_RP} from 'src/admin/constants'
@ -137,6 +142,17 @@ describe('Admin.InfluxDB.Reducers', () => {
state = {databases: [db1, db2]}
})
it('can load databases', () => {
const {databases, selectedDBs} = reducer(
undefined,
loadDatabases([{name: 'db1'}])
)
expect({databases, selectedDBs}).toEqual({
databases: [{name: 'db1'}],
selectedDBs: ['*'],
})
})
it('can add a database', () => {
const actual = reducer(state, addDatabase())
const expected = [{...NEW_DEFAULT_DATABASE, isEditing: true}, db1, db2]
@ -209,7 +225,15 @@ describe('Admin.InfluxDB.Reducers', () => {
expect(actual.databases).toEqual(expected)
})
})
it('it can load users', () => {
const {users: d, usersFilter} = reducer(state, loadUsers({users}))
const expected = {
users,
usersFilter: '',
}
expect({users: d, usersFilter}).toEqual(expected)
})
it('it can sync a stale user', () => {
const staleUser = {...u1, roles: []}
state = {users: [u2, staleUser], roles: []}
@ -315,13 +339,14 @@ describe('Admin.InfluxDB.Reducers', () => {
expect(actual.users).toEqual(expected.users)
})
it('it can load the roles', () => {
const actual = reducer(state, loadRoles({roles}))
it('it can load roles', () => {
const {roles: d, rolesFilter} = reducer(state, loadRoles({roles}))
const expected = {
roles,
rolesFilter: '',
}
expect(actual.roles).toEqual(expected.roles)
expect({roles: d, rolesFilter}).toEqual(expected)
})
it('it can delete a non-existing role', () => {
@ -382,15 +407,16 @@ describe('Admin.InfluxDB.Reducers', () => {
const text = 'x'
const actual = reducer(state, filterRoles(text))
const {roles: d, rolesFilter} = reducer(state, filterRoles(text))
const expected = {
roles: [
{...r1, hidden: false},
{...r2, hidden: true},
],
rolesFilter: text,
}
expect(actual.roles).toEqual(expected.roles)
expect({roles: d, rolesFilter}).toEqual(expected)
})
it('can filter users w/ "zero" text', () => {
@ -400,15 +426,16 @@ describe('Admin.InfluxDB.Reducers', () => {
const text = 'zero'
const actual = reducer(state, filterUsers(text))
const {users: d, usersFilter} = reducer(state, filterUsers(text))
const expected = {
users: [
{...u1, hidden: true},
{...u2, hidden: false},
],
usersFilter: text,
}
expect(actual.users).toEqual(expected.users)
expect({users: d, usersFilter}).toEqual(expected)
})
// Permissions
@ -488,4 +515,56 @@ describe('Admin.InfluxDB.Reducers', () => {
expect(actual.queries[2].id).toEqual(1)
})
})
describe('filters', () => {
it('can change selected DBS', () => {
const testPairs = [
{
prev: undefined,
change: ['db1'],
next: ['db1'],
},
{
prev: [],
change: ['db1'],
next: ['db1'],
},
{
prev: ['db1'],
change: ['db1', '*'],
next: ['*'],
},
{
prev: ['*'],
change: ['db1', '*'],
next: ['db1'],
},
{
prev: ['db1'],
change: [],
next: [],
},
]
testPairs.forEach(({prev, change, next}) => {
const {selectedDBs} = reducer(
{selectedDBs: prev},
changeSelectedDBs(change)
)
expect(selectedDBs).toEqual(next)
})
})
it('can change showUsers flag', () => {
const vals = [undefined, true, false]
vals.forEach(prev => {
const {showUsers} = reducer({showUsers: prev}, changeShowUsers())
expect(showUsers).toEqual(!prev)
})
})
it('can change showRoles flag', () => {
const vals = [undefined, true, false]
vals.forEach(prev => {
const {showRoles} = reducer({showRoles: prev}, changeShowRoles())
expect(showRoles).toEqual(!prev)
})
})
})
})