Merge pull request #3283 from influxdata/enhancement/generic-page-tabs
Generic Page Tabspull/10616/head
commit
1d594d8fcf
|
@ -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: <DatabaseManagerPage source={source} />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'Users',
|
|
||||||
component: (
|
|
||||||
<UsersTable
|
|
||||||
users={users}
|
|
||||||
allRoles={roles}
|
|
||||||
hasRoles={hasRoles}
|
|
||||||
permissions={permissions}
|
|
||||||
isEditing={isEditingUsers}
|
|
||||||
onSave={onSaveUser}
|
|
||||||
onCancel={onCancelEditUser}
|
|
||||||
onClickCreate={onClickCreate}
|
|
||||||
onEdit={onEditUser}
|
|
||||||
onDelete={onDeleteUser}
|
|
||||||
onFilter={onFilterUsers}
|
|
||||||
onUpdatePermissions={onUpdateUserPermissions}
|
|
||||||
onUpdateRoles={onUpdateUserRoles}
|
|
||||||
onUpdatePassword={onUpdateUserPassword}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'Roles',
|
|
||||||
component: (
|
|
||||||
<RolesTable
|
|
||||||
roles={roles}
|
|
||||||
allUsers={users}
|
|
||||||
permissions={permissions}
|
|
||||||
isEditing={isEditingRoles}
|
|
||||||
onClickCreate={onClickCreate}
|
|
||||||
onEdit={onEditRole}
|
|
||||||
onSave={onSaveRole}
|
|
||||||
onCancel={onCancelEditRole}
|
|
||||||
onDelete={onDeleteRole}
|
|
||||||
onFilter={onFilterRoles}
|
|
||||||
onUpdateRoleUsers={onUpdateRoleUsers}
|
|
||||||
onUpdateRolePermissions={onUpdateRolePermissions}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'Queries',
|
|
||||||
component: <QueriesPage source={source} />,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
if (!hasRoles) {
|
|
||||||
tabs = tabs.filter(t => t.type !== 'Roles')
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tabs className="row">
|
|
||||||
<TabList customClass="col-md-2 admin-tabs">
|
|
||||||
{tabs.map((t, i) => <Tab key={tabs[i].type}>{tabs[i].type}</Tab>)}
|
|
||||||
</TabList>
|
|
||||||
<TabPanels customClass="col-md-10 admin-tabs--content">
|
|
||||||
{tabs.map((t, i) => (
|
|
||||||
<TabPanel key={tabs[i].type}>{t.component}</TabPanel>
|
|
||||||
))}
|
|
||||||
</TabPanels>
|
|
||||||
</Tabs>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
|
@ -150,7 +150,7 @@ class DatabaseRow extends Component {
|
||||||
ref={r => (this.name = r)}
|
ref={r => (this.name = r)}
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
autoComplete={false}
|
autoComplete="false"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
name
|
name
|
||||||
|
@ -167,7 +167,7 @@ class DatabaseRow extends Component {
|
||||||
ref={r => (this.duration = r)}
|
ref={r => (this.duration = r)}
|
||||||
autoFocus={!isNew}
|
autoFocus={!isNew}
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
autoComplete={false}
|
autoComplete="false"
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
{isRFDisplayed ? (
|
{isRFDisplayed ? (
|
||||||
|
@ -182,7 +182,7 @@ class DatabaseRow extends Component {
|
||||||
onKeyDown={this.handleKeyDown}
|
onKeyDown={this.handleKeyDown}
|
||||||
ref={r => (this.replication = r)}
|
ref={r => (this.replication = r)}
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
autoComplete={false}
|
autoComplete="false"
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
|
@ -98,7 +98,7 @@ const Header = ({
|
||||||
onChange={onDatabaseDeleteConfirm(database)}
|
onChange={onDatabaseDeleteConfirm(database)}
|
||||||
onKeyDown={onDatabaseDeleteConfirm(database)}
|
onKeyDown={onDatabaseDeleteConfirm(database)}
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
autoComplete={false}
|
autoComplete="false"
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
/>
|
/>
|
||||||
<ConfirmOrCancel
|
<ConfirmOrCancel
|
||||||
|
@ -130,7 +130,7 @@ const EditHeader = ({database, onEdit, onKeyDown, onConfirm, onCancel}) => (
|
||||||
onKeyDown={onKeyDown(database)}
|
onKeyDown={onKeyDown(database)}
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
autoComplete={false}
|
autoComplete="false"
|
||||||
/>
|
/>
|
||||||
<ConfirmOrCancel
|
<ConfirmOrCancel
|
||||||
item={database}
|
item={database}
|
||||||
|
|
|
@ -38,7 +38,7 @@ class RoleEditingRow extends Component {
|
||||||
onKeyPress={this.handleKeyPress(role)}
|
onKeyPress={this.handleKeyPress(role)}
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
autoComplete={false}
|
autoComplete="false"
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
)
|
)
|
||||||
|
|
|
@ -38,7 +38,7 @@ class UserEditName extends Component {
|
||||||
onKeyPress={this.handleKeyPress(user)}
|
onKeyPress={this.handleKeyPress(user)}
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
autoComplete={false}
|
autoComplete="false"
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
)
|
)
|
||||||
|
|
|
@ -34,7 +34,7 @@ class UserNewPassword extends Component {
|
||||||
onChange={this.handleEdit(user)}
|
onChange={this.handleEdit(user)}
|
||||||
onKeyPress={this.handleKeyPress(user)}
|
onKeyPress={this.handleKeyPress(user)}
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
autoComplete={false}
|
autoComplete="false"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
'--'
|
'--'
|
||||||
|
|
|
@ -25,9 +25,13 @@ import {
|
||||||
filterRoles as filterRolesAction,
|
filterRoles as filterRolesAction,
|
||||||
} from 'src/admin/actions/influxdb'
|
} 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 SourceIndicator from 'shared/components/SourceIndicator'
|
||||||
import FancyScrollbar from 'shared/components/FancyScrollbar'
|
import FancyScrollbar from 'shared/components/FancyScrollbar'
|
||||||
|
import SubSections from 'shared/components/SubSections'
|
||||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||||
|
|
||||||
import {notify as notifyAction} from 'shared/actions/notifications'
|
import {notify as notifyAction} from 'shared/actions/notifications'
|
||||||
|
@ -141,7 +145,7 @@ class AdminInfluxDBPage extends Component {
|
||||||
this.props.updateUserPassword(user, password)
|
this.props.updateUserPassword(user, password)
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
getAdminSubSections = () => {
|
||||||
const {
|
const {
|
||||||
users,
|
users,
|
||||||
roles,
|
roles,
|
||||||
|
@ -154,6 +158,69 @@ class AdminInfluxDBPage extends Component {
|
||||||
const globalPermissions = permissions.find(p => p.scope === 'all')
|
const globalPermissions = permissions.find(p => p.scope === 'all')
|
||||||
const allowed = globalPermissions ? globalPermissions.allowed : []
|
const allowed = globalPermissions ? globalPermissions.allowed : []
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
url: 'databases',
|
||||||
|
name: 'Databases',
|
||||||
|
enabled: true,
|
||||||
|
component: <DatabaseManagerPage source={source} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: 'users',
|
||||||
|
name: 'Users',
|
||||||
|
enabled: true,
|
||||||
|
component: (
|
||||||
|
<UsersTable
|
||||||
|
users={users}
|
||||||
|
allRoles={roles}
|
||||||
|
hasRoles={hasRoles}
|
||||||
|
permissions={allowed}
|
||||||
|
isEditing={users.some(u => 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: (
|
||||||
|
<RolesTable
|
||||||
|
roles={roles}
|
||||||
|
allUsers={users}
|
||||||
|
permissions={allowed}
|
||||||
|
isEditing={roles.some(r => 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: <QueriesPage source={source} />,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {users, source, params} = this.props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="page">
|
<div className="page">
|
||||||
<div className="page-header">
|
<div className="page-header">
|
||||||
|
@ -169,33 +236,12 @@ class AdminInfluxDBPage extends Component {
|
||||||
<FancyScrollbar className="page-contents">
|
<FancyScrollbar className="page-contents">
|
||||||
{users ? (
|
{users ? (
|
||||||
<div className="container-fluid">
|
<div className="container-fluid">
|
||||||
<div className="row">
|
<SubSections
|
||||||
<AdminTabs
|
parentUrl="admin-influxdb"
|
||||||
users={users}
|
sourceID={source.id}
|
||||||
roles={roles}
|
activeSection={params.tab}
|
||||||
source={source}
|
sections={this.getAdminSubSections()}
|
||||||
hasRoles={hasRoles}
|
/>
|
||||||
permissions={allowed}
|
|
||||||
onFilterUsers={filterUsers}
|
|
||||||
onFilterRoles={filterRoles}
|
|
||||||
onEditUser={this.handleEditUser}
|
|
||||||
onEditRole={this.handleEditRole}
|
|
||||||
onSaveUser={this.handleSaveUser}
|
|
||||||
onSaveRole={this.handleSaveRole}
|
|
||||||
onDeleteUser={this.handleDeleteUser}
|
|
||||||
onDeleteRole={this.handleDeleteRole}
|
|
||||||
onClickCreate={this.handleClickCreate}
|
|
||||||
onCancelEditUser={this.handleCancelEditUser}
|
|
||||||
onCancelEditRole={this.handleCancelEditRole}
|
|
||||||
isEditingUsers={users.some(u => u.isEditing)}
|
|
||||||
isEditingRoles={roles.some(r => r.isEditing)}
|
|
||||||
onUpdateRoleUsers={this.handleUpdateRoleUsers}
|
|
||||||
onUpdateUserRoles={this.handleUpdateUserRoles}
|
|
||||||
onUpdateUserPassword={this.handleUpdateUserPassword}
|
|
||||||
onUpdateRolePermissions={this.handleUpdateRolePermissions}
|
|
||||||
onUpdateUserPermissions={this.handleUpdateUserPermissions}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="page-spinner" />
|
<div className="page-spinner" />
|
||||||
|
@ -239,6 +285,9 @@ AdminInfluxDBPage.propTypes = {
|
||||||
updateUserRoles: func,
|
updateUserRoles: func,
|
||||||
updateUserPassword: func,
|
updateUserPassword: func,
|
||||||
notify: func.isRequired,
|
notify: func.isRequired,
|
||||||
|
params: shape({
|
||||||
|
tab: string,
|
||||||
|
}).isRequired,
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapStateToProps = ({adminInfluxDB: {users, roles, permissions}}) => ({
|
const mapStateToProps = ({adminInfluxDB: {users, roles, permissions}}) => ({
|
||||||
|
|
|
@ -20,7 +20,7 @@ const RowValues = ({
|
||||||
onStartEdit={onStartEdit}
|
onStartEdit={onStartEdit}
|
||||||
autoFocusTarget={autoFocusTarget}
|
autoFocusTarget={autoFocusTarget}
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
autoComplete={false}
|
autoComplete="false"
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -147,7 +147,7 @@ class Root extends PureComponent<{}, State> {
|
||||||
component={KapacitorPage}
|
component={KapacitorPage}
|
||||||
/>
|
/>
|
||||||
<Route path="admin-chronograf" component={AdminChronografPage} />
|
<Route path="admin-chronograf" component={AdminChronografPage} />
|
||||||
<Route path="admin-influxdb" component={AdminInfluxDBPage} />
|
<Route path="admin-influxdb/:tab" component={AdminInfluxDBPage} />
|
||||||
<Route path="manage-sources" component={ManageSources} />
|
<Route path="manage-sources" component={ManageSources} />
|
||||||
<Route path="manage-sources/new" component={SourcePage} />
|
<Route path="manage-sources/new" component={SourcePage} />
|
||||||
<Route path="manage-sources/:id/edit" component={SourcePage} />
|
<Route path="manage-sources/:id/edit" component={SourcePage} />
|
||||||
|
|
|
@ -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<Props> {
|
||||||
|
constructor(props) {
|
||||||
|
super(props)
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
const {sections, activeSection} = this.props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="row subsection">
|
||||||
|
<div className="col-md-2 subsection--nav" data-test="subsectionNav">
|
||||||
|
<div className="subsection--tabs">
|
||||||
|
{sections.map(
|
||||||
|
section =>
|
||||||
|
section.enabled && (
|
||||||
|
<SubSectionsTab
|
||||||
|
key={uuid.v4()}
|
||||||
|
section={section}
|
||||||
|
handleClick={this.handleTabClick(section.url)}
|
||||||
|
activeSection={activeSection}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="col-md-10 subsection--content"
|
||||||
|
data-test="subsectionContent"
|
||||||
|
>
|
||||||
|
{this.activeSectionComponent}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
|
@ -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<TabProps> = ({
|
||||||
|
handleClick,
|
||||||
|
section,
|
||||||
|
activeSection,
|
||||||
|
}) => (
|
||||||
|
<div
|
||||||
|
className={`subsection--tab ${
|
||||||
|
section.url === activeSection ? 'active' : ''
|
||||||
|
}`}
|
||||||
|
onClick={handleClick}
|
||||||
|
>
|
||||||
|
{section.name}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default SubSectionsTab
|
|
@ -1,6 +1,7 @@
|
||||||
import React, {PureComponent, SFC, ReactNode, ReactElement} from 'react'
|
import React, {PureComponent, SFC, ReactNode, ReactElement} from 'react'
|
||||||
import {Link} from 'react-router'
|
import {Link} from 'react-router'
|
||||||
import classnames from 'classnames'
|
import classnames from 'classnames'
|
||||||
|
import _ from 'lodash'
|
||||||
|
|
||||||
interface NavListItemProps {
|
interface NavListItemProps {
|
||||||
link: string
|
link: string
|
||||||
|
@ -62,17 +63,14 @@ interface NavBlockProps {
|
||||||
icon: string
|
icon: string
|
||||||
location?: string
|
location?: string
|
||||||
className?: string
|
className?: string
|
||||||
matcher?: string
|
highlightWhen: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
class NavBlock extends PureComponent<NavBlockProps> {
|
class NavBlock extends PureComponent<NavBlockProps> {
|
||||||
public render() {
|
public render() {
|
||||||
const {location, className} = this.props
|
const {location, className, highlightWhen} = this.props
|
||||||
const isActive = React.Children.toArray(this.props.children).find(
|
const {length} = _.intersection(_.split(location, '/'), highlightWhen)
|
||||||
(child: ReactElement<any>) => {
|
const isActive = !!length
|
||||||
return location.startsWith(child.props.link) // if location is undefined, this will fail silently
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const children = React.Children.map(
|
const children = React.Children.map(
|
||||||
this.props.children,
|
this.props.children,
|
||||||
|
|
|
@ -62,19 +62,26 @@ class SideNav extends PureComponent<Props> {
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<NavBlock
|
<NavBlock
|
||||||
|
highlightWhen={['hosts']}
|
||||||
icon="cubo-node"
|
icon="cubo-node"
|
||||||
link={`${sourcePrefix}/hosts`}
|
link={`${sourcePrefix}/hosts`}
|
||||||
location={location}
|
location={location}
|
||||||
>
|
>
|
||||||
<NavHeader link={`${sourcePrefix}/hosts`} title="Host List" />
|
<NavHeader link={`${sourcePrefix}/hosts`} title="Host List" />
|
||||||
</NavBlock>
|
</NavBlock>
|
||||||
<NavBlock icon="graphline" link={dataExplorerLink} location={location}>
|
<NavBlock
|
||||||
|
highlightWhen={['data-explorer', 'delorean']}
|
||||||
|
icon="graphline"
|
||||||
|
link={dataExplorerLink}
|
||||||
|
location={location}
|
||||||
|
>
|
||||||
<NavHeader link={dataExplorerLink} title="Data Explorer" />
|
<NavHeader link={dataExplorerLink} title="Data Explorer" />
|
||||||
<FeatureFlag name="time-machine">
|
<FeatureFlag name="time-machine">
|
||||||
<NavHeader link={`${sourcePrefix}/delorean`} title="Time Machine" />
|
<NavHeader link={`${sourcePrefix}/delorean`} title="Time Machine" />
|
||||||
</FeatureFlag>
|
</FeatureFlag>
|
||||||
</NavBlock>
|
</NavBlock>
|
||||||
<NavBlock
|
<NavBlock
|
||||||
|
highlightWhen={['dashboards']}
|
||||||
icon="dash-h"
|
icon="dash-h"
|
||||||
link={`${sourcePrefix}/dashboards`}
|
link={`${sourcePrefix}/dashboards`}
|
||||||
location={location}
|
location={location}
|
||||||
|
@ -82,7 +89,7 @@ class SideNav extends PureComponent<Props> {
|
||||||
<NavHeader link={`${sourcePrefix}/dashboards`} title="Dashboards" />
|
<NavHeader link={`${sourcePrefix}/dashboards`} title="Dashboards" />
|
||||||
</NavBlock>
|
</NavBlock>
|
||||||
<NavBlock
|
<NavBlock
|
||||||
matcher="alerts"
|
highlightWhen={['alerts', 'alert-rules', 'tickscript']}
|
||||||
icon="alert-triangle"
|
icon="alert-triangle"
|
||||||
link={`${sourcePrefix}/alert-rules`}
|
link={`${sourcePrefix}/alert-rules`}
|
||||||
location={location}
|
location={location}
|
||||||
|
@ -100,18 +107,20 @@ class SideNav extends PureComponent<Props> {
|
||||||
requiredRole={ADMIN_ROLE}
|
requiredRole={ADMIN_ROLE}
|
||||||
replaceWithIfNotUsingAuth={
|
replaceWithIfNotUsingAuth={
|
||||||
<NavBlock
|
<NavBlock
|
||||||
|
highlightWhen={['admin-influxdb']}
|
||||||
icon="crown2"
|
icon="crown2"
|
||||||
link={`${sourcePrefix}/admin-influxdb`}
|
link={`${sourcePrefix}/admin-influxdb/databases`}
|
||||||
location={location}
|
location={location}
|
||||||
>
|
>
|
||||||
<NavHeader
|
<NavHeader
|
||||||
link={`${sourcePrefix}/admin-influxdb`}
|
link={`${sourcePrefix}/admin-influxdb/databases`}
|
||||||
title="InfluxDB Admin"
|
title="InfluxDB Admin"
|
||||||
/>
|
/>
|
||||||
</NavBlock>
|
</NavBlock>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<NavBlock
|
<NavBlock
|
||||||
|
highlightWhen={['admin-chronograf', 'admin-influxdb']}
|
||||||
icon="crown2"
|
icon="crown2"
|
||||||
link={`${sourcePrefix}/admin-chronograf`}
|
link={`${sourcePrefix}/admin-chronograf`}
|
||||||
location={location}
|
location={location}
|
||||||
|
@ -123,12 +132,13 @@ class SideNav extends PureComponent<Props> {
|
||||||
<NavListItem link={`${sourcePrefix}/admin-chronograf`}>
|
<NavListItem link={`${sourcePrefix}/admin-chronograf`}>
|
||||||
Chronograf
|
Chronograf
|
||||||
</NavListItem>
|
</NavListItem>
|
||||||
<NavListItem link={`${sourcePrefix}/admin-influxdb`}>
|
<NavListItem link={`${sourcePrefix}/admin-influxdb/databases`}>
|
||||||
InfluxDB
|
InfluxDB
|
||||||
</NavListItem>
|
</NavListItem>
|
||||||
</NavBlock>
|
</NavBlock>
|
||||||
</Authorized>
|
</Authorized>
|
||||||
<NavBlock
|
<NavBlock
|
||||||
|
highlightWhen={['manage-sources', 'kapacitors']}
|
||||||
icon="cog-thick"
|
icon="cog-thick"
|
||||||
link={`${sourcePrefix}/manage-sources`}
|
link={`${sourcePrefix}/manage-sources`}
|
||||||
location={location}
|
location={location}
|
||||||
|
|
|
@ -27,6 +27,7 @@
|
||||||
// Layout
|
// Layout
|
||||||
@import 'layout/page';
|
@import 'layout/page';
|
||||||
@import 'layout/page-header';
|
@import 'layout/page-header';
|
||||||
|
@import 'layout/page-subsections';
|
||||||
@import 'layout/sidebar';
|
@import 'layout/sidebar';
|
||||||
@import 'layout/overlay';
|
@import 'layout/overlay';
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
/*
|
||||||
|
Page Sub-Sections
|
||||||
|
----------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
$subsection-font: 17px;
|
||||||
|
|
||||||
|
.subsection {
|
||||||
|
.panel {
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
}
|
||||||
|
.panel-heading {
|
||||||
|
height: 60px;
|
||||||
|
padding-top: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
.panel-heading + .panel-body {
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
.panel-body {
|
||||||
|
min-height: 500px;
|
||||||
|
}
|
||||||
|
.panel-title {
|
||||||
|
font-size: $subsection-font;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.subsection--tabs {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subsection--tab {
|
||||||
|
border-radius: $radius 0 0 $radius;
|
||||||
|
padding: 0 8px 0 16px;
|
||||||
|
height: $chronograf-page-header-height;
|
||||||
|
white-space: nowrap;
|
||||||
|
line-height: $chronograf-page-header-height;
|
||||||
|
text-align: left;
|
||||||
|
font-size: $subsection-font;
|
||||||
|
font-weight: 500;
|
||||||
|
color: $g11-sidewalk;
|
||||||
|
@include no-user-select();
|
||||||
|
transition: background-color 0.25s ease, color 0.25s ease;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&.active {
|
||||||
|
cursor: pointer;
|
||||||
|
color: $g18-cloud;
|
||||||
|
background-color: $g3-castle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: $grid--breakpoint-md) {
|
||||||
|
.subsection {
|
||||||
|
.subsection--nav {
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
.subsection--content {
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,84 +3,6 @@
|
||||||
----------------------------------------------------------------------------
|
----------------------------------------------------------------------------
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/*
|
|
||||||
Admin Tabs
|
|
||||||
----------------------------------------------------------------------------
|
|
||||||
*/
|
|
||||||
.admin-tabs .btn-group {
|
|
||||||
margin: 0;
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
align-items: stretch;
|
|
||||||
|
|
||||||
.tab {
|
|
||||||
font-weight: 500 !important;
|
|
||||||
border-radius: $radius $radius 0 0 !important;
|
|
||||||
transition: background-color 0.25s ease, color 0.25s ease !important;
|
|
||||||
border: 0 !important;
|
|
||||||
text-align: left;
|
|
||||||
height: 60px !important;
|
|
||||||
line-height: 60px !important;
|
|
||||||
padding: 0 30px !important;
|
|
||||||
font-size: 17px;
|
|
||||||
background-color: transparent !important;
|
|
||||||
color: $g11-sidewalk !important;
|
|
||||||
|
|
||||||
&:hover,
|
|
||||||
&:active,
|
|
||||||
&:active:hover {
|
|
||||||
background-color: $g3-castle !important;
|
|
||||||
color: $g15-platinum !important;
|
|
||||||
}
|
|
||||||
&.active {
|
|
||||||
background-color: $g3-castle !important;
|
|
||||||
color: $g18-cloud !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.admin-tabs--content {
|
|
||||||
.panel {
|
|
||||||
border-top-left-radius: 0;
|
|
||||||
}
|
|
||||||
.panel-heading {
|
|
||||||
height: 60px;
|
|
||||||
}
|
|
||||||
.panel-title {
|
|
||||||
font-size: 17px;
|
|
||||||
font-weight: 400 !important;
|
|
||||||
color: $g12-forge;
|
|
||||||
padding: 6px 0;
|
|
||||||
}
|
|
||||||
.panel-body {
|
|
||||||
min-height: 300px;
|
|
||||||
}
|
|
||||||
.panel-heading + .panel-body {
|
|
||||||
padding-top: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Responsive Layout for Admin Tabs on Small Screens
|
|
||||||
----------------------------------------------------------------------------
|
|
||||||
*/
|
|
||||||
|
|
||||||
@media screen and (min-width: 992px) {
|
|
||||||
.admin-tabs {
|
|
||||||
padding-right: 0;
|
|
||||||
|
|
||||||
.btn-group {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
.btn-group .tab {
|
|
||||||
border-radius: $radius 0 0 $radius !important;
|
|
||||||
padding: 0 0 0 16px !important;
|
|
||||||
}
|
|
||||||
& + div {
|
|
||||||
padding-left: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Admin Table
|
Admin Table
|
||||||
----------------------------------------------------------------------------
|
----------------------------------------------------------------------------
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import {ReactNode} from 'react'
|
||||||
|
|
||||||
export type DropdownItem =
|
export type DropdownItem =
|
||||||
| {
|
| {
|
||||||
text: string
|
text: string
|
||||||
|
@ -9,3 +11,10 @@ export interface DropdownAction {
|
||||||
text: string
|
text: string
|
||||||
handler: () => void
|
handler: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PageSection {
|
||||||
|
url: string
|
||||||
|
name: string
|
||||||
|
component: ReactNode
|
||||||
|
enabled: boolean
|
||||||
|
}
|
||||||
|
|
|
@ -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 <div />
|
||||||
|
}
|
||||||
|
|
||||||
|
const Mango = () => {
|
||||||
|
return <div />
|
||||||
|
}
|
||||||
|
|
||||||
|
const Pineapple = () => {
|
||||||
|
return <div />
|
||||||
|
}
|
||||||
|
|
||||||
|
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: <Guava />,
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: mangoURL,
|
||||||
|
name: 'Mango',
|
||||||
|
component: <Mango />,
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: pineappleURL,
|
||||||
|
name: 'Pineapple',
|
||||||
|
component: <Pineapple />,
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
const setup = (override?: {}) => {
|
||||||
|
const props = {
|
||||||
|
...defaultProps,
|
||||||
|
...override,
|
||||||
|
}
|
||||||
|
|
||||||
|
return shallow(<SubSections {...props} />)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
Loading…
Reference in New Issue