diff --git a/ui/src/admin/components/AdminTabs.js b/ui/src/admin/components/AdminTabs.js deleted file mode 100644 index 1296be1b21..0000000000 --- a/ui/src/admin/components/AdminTabs.js +++ /dev/null @@ -1,140 +0,0 @@ -import React from 'react' -import PropTypes from 'prop-types' -import {Tab, Tabs, TabPanel, TabPanels, TabList} from 'shared/components/Tabs' -import UsersTable from 'src/admin/components/UsersTable' -import RolesTable from 'src/admin/components/RolesTable' -import QueriesPage from 'src/admin/containers/QueriesPage' -import DatabaseManagerPage from 'src/admin/containers/DatabaseManagerPage' - -const AdminTabs = ({ - users, - roles, - permissions, - source, - hasRoles, - isEditingUsers, - isEditingRoles, - onClickCreate, - onEditUser, - onSaveUser, - onCancelEditUser, - onEditRole, - onSaveRole, - onCancelEditRole, - onDeleteRole, - onDeleteUser, - onFilterRoles, - onFilterUsers, - onUpdateRoleUsers, - onUpdateRolePermissions, - onUpdateUserRoles, - onUpdateUserPermissions, - onUpdateUserPassword, -}) => { - let tabs = [ - { - type: 'Databases', - component: , - }, - { - type: 'Users', - component: ( - - ), - }, - { - type: 'Roles', - component: ( - - ), - }, - { - type: 'Queries', - component: , - }, - ] - - if (!hasRoles) { - tabs = tabs.filter(t => t.type !== 'Roles') - } - - return ( - - - {tabs.map((t, i) => {tabs[i].type})} - - - {tabs.map((t, i) => ( - {t.component} - ))} - - - ) -} - -const {arrayOf, bool, func, shape, string} = PropTypes - -AdminTabs.propTypes = { - users: arrayOf( - shape({ - name: string.isRequired, - roles: arrayOf( - shape({ - name: string, - }) - ), - }) - ), - roles: arrayOf(shape()), - source: shape(), - permissions: arrayOf(string), - isEditingUsers: bool, - isEditingRoles: bool, - onClickCreate: func.isRequired, - onEditUser: func.isRequired, - onSaveUser: func.isRequired, - onCancelEditUser: func.isRequired, - onEditRole: func.isRequired, - onSaveRole: func.isRequired, - onCancelEditRole: func.isRequired, - onDeleteRole: func.isRequired, - onDeleteUser: func.isRequired, - onFilterRoles: func.isRequired, - onFilterUsers: func.isRequired, - onUpdateRoleUsers: func.isRequired, - onUpdateRolePermissions: func.isRequired, - hasRoles: bool.isRequired, - onUpdateUserPermissions: func, - onUpdateUserRoles: func, - onUpdateUserPassword: func, -} - -export default AdminTabs diff --git a/ui/src/admin/components/DatabaseRow.js b/ui/src/admin/components/DatabaseRow.js index 9a0e7ff2a5..4ea6470922 100644 --- a/ui/src/admin/components/DatabaseRow.js +++ b/ui/src/admin/components/DatabaseRow.js @@ -150,7 +150,7 @@ class DatabaseRow extends Component { ref={r => (this.name = r)} autoFocus={true} spellCheck={false} - autoComplete={false} + autoComplete="false" /> ) : ( name @@ -167,7 +167,7 @@ class DatabaseRow extends Component { ref={r => (this.duration = r)} autoFocus={!isNew} spellCheck={false} - autoComplete={false} + autoComplete="false" /> {isRFDisplayed ? ( @@ -182,7 +182,7 @@ class DatabaseRow extends Component { onKeyDown={this.handleKeyDown} ref={r => (this.replication = r)} spellCheck={false} - autoComplete={false} + autoComplete="false" /> ) : null} diff --git a/ui/src/admin/components/DatabaseTableHeader.js b/ui/src/admin/components/DatabaseTableHeader.js index b56e55e815..d37def718c 100644 --- a/ui/src/admin/components/DatabaseTableHeader.js +++ b/ui/src/admin/components/DatabaseTableHeader.js @@ -98,7 +98,7 @@ const Header = ({ onChange={onDatabaseDeleteConfirm(database)} onKeyDown={onDatabaseDeleteConfirm(database)} autoFocus={true} - autoComplete={false} + autoComplete="false" spellCheck={false} /> ( onKeyDown={onKeyDown(database)} autoFocus={true} spellCheck={false} - autoComplete={false} + autoComplete="false" /> ) diff --git a/ui/src/admin/components/UserEditName.js b/ui/src/admin/components/UserEditName.js index 357ae65f55..34b3a8b3cf 100644 --- a/ui/src/admin/components/UserEditName.js +++ b/ui/src/admin/components/UserEditName.js @@ -38,7 +38,7 @@ class UserEditName extends Component { onKeyPress={this.handleKeyPress(user)} autoFocus={true} spellCheck={false} - autoComplete={false} + autoComplete="false" /> ) diff --git a/ui/src/admin/components/UserNewPassword.js b/ui/src/admin/components/UserNewPassword.js index 3d50067cad..a24aeef3e7 100644 --- a/ui/src/admin/components/UserNewPassword.js +++ b/ui/src/admin/components/UserNewPassword.js @@ -34,7 +34,7 @@ class UserNewPassword extends Component { onChange={this.handleEdit(user)} onKeyPress={this.handleKeyPress(user)} spellCheck={false} - autoComplete={false} + autoComplete="false" /> ) : ( '--' diff --git a/ui/src/admin/containers/AdminInfluxDBPage.js b/ui/src/admin/containers/AdminInfluxDBPage.js index 5f95977285..d39060fbce 100644 --- a/ui/src/admin/containers/AdminInfluxDBPage.js +++ b/ui/src/admin/containers/AdminInfluxDBPage.js @@ -25,9 +25,13 @@ import { filterRoles as filterRolesAction, } from 'src/admin/actions/influxdb' -import AdminTabs from 'src/admin/components/AdminTabs' +import UsersTable from 'src/admin/components/UsersTable' +import RolesTable from 'src/admin/components/RolesTable' +import QueriesPage from 'src/admin/containers/QueriesPage' +import DatabaseManagerPage from 'src/admin/containers/DatabaseManagerPage' import SourceIndicator from 'shared/components/SourceIndicator' import FancyScrollbar from 'shared/components/FancyScrollbar' +import SubSections from 'shared/components/SubSections' import {ErrorHandling} from 'src/shared/decorators/errors' import {notify as notifyAction} from 'shared/actions/notifications' @@ -141,7 +145,7 @@ class AdminInfluxDBPage extends Component { this.props.updateUserPassword(user, password) } - render() { + getAdminSubSections = () => { const { users, roles, @@ -154,6 +158,69 @@ class AdminInfluxDBPage extends Component { const globalPermissions = permissions.find(p => p.scope === 'all') const allowed = globalPermissions ? globalPermissions.allowed : [] + return [ + { + url: 'databases', + name: 'Databases', + enabled: true, + component: , + }, + { + url: 'users', + name: 'Users', + enabled: true, + component: ( + u.isEditing)} + onSave={this.handleSaveUser} + onCancel={this.handleCancelEditUser} + onClickCreate={this.handleClickCreate} + onEdit={this.handleEditUser} + onDelete={this.handleDeleteUser} + onFilter={filterUsers} + onUpdatePermissions={this.handleUpdateUserPermissions} + onUpdateRoles={this.handleUpdateUserRoles} + onUpdatePassword={this.handleUpdateUserPassword} + /> + ), + }, + { + url: 'roles', + name: 'Roles', + enabled: hasRoles, + component: ( + r.isEditing)} + onClickCreate={this.handleClickCreate} + onEdit={this.handleEditRole} + onSave={this.handleSaveRole} + onCancel={this.handleCancelEditRole} + onDelete={this.handleDeleteRole} + onFilter={filterRoles} + onUpdateRoleUsers={this.handleUpdateRoleUsers} + onUpdateRolePermissions={this.handleUpdateRolePermissions} + /> + ), + }, + { + url: 'queries', + name: 'Queries', + enabled: true, + component: , + }, + ] + } + + render() { + const {users, source, params} = this.props + return (
@@ -169,33 +236,12 @@ class AdminInfluxDBPage extends Component { {users ? (
-
- u.isEditing)} - isEditingRoles={roles.some(r => r.isEditing)} - onUpdateRoleUsers={this.handleUpdateRoleUsers} - onUpdateUserRoles={this.handleUpdateUserRoles} - onUpdateUserPassword={this.handleUpdateUserPassword} - onUpdateRolePermissions={this.handleUpdateRolePermissions} - onUpdateUserPermissions={this.handleUpdateUserPermissions} - /> -
+
) : (
@@ -239,6 +285,9 @@ AdminInfluxDBPage.propTypes = { updateUserRoles: func, updateUserPassword: func, notify: func.isRequired, + params: shape({ + tab: string, + }).isRequired, } const mapStateToProps = ({adminInfluxDB: {users, roles, permissions}}) => ({ diff --git a/ui/src/dashboards/components/template_variables/RowValues.js b/ui/src/dashboards/components/template_variables/RowValues.js index dc77afb51d..bc0a915de0 100644 --- a/ui/src/dashboards/components/template_variables/RowValues.js +++ b/ui/src/dashboards/components/template_variables/RowValues.js @@ -20,7 +20,7 @@ const RowValues = ({ onStartEdit={onStartEdit} autoFocusTarget={autoFocusTarget} spellCheck={false} - autoComplete={false} + autoComplete="false" /> ) } diff --git a/ui/src/index.tsx b/ui/src/index.tsx index 10ffc7dec0..5f8572b98f 100644 --- a/ui/src/index.tsx +++ b/ui/src/index.tsx @@ -147,7 +147,7 @@ class Root extends PureComponent<{}, State> { component={KapacitorPage} /> - + diff --git a/ui/src/shared/components/SubSections.tsx b/ui/src/shared/components/SubSections.tsx new file mode 100644 index 0000000000..f6fe764226 --- /dev/null +++ b/ui/src/shared/components/SubSections.tsx @@ -0,0 +1,65 @@ +import React, {Component, ReactNode} from 'react' +import uuid from 'uuid' +import {withRouter, InjectedRouter} from 'react-router' + +import SubSectionsTab from 'src/shared/components/SubSectionsTab' +import {ErrorHandling} from 'src/shared/decorators/errors' +import {PageSection} from 'src/types/shared' + +interface Props { + sections: PageSection[] + activeSection: string + sourceID: string + router: InjectedRouter + parentUrl: string +} + +@ErrorHandling +class SubSections extends Component { + constructor(props) { + super(props) + } + + public render() { + const {sections, activeSection} = this.props + + return ( +
+
+
+ {sections.map( + section => + section.enabled && ( + + ) + )} +
+
+
+ {this.activeSectionComponent} +
+
+ ) + } + + private get activeSectionComponent(): ReactNode { + const {sections, activeSection} = this.props + const {component} = sections.find(section => section.url === activeSection) + return component + } + + public handleTabClick = url => () => { + const {router, sourceID, parentUrl} = this.props + router.push(`/sources/${sourceID}/${parentUrl}/${url}`) + } +} + +export default withRouter(SubSections) diff --git a/ui/src/shared/components/SubSectionsTab.tsx b/ui/src/shared/components/SubSectionsTab.tsx new file mode 100644 index 0000000000..399a46a41d --- /dev/null +++ b/ui/src/shared/components/SubSectionsTab.tsx @@ -0,0 +1,25 @@ +import React, {SFC} from 'react' +import {PageSection} from 'src/types/shared' + +interface TabProps { + handleClick: () => void + section: PageSection + activeSection: string +} + +const SubSectionsTab: SFC = ({ + handleClick, + section, + activeSection, +}) => ( +
+ {section.name} +
+) + +export default SubSectionsTab diff --git a/ui/src/side_nav/components/NavItems.tsx b/ui/src/side_nav/components/NavItems.tsx index 927f0ff59c..db51eeb4c9 100644 --- a/ui/src/side_nav/components/NavItems.tsx +++ b/ui/src/side_nav/components/NavItems.tsx @@ -1,6 +1,7 @@ import React, {PureComponent, SFC, ReactNode, ReactElement} from 'react' import {Link} from 'react-router' import classnames from 'classnames' +import _ from 'lodash' interface NavListItemProps { link: string @@ -62,17 +63,14 @@ interface NavBlockProps { icon: string location?: string className?: string - matcher?: string + highlightWhen: string[] } class NavBlock extends PureComponent { public render() { - const {location, className} = this.props - const isActive = React.Children.toArray(this.props.children).find( - (child: ReactElement) => { - return location.startsWith(child.props.link) // if location is undefined, this will fail silently - } - ) + const {location, className, highlightWhen} = this.props + const {length} = _.intersection(_.split(location, '/'), highlightWhen) + const isActive = !!length const children = React.Children.map( this.props.children, diff --git a/ui/src/side_nav/containers/SideNav.tsx b/ui/src/side_nav/containers/SideNav.tsx index dc17ddbb07..1f53c64a52 100644 --- a/ui/src/side_nav/containers/SideNav.tsx +++ b/ui/src/side_nav/containers/SideNav.tsx @@ -62,19 +62,26 @@ class SideNav extends PureComponent {
- + { { requiredRole={ADMIN_ROLE} replaceWithIfNotUsingAuth={ } > { Chronograf - + InfluxDB void } + +export interface PageSection { + url: string + name: string + component: ReactNode + enabled: boolean +} diff --git a/ui/test/shared/components/SubSections.test.tsx b/ui/test/shared/components/SubSections.test.tsx new file mode 100644 index 0000000000..229d4938c6 --- /dev/null +++ b/ui/test/shared/components/SubSections.test.tsx @@ -0,0 +1,86 @@ +import {shallow} from 'enzyme' +import React from 'react' +import SubSections from 'src/shared/components/SubSections' +import SubSectionsTab from 'src/shared/components/SubSectionsTab' + +const Guava = () => { + return
+} + +const Mango = () => { + return
+} + +const Pineapple = () => { + return
+} + +const guavaURL = 'guava' +const mangoURL = 'mango' +const pineappleURL = 'pineapple' + +const defaultProps = { + router: { + push: () => {}, + replace: () => {}, + go: () => {}, + goBack: () => {}, + goForward: () => {}, + setRouteLeaveHook: () => {}, + isActive: () => {}, + }, + sourceID: 'fruitstand', + parentUrl: 'fred-the-fruit-guy', + activeSection: guavaURL, + sections: [ + { + url: guavaURL, + name: 'Guava', + component: , + enabled: true, + }, + { + url: mangoURL, + name: 'Mango', + component: , + enabled: true, + }, + { + url: pineappleURL, + name: 'Pineapple', + component: , + enabled: false, + }, + ], +} + +const setup = (override?: {}) => { + const props = { + ...defaultProps, + ...override, + } + + return shallow() +} + +describe('SubSections', () => { + describe('render', () => { + it('renders the currently active tab', () => { + const wrapper = setup() + const content = wrapper.dive().find({'data-test': 'subsectionContent'}) + + expect(content.find(Guava).exists()).toBe(true) + }) + + it('only renders enabled tabs', () => { + const wrapper = setup() + const nav = wrapper.dive().find({'data-test': 'subsectionNav'}) + + const tabs = nav.find(SubSectionsTab) + + tabs.forEach(tab => { + expect(tab.exists()).toBe(tab.props().section.enabled) + }) + }) + }) +})