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)}
|
||||
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"
|
||||
/>
|
||||
</td>
|
||||
{isRFDisplayed ? (
|
||||
|
@ -182,7 +182,7 @@ class DatabaseRow extends Component {
|
|||
onKeyDown={this.handleKeyDown}
|
||||
ref={r => (this.replication = r)}
|
||||
spellCheck={false}
|
||||
autoComplete={false}
|
||||
autoComplete="false"
|
||||
/>
|
||||
</td>
|
||||
) : null}
|
||||
|
|
|
@ -98,7 +98,7 @@ const Header = ({
|
|||
onChange={onDatabaseDeleteConfirm(database)}
|
||||
onKeyDown={onDatabaseDeleteConfirm(database)}
|
||||
autoFocus={true}
|
||||
autoComplete={false}
|
||||
autoComplete="false"
|
||||
spellCheck={false}
|
||||
/>
|
||||
<ConfirmOrCancel
|
||||
|
@ -130,7 +130,7 @@ const EditHeader = ({database, onEdit, onKeyDown, onConfirm, onCancel}) => (
|
|||
onKeyDown={onKeyDown(database)}
|
||||
autoFocus={true}
|
||||
spellCheck={false}
|
||||
autoComplete={false}
|
||||
autoComplete="false"
|
||||
/>
|
||||
<ConfirmOrCancel
|
||||
item={database}
|
||||
|
|
|
@ -38,7 +38,7 @@ class RoleEditingRow extends Component {
|
|||
onKeyPress={this.handleKeyPress(role)}
|
||||
autoFocus={true}
|
||||
spellCheck={false}
|
||||
autoComplete={false}
|
||||
autoComplete="false"
|
||||
/>
|
||||
</td>
|
||||
)
|
||||
|
|
|
@ -38,7 +38,7 @@ class UserEditName extends Component {
|
|||
onKeyPress={this.handleKeyPress(user)}
|
||||
autoFocus={true}
|
||||
spellCheck={false}
|
||||
autoComplete={false}
|
||||
autoComplete="false"
|
||||
/>
|
||||
</td>
|
||||
)
|
||||
|
|
|
@ -34,7 +34,7 @@ class UserNewPassword extends Component {
|
|||
onChange={this.handleEdit(user)}
|
||||
onKeyPress={this.handleKeyPress(user)}
|
||||
spellCheck={false}
|
||||
autoComplete={false}
|
||||
autoComplete="false"
|
||||
/>
|
||||
) : (
|
||||
'--'
|
||||
|
|
|
@ -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: <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 (
|
||||
<div className="page">
|
||||
<div className="page-header">
|
||||
|
@ -169,33 +236,12 @@ class AdminInfluxDBPage extends Component {
|
|||
<FancyScrollbar className="page-contents">
|
||||
{users ? (
|
||||
<div className="container-fluid">
|
||||
<div className="row">
|
||||
<AdminTabs
|
||||
users={users}
|
||||
roles={roles}
|
||||
source={source}
|
||||
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>
|
||||
<SubSections
|
||||
parentUrl="admin-influxdb"
|
||||
sourceID={source.id}
|
||||
activeSection={params.tab}
|
||||
sections={this.getAdminSubSections()}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="page-spinner" />
|
||||
|
@ -239,6 +285,9 @@ AdminInfluxDBPage.propTypes = {
|
|||
updateUserRoles: func,
|
||||
updateUserPassword: func,
|
||||
notify: func.isRequired,
|
||||
params: shape({
|
||||
tab: string,
|
||||
}).isRequired,
|
||||
}
|
||||
|
||||
const mapStateToProps = ({adminInfluxDB: {users, roles, permissions}}) => ({
|
||||
|
|
|
@ -20,7 +20,7 @@ const RowValues = ({
|
|||
onStartEdit={onStartEdit}
|
||||
autoFocusTarget={autoFocusTarget}
|
||||
spellCheck={false}
|
||||
autoComplete={false}
|
||||
autoComplete="false"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -147,7 +147,7 @@ class Root extends PureComponent<{}, State> {
|
|||
component={KapacitorPage}
|
||||
/>
|
||||
<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/new" 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 {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<NavBlockProps> {
|
||||
public render() {
|
||||
const {location, className} = this.props
|
||||
const isActive = React.Children.toArray(this.props.children).find(
|
||||
(child: ReactElement<any>) => {
|
||||
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,
|
||||
|
|
|
@ -62,19 +62,26 @@ class SideNav extends PureComponent<Props> {
|
|||
</Link>
|
||||
</div>
|
||||
<NavBlock
|
||||
highlightWhen={['hosts']}
|
||||
icon="cubo-node"
|
||||
link={`${sourcePrefix}/hosts`}
|
||||
location={location}
|
||||
>
|
||||
<NavHeader link={`${sourcePrefix}/hosts`} title="Host List" />
|
||||
</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" />
|
||||
<FeatureFlag name="time-machine">
|
||||
<NavHeader link={`${sourcePrefix}/delorean`} title="Time Machine" />
|
||||
</FeatureFlag>
|
||||
</NavBlock>
|
||||
<NavBlock
|
||||
highlightWhen={['dashboards']}
|
||||
icon="dash-h"
|
||||
link={`${sourcePrefix}/dashboards`}
|
||||
location={location}
|
||||
|
@ -82,7 +89,7 @@ class SideNav extends PureComponent<Props> {
|
|||
<NavHeader link={`${sourcePrefix}/dashboards`} title="Dashboards" />
|
||||
</NavBlock>
|
||||
<NavBlock
|
||||
matcher="alerts"
|
||||
highlightWhen={['alerts', 'alert-rules', 'tickscript']}
|
||||
icon="alert-triangle"
|
||||
link={`${sourcePrefix}/alert-rules`}
|
||||
location={location}
|
||||
|
@ -100,18 +107,20 @@ class SideNav extends PureComponent<Props> {
|
|||
requiredRole={ADMIN_ROLE}
|
||||
replaceWithIfNotUsingAuth={
|
||||
<NavBlock
|
||||
highlightWhen={['admin-influxdb']}
|
||||
icon="crown2"
|
||||
link={`${sourcePrefix}/admin-influxdb`}
|
||||
link={`${sourcePrefix}/admin-influxdb/databases`}
|
||||
location={location}
|
||||
>
|
||||
<NavHeader
|
||||
link={`${sourcePrefix}/admin-influxdb`}
|
||||
link={`${sourcePrefix}/admin-influxdb/databases`}
|
||||
title="InfluxDB Admin"
|
||||
/>
|
||||
</NavBlock>
|
||||
}
|
||||
>
|
||||
<NavBlock
|
||||
highlightWhen={['admin-chronograf', 'admin-influxdb']}
|
||||
icon="crown2"
|
||||
link={`${sourcePrefix}/admin-chronograf`}
|
||||
location={location}
|
||||
|
@ -123,12 +132,13 @@ class SideNav extends PureComponent<Props> {
|
|||
<NavListItem link={`${sourcePrefix}/admin-chronograf`}>
|
||||
Chronograf
|
||||
</NavListItem>
|
||||
<NavListItem link={`${sourcePrefix}/admin-influxdb`}>
|
||||
<NavListItem link={`${sourcePrefix}/admin-influxdb/databases`}>
|
||||
InfluxDB
|
||||
</NavListItem>
|
||||
</NavBlock>
|
||||
</Authorized>
|
||||
<NavBlock
|
||||
highlightWhen={['manage-sources', 'kapacitors']}
|
||||
icon="cog-thick"
|
||||
link={`${sourcePrefix}/manage-sources`}
|
||||
location={location}
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
// Layout
|
||||
@import 'layout/page';
|
||||
@import 'layout/page-header';
|
||||
@import 'layout/page-subsections';
|
||||
@import 'layout/sidebar';
|
||||
@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
|
||||
----------------------------------------------------------------------------
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import {ReactNode} from 'react'
|
||||
|
||||
export type DropdownItem =
|
||||
| {
|
||||
text: string
|
||||
|
@ -9,3 +11,10 @@ export interface DropdownAction {
|
|||
text: string
|
||||
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