feat(ui): implement new navigation design (#17206)

* refactor(ui): WIP implement tree nav

* feat(ui): enable expand/collapse behavior of nav tree

* chore(ui): rename "settings" to "organization"

* fix(ui): add nav tree key to mock state

* refactor(ui): remove CloudNav component

* fix(ui): cleanup

* refactor(ui): move nav tree object to constants file

* fix(ui): use correct icon for data nav item

* fix(ui): repair affected test

* refactor(ui): use a more explicit type than boolean for nav state

* chore(ui): write test for nav bar state action

* refactor(ui): handle org switching with an overlay

* chore(ui): replace reference to local clockface with reference to package

* fix(ui): update nav selector in tasks e2e test

* chore(ui): add testids to all nav items for easier selection

* fix(ui): prevent expanded navbar from breaking e2e tests

* fix(ui): update broken login e2e test

* fix(ui): udpate selectors in query builder test

* refactor(ui): align nav structure with quartz counterpart

* fix(ui): prettier

* refactor(ui): move usage and billing into user widget

* refactor(ui): use correct url for usage and billing

* chore(ui): upgrade clockface dependency to 2.0.3

* refactor(ui): implement short labels in the navigation tree

* refactor(ui): wrap tree nav in feature flag

* chore(ui): prettier

* chore(ui): remove deleted file accidentally included in rebase

* refactor(ui): use get in TreeNav mstp to prevent potential future breakage

* chore(ui): set default values for treeNav feature flag

* refactor(ui): reinstate cloud nav but wrapped with a feature flag

* refactor(ui): reinstate old navbar wrapped in feature flag

* feat(ui): add Upgrade button to all page headers except DashboardPage

* feat(ui): add upgrade banner to treenav

* chore(ui): remove comments

* refactor(ui): polish upgrade banner

* refactor(ui): only show tiny banner ad if screen is large enough and menu is collapsed

* refactor(ui): ensure settings page supports old and new navigation

* chore(ui): cleanup

* chore(ui): revert comment change

* chore(ui): ensure custom classname exists on new upgrade button

* chore(ui): cleanup

* fix(ui): remove duplicate route

* refactor(ui): make const for repeated comparison

* refactor(ui): cleanup from pr review

* fix(ui): oops

* refactor(ui): update user widget to follow most recent logout approach

Co-Authored-By: Ariel Salem <ariel.salem1989@gmail.com>

Co-authored-by: Ariel Salem <ariel.salem1989@gmail.com>
pull/17424/head
alexpaxton 2020-03-25 13:57:24 -07:00 committed by GitHub
parent 36d99bfbf8
commit f1e2dad6d6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
48 changed files with 1284 additions and 82 deletions

View File

@ -45,7 +45,7 @@ describe('The Query Builder', () => {
cy.get('.giraffe-plot').should('exist')
cy.contains('Save As').click()
cy.getByTestID('save-query-as').click()
// open the dashboard selector dropdown
cy.getByTestID('save-as-dashboard-cell--dropdown').click()

View File

@ -326,6 +326,7 @@ http.post(
.click()
// verify that it is the correct data
cy.getByInputValue(secondTask)
cy.get('div.cf-nav--item.active').click()
// navigate back to the first one to verify that the name is correct
cy.getByTestID('task-card--name')

View File

@ -6,11 +6,13 @@ import classnames from 'classnames'
// Components
import {AppWrapper} from '@influxdata/clockface'
import Nav from 'src/pageLayout'
import TreeNav from 'src/pageLayout/containers/TreeNav'
import Nav from 'src/pageLayout/containers/Nav'
import TooltipPortal from 'src/portals/TooltipPortal'
import NotesPortal from 'src/portals/NotesPortal'
import Notifications from 'src/shared/components/notifications/Notifications'
import OverlayController from 'src/overlays/components/OverlayController'
import {FeatureFlag} from 'src/shared/utils/featureFlag'
import CloudNav from 'src/pageLayout/components/CloudNav'
import CloudOnly from 'src/shared/components/cloud/CloudOnly'
@ -40,9 +42,11 @@ const App: SFC<Props> = ({
return (
<>
<CloudOnly>
<CloudNav />
</CloudOnly>
<FeatureFlag name="treeNav" equals={false}>
<CloudOnly>
<CloudNav />
</CloudOnly>
</FeatureFlag>
<AppWrapper
presentationMode={inPresentationMode}
className={appWrapperClass}
@ -51,7 +55,12 @@ const App: SFC<Props> = ({
<TooltipPortal />
<NotesPortal />
<OverlayController />
<Nav />
<FeatureFlag name="treeNav">
<TreeNav />
</FeatureFlag>
<FeatureFlag name="treeNav" equals={false}>
<Nav />
</FeatureFlag>
{children}
</AppWrapper>
</>

View File

@ -9,6 +9,7 @@ import EventTable from 'src/eventViewer/components/EventTable'
import AlertHistoryControls from 'src/alerting/components/AlertHistoryControls'
import AlertHistoryQueryParams from 'src/alerting/components/AlertHistoryQueryParams'
import GetResources from 'src/resources/components/GetResources'
import CloudUpgradeButton from 'src/shared/components/CloudUpgradeButton'
// Constants
import {
@ -77,6 +78,7 @@ const AlertHistoryIndex: FC<Props> = ({params: {orgID}, resourceIDs}) => {
title="Check Statuses"
testID="alert-history-title"
/>
<CloudUpgradeButton />
</Page.Header>
<Page.ControlBar fullWidth={true}>
<AlertHistoryQueryParams

View File

@ -10,6 +10,7 @@ import EndpointsColumn from 'src/notifications/endpoints/components/EndpointsCol
import GetAssetLimits from 'src/cloud/components/GetAssetLimits'
import AssetLimitAlert from 'src/cloud/components/AssetLimitAlert'
import GetResources from 'src/resources/components/GetResources'
import CloudUpgradeButton from 'src/shared/components/CloudUpgradeButton'
// Utils
import {pageTitleSuffixer} from 'src/shared/utils/pageTitles'
@ -34,9 +35,10 @@ const AlertingIndex: FunctionComponent<StateProps> = ({
}) => {
return (
<>
<Page titleTag={pageTitleSuffixer(['Monitoring & Alerting'])}>
<Page titleTag={pageTitleSuffixer(['Alerts'])}>
<Page.Header fullWidth={false}>
<Page.Title title="Monitoring & Alerting" />
<Page.Title title="Alerts" />
<CloudUpgradeButton />
</Page.Header>
<Page.Contents fullWidth={false} scrollable={false}>
<GetResources resources={[ResourceType.Labels]}>

View File

@ -10,6 +10,7 @@ import CheckHistoryVisualization from 'src/checks/components/CheckHistoryVisuali
import AlertHistoryQueryParams from 'src/alerting/components/AlertHistoryQueryParams'
import EventTable from 'src/eventViewer/components/EventTable'
import GetResources from 'src/resources/components/GetResources'
import CloudUpgradeButton from 'src/shared/components/CloudUpgradeButton'
//Context
import {ResourceIDsContext} from 'src/alerting/components/AlertHistoryIndex'
@ -63,6 +64,7 @@ const CheckHistory: FC<Props> = ({
title="Check Statuses"
testID="alert-history-title"
/>
<CloudUpgradeButton />
</Page.Header>
<Page.ControlBar fullWidth={true}>
<Page.ControlBarLeft>

View File

@ -13,6 +13,7 @@ import SearchWidget from 'src/shared/components/search_widget/SearchWidget'
import AddResourceDropdown from 'src/shared/components/AddResourceDropdown'
import GetAssetLimits from 'src/cloud/components/GetAssetLimits'
import AssetLimitAlert from 'src/cloud/components/AssetLimitAlert'
import CloudUpgradeButton from 'src/shared/components/CloudUpgradeButton'
// Utils
import {pageTitleSuffixer} from 'src/shared/utils/pageTitles'
@ -66,6 +67,7 @@ class DashboardIndex extends PureComponent<Props, State> {
>
<Page.Header fullWidth={false}>
<Page.Title title="Dashboards" />
<CloudUpgradeButton />
</Page.Header>
<Page.ControlBar fullWidth={false}>
<Page.ControlBarLeft>

View File

@ -10,6 +10,7 @@ import ViewTypeDropdown from 'src/timeMachine/components/view_options/ViewTypeDr
import GetResources from 'src/resources/components/GetResources'
import TimeZoneDropdown from 'src/shared/components/TimeZoneDropdown'
import DeleteDataButton from 'src/dataExplorer/components/DeleteDataButton'
import CloudUpgradeButton from 'src/shared/components/CloudUpgradeButton'
// Types
import {ResourceType} from 'src/types'
@ -24,6 +25,7 @@ const DataExplorerPage: SFC = ({children}) => {
<GetResources resources={[ResourceType.Variables, ResourceType.Buckets]}>
<Page.Header fullWidth={true}>
<Page.Title title="Data Explorer" />
<CloudUpgradeButton />
</Page.Header>
<Page.ControlBar fullWidth={true}>
<Page.ControlBarLeft>

View File

@ -415,7 +415,7 @@ class Root extends PureComponent {
/>
</Route>
<Route path="labels" component={LabelsIndex} />
<Route path="profile" component={OrgProfilePage}>
<Route path="about" component={OrgProfilePage}>
<Route
path="rename"
component={RenameOrgOverlay}
@ -460,6 +460,12 @@ class Root extends PureComponent {
path="checks/:checkID"
component={CheckHistory}
/>
<Route path="about" component={OrgProfilePage}>
<Route path="rename" component={RenameOrgOverlay} />
</Route>
{!CLOUD && (
<Route path="members" component={MembersIndex} />
)}
</Route>
</Route>
</Route>

View File

@ -16,6 +16,7 @@ import {
import Resources from 'src/me/components/Resources'
import Docs from 'src/me/components/Docs'
import GettingStarted from 'src/me/components/GettingStarted'
import CloudUpgradeButton from 'src/shared/components/CloudUpgradeButton'
// Utils
import {pageTitleSuffixer} from 'src/shared/utils/pageTitles'
@ -39,6 +40,7 @@ export class MePage extends PureComponent<StateProps> {
<Page titleTag={pageTitleSuffixer(['Home'])}>
<Page.Header fullWidth={false}>
<Page.Title title="Getting Started" />
<CloudUpgradeButton />
</Page.Header>
<Page.Contents fullWidth={false} scrollable={true}>
<Grid>

View File

@ -4,11 +4,14 @@ import {connect} from 'react-redux'
// Components
import {ErrorHandling} from 'src/shared/decorators/errors'
import OrgTabbedPage from 'src/organizations/components/OrgTabbedPage'
import OrgHeader from 'src/organizations/components/OrgHeader'
import SettingsTabbedPage from 'src/settings/components/SettingsTabbedPage'
import SettingsHeader from 'src/settings/components/SettingsHeader'
import {Page} from '@influxdata/clockface'
import GetResources from 'src/resources/components/GetResources'
import Members from 'src/members/components/Members'
import {FeatureFlag} from 'src/shared/utils/featureFlag'
// Utils
import {pageTitleSuffixer} from 'src/shared/utils/pageTitles'
@ -34,14 +37,26 @@ class MembersIndex extends Component<Props> {
return (
<>
<Page titleTag={pageTitleSuffixer(['Members', 'Settings'])}>
<SettingsHeader />
<SettingsTabbedPage activeTab="members" orgID={org.id}>
<GetResources resources={[ResourceType.Members]}>
<Members />
</GetResources>
</SettingsTabbedPage>
</Page>
<FeatureFlag name="treeNav">
<Page titleTag={pageTitleSuffixer(['Members', 'Organization'])}>
<OrgHeader />
<OrgTabbedPage activeTab="members" orgID={org.id}>
<GetResources resources={[ResourceType.Members]}>
<Members />
</GetResources>
</OrgTabbedPage>
</Page>
</FeatureFlag>
<FeatureFlag name="treeNav" equals={false}>
<Page titleTag={pageTitleSuffixer(['Members', 'Settings'])}>
<SettingsHeader />
<SettingsTabbedPage activeTab="members" orgID={org.id}>
<GetResources resources={[ResourceType.Members]}>
<Members />
</GetResources>
</SettingsTabbedPage>
</Page>
</FeatureFlag>
{children}
</>
)

View File

@ -20,6 +20,7 @@ export const localState: LocalStorage = {
persisted: {
autoRefresh: 0,
showTemplateControlBar: false,
navBarState: 'expanded',
timeZone: 'Local' as TimeZone,
theme: 'dark',
},

View File

@ -0,0 +1,18 @@
import React, {Component} from 'react'
// Components
import {Page} from '@influxdata/clockface'
import CloudUpgradeButton from 'src/shared/components/CloudUpgradeButton'
class OrgHeader extends Component {
public render() {
return (
<Page.Header fullWidth={false}>
<Page.Title title="Organization" />
<CloudUpgradeButton />
</Page.Header>
)
}
}
export default OrgHeader

View File

@ -0,0 +1,110 @@
// Libraries
import React, {PureComponent} from 'react'
import _ from 'lodash'
import {withRouter, WithRouterProps} from 'react-router'
// Components
import {Tabs, Orientation, ComponentSize} from '@influxdata/clockface'
import CloudExclude from 'src/shared/components/cloud/CloudExclude'
import CloudOnly from 'src/shared/components/cloud/CloudOnly'
import {FeatureFlag} from 'src/shared/utils/featureFlag'
// Constants
import {CLOUD_USERS_PATH} from 'src/shared/constants'
// Decorators
import {ErrorHandling} from 'src/shared/decorators/errors'
interface OwnProps {
activeTab: string
orgID: string
}
type Props = OwnProps & WithRouterProps
interface OrgPageTab {
text: string
id: string
cloudExclude?: boolean
cloudOnly?: boolean
onClick: () => void
featureFlag?: string
}
@ErrorHandling
class OrgNavigation extends PureComponent<Props> {
public render() {
const {activeTab, orgID, router} = this.props
const tabs: OrgPageTab[] = [
{
text: 'Members',
id: 'members-oss',
cloudExclude: true,
onClick: () => {
router.push(`/orgs/${orgID}/members`)
},
},
{
text: 'Members',
id: 'members-cloud',
featureFlag: 'multiUser',
cloudOnly: true,
onClick: () => {
window.location.assign(`/orgs/${orgID}/${CLOUD_USERS_PATH}`)
},
},
{
text: 'About',
id: 'about',
onClick: () => {
router.push(`/orgs/${orgID}/about`)
},
},
]
return (
<Tabs orientation={Orientation.Horizontal} size={ComponentSize.Large}>
{tabs.map(t => {
let isActive = t.id === activeTab
if (t.id === 'members-oss' || t.id === 'members-cloud') {
if (activeTab === 'members') {
isActive = true
}
}
let tab = (
<Tabs.Tab
key={t.id}
text={t.text}
id={t.id}
onClick={t.onClick}
active={isActive}
/>
)
if (t.cloudExclude) {
tab = <CloudExclude key={t.id}>{tab}</CloudExclude>
}
if (t.cloudOnly) {
tab = <CloudOnly key={t.id}>{tab}</CloudOnly>
}
if (t.featureFlag) {
tab = (
<FeatureFlag key={t.id} name={t.featureFlag}>
{tab}
</FeatureFlag>
)
}
return tab
})}
</Tabs>
)
}
}
export default withRouter(OrgNavigation)

View File

@ -73,7 +73,7 @@ class OrgProfileTab extends PureComponent<Props> {
router,
} = this.props
router.push(`/orgs/${orgID}/settings/profile/rename`)
router.push(`/orgs/${orgID}/settings/about/rename`)
}
}

View File

@ -0,0 +1,33 @@
// Libraries
import React, {PureComponent} from 'react'
import _ from 'lodash'
// Components
import OrgNavigation from 'src/organizations/components/OrgNavigation'
import {Tabs, Orientation, Page} from '@influxdata/clockface'
// Decorators
import {ErrorHandling} from 'src/shared/decorators/errors'
interface Props {
activeTab: string
orgID: string
}
@ErrorHandling
class OrgTabbedPage extends PureComponent<Props> {
public render() {
const {activeTab, orgID, children} = this.props
return (
<Page.Contents fullWidth={false} scrollable={true}>
<Tabs.Container orientation={Orientation.Horizontal}>
<OrgNavigation activeTab={activeTab} orgID={orgID} />
<Tabs.TabContents>{children}</Tabs.TabContents>
</Tabs.Container>
</Page.Contents>
)
}
}
export default OrgTabbedPage

View File

@ -118,7 +118,7 @@ class RenameOrgForm extends PureComponent<Props, State> {
}
private handleGoBack = () => {
this.props.router.push(`/orgs/${this.props.startOrg.id}/settings/profile`)
this.props.router.push(`/orgs/${this.props.startOrg.id}/settings/about`)
}
private handleValidation = (orgName: string): string | null => {

View File

@ -47,7 +47,7 @@ class RenameOrgOverlay extends PureComponent<WithRouterProps> {
params: {orgID},
} = this.props
router.push(`/orgs/${orgID}/settings/profile`)
router.push(`/orgs/${orgID}/settings/about`)
}
}

View File

@ -4,9 +4,12 @@ import {connect} from 'react-redux'
// Components
import {ErrorHandling} from 'src/shared/decorators/errors'
import OrgTabbedPage from 'src/organizations/components/OrgTabbedPage'
import OrgHeader from 'src/organizations/components/OrgHeader'
import SettingsTabbedPage from 'src/settings/components/SettingsTabbedPage'
import SettingsHeader from 'src/settings/components/SettingsHeader'
import {Grid, Columns, Page} from '@influxdata/clockface'
import {FeatureFlag} from 'src/shared/utils/featureFlag'
// Utils
import {pageTitleSuffixer} from 'src/shared/utils/pageTitles'
@ -27,18 +30,34 @@ class OrgProfilePage extends Component<StateProps> {
return (
<>
<Page titleTag={pageTitleSuffixer(['Org Profile', 'Settings'])}>
<SettingsHeader />
<SettingsTabbedPage activeTab="profile" orgID={org.id}>
<Grid>
<Grid.Row>
<Grid.Column widthXS={Columns.Twelve} widthSM={Columns.Six}>
<OrgProfileTab />
</Grid.Column>
</Grid.Row>
</Grid>
</SettingsTabbedPage>
</Page>
<FeatureFlag name="treeNav">
<Page titleTag={pageTitleSuffixer(['About', 'Organization'])}>
<OrgHeader />
<OrgTabbedPage activeTab="about" orgID={org.id}>
<Grid>
<Grid.Row>
<Grid.Column widthXS={Columns.Twelve} widthSM={Columns.Six}>
<OrgProfileTab />
</Grid.Column>
</Grid.Row>
</Grid>
</OrgTabbedPage>
</Page>
</FeatureFlag>
<FeatureFlag name="treeNav" equals={false}>
<Page titleTag={pageTitleSuffixer(['About', 'Settings'])}>
<SettingsHeader />
<SettingsTabbedPage activeTab="about" orgID={org.id}>
<Grid>
<Grid.Row>
<Grid.Column widthXS={Columns.Twelve} widthSM={Columns.Six}>
<OrgProfileTab />
</Grid.Column>
</Grid.Row>
</Grid>
</SettingsTabbedPage>
</Page>
</FeatureFlag>
{children}
</>
)

View File

@ -13,6 +13,9 @@ import AllAccessTokenOverlay from 'src/authorizations/components/AllAccessTokenO
import BucketsTokenOverlay from 'src/authorizations/components/BucketsTokenOverlay'
import TelegrafConfigOverlay from 'src/telegrafs/components/TelegrafConfigOverlay'
import TelegrafOutputOverlay from 'src/telegrafs/components/TelegrafOutputOverlay'
import OrgSwitcherOverlay from 'src/pageLayout/components/OrgSwitcherOverlay'
// Actions
import {dismissOverlay} from 'src/overlays/actions/overlays'
interface StateProps {
@ -56,6 +59,9 @@ const OverlayController: FunctionComponent<OverlayControllerProps> = props => {
case 'telegraf-output':
activeOverlay = <TelegrafOutputOverlay onClose={closer} />
break
case 'switch-organizations':
activeOverlay = <OrgSwitcherOverlay onClose={closer} />
break
default:
visibility = false
}

View File

@ -11,6 +11,7 @@ export type OverlayID =
| 'add-token'
| 'telegraf-config'
| 'telegraf-output'
| 'switch-organizations'
export interface OverlayParams {
[key: string]: string

View File

@ -0,0 +1,45 @@
// Libraries
import React, {FC} from 'react'
import {Link} from 'react-router'
// Components
import {
TreeNav,
IconFont,
InfluxDBCloudLogo,
Icon,
ComponentColor,
} from '@influxdata/clockface'
import CloudExclude from 'src/shared/components/cloud/CloudExclude'
import CloudOnly from 'src/shared/components/cloud/CloudOnly'
interface Props {
link: string
}
const NavHeader: FC<Props> = ({link}) => {
return (
<>
<CloudExclude>
<TreeNav.Header
id="getting-started"
icon={<Icon glyph={IconFont.CuboNav} />}
label={<InfluxDBCloudLogo cloud={false} />}
color={ComponentColor.Secondary}
linkElement={className => <Link className={className} to={link} />}
/>
</CloudExclude>
<CloudOnly>
<TreeNav.Header
id="getting-started"
icon={<Icon glyph={IconFont.CuboNav} />}
label={<InfluxDBCloudLogo cloud={true} />}
color={ComponentColor.Primary}
linkElement={className => <Link className={className} to={link} />}
/>
</CloudOnly>
</>
)
}
export default NavHeader

View File

@ -0,0 +1,51 @@
// Libraries
import React, {FC} from 'react'
import classnames from 'classnames'
import {WithRouterProps, withRouter} from 'react-router'
// Components
import {IconFont, Icon} from '@influxdata/clockface'
interface ComponentProps {
orgName: string
orgID: string
selected: boolean
onDismiss: () => void
}
type Props = ComponentProps & WithRouterProps
const OrgSwitcherItem: FC<Props> = ({
orgName,
selected,
onDismiss,
orgID,
router,
}) => {
const orgSwitcherItemClass = classnames('org-switcher--item', {
'org-switcher--item__selected': selected,
})
const orgSwitcherIcon = selected ? IconFont.Checkmark : IconFont.CaretRight
const handleClick = (): void => {
onDismiss()
router.push(`orgs/${orgID}`)
}
const currentOrgIndicator = selected ? <em>Current</em> : null
return (
<li className={orgSwitcherItemClass} onClick={handleClick}>
<div className="org-switcher--item-circle">
<Icon glyph={orgSwitcherIcon} className="org-switcher--item-icon" />
</div>
<span className="org-switcher--item-label">
{orgName}
{currentOrgIndicator}
</span>
</li>
)
}
export default withRouter(OrgSwitcherItem)

View File

@ -0,0 +1,77 @@
.org-switcher--list {
display: flex;
flex-direction: column;
align-items: stretch;
}
.org-switcher--prompt {
text-align: center;
margin-top: 0;
margin-bottom: $cf-marg-a;
color: $g9-mountain;
}
.org-switcher--item {
display: flex;
align-items: center;
color: $g11-sidewalk;
background-color: $g2-kevlar;
padding: $cf-marg-c;
margin-bottom: $cf-marg-a;
border-radius: $cf-radius;
transition: background-color 0.25s ease, color 0.25s ease;
&:hover {
cursor: pointer;
color: $g15-platinum;
background-color: $g4-onyx;
}
}
.org-switcher--item-label {
user-select: none;
font-size: 1.125em;
font-weight: $cf-font-weight--medium;
em {
font-weight: $cf-font-weight--regular;
font-size: 0.8em;
font-style: normal;
opacity: 0.3;
display: inline-block;
margin-left: $cf-marg-b;
}
}
.org-switcher--item-circle {
width: $cf-marg-d;
height: $cf-marg-d;
border-radius: 50%;
background-color: $g1-raven;
margin-right: $cf-marg-c;
display: flex;
justify-content: center;
align-items: center;
align-content: center;
transition: background-color 0.25s ease, color 0.25s ease, text-shadow 0.25s ease;
color: $g1-raven;
.org-switcher--item:hover & {
text-shadow: 0 0 4px $c-ocean;
background-color: $g2-kevlar;
color: $c-pool;
}
}
.org-switcher--item__selected,
.org-switcher--item__selected:hover {
cursor: default;
background-color: $g5-pepper;
color: $g20-white;
.org-switcher--item-circle {
text-shadow: 0 0 4px $c-viridian;
color: $c-honeydew;
background-color: $g3-castle;
}
}

View File

@ -0,0 +1,67 @@
// Libraries
import React, {FC} from 'react'
import {connect} from 'react-redux'
// Components
import {Overlay, Sort} from '@influxdata/clockface'
import OrgSwitcherItem from 'src/pageLayout/components/OrgSwitcherItem'
import SortingHat from 'src/shared/components/sorting_hat/SortingHat'
// Types
import {AppState, Organization, ResourceType} from 'src/types'
// Selectors
import {getOrg} from 'src/organizations/selectors'
import {getAll} from 'src/resources/selectors'
interface ComponentProps {
onClose: () => void
}
interface StateProps {
orgs: Organization[]
currentOrg: Organization
}
type Props = ComponentProps & StateProps
const OrgSwitcherOverlay: FC<Props> = ({orgs, onClose, currentOrg}) => {
return (
<Overlay.Container maxWidth={500}>
<Overlay.Header title="Switch Organizations" onDismiss={onClose} />
<Overlay.Body>
<p className="org-switcher--prompt">Choose an organization</p>
<SortingHat list={orgs} sortKey="name" direction={Sort.Ascending}>
{sortedOrgs => (
<div className="org-switcher--list">
{sortedOrgs.map(org => (
<OrgSwitcherItem
key={org.id}
orgID={org.id}
orgName={org.name}
selected={org.id === currentOrg.id}
onDismiss={onClose}
/>
))}
</div>
)}
</SortingHat>
</Overlay.Body>
</Overlay.Container>
)
}
const mstp = (state: AppState): StateProps => {
const orgs = getAll<Organization>(state, ResourceType.Orgs)
const currentOrg = getOrg(state)
return {
orgs,
currentOrg,
}
}
export default connect<StateProps, {}, ComponentProps>(
mstp,
null
)(OrgSwitcherOverlay)

View File

@ -0,0 +1,149 @@
// Libraries
import React, {FC} from 'react'
import {Link} from 'react-router'
import {connect} from 'react-redux'
// Components
import {TreeNav} from '@influxdata/clockface'
import CloudExclude from 'src/shared/components/cloud/CloudExclude'
import CloudOnly from 'src/shared/components/cloud/CloudOnly'
import {FeatureFlag} from 'src/shared/utils/featureFlag'
// Actions
import {showOverlay, dismissOverlay} from 'src/overlays/actions/overlays'
// Constants
import {
CLOUD_URL,
CLOUD_LOGOUT_PATH,
CLOUD_USAGE_PATH,
CLOUD_BILLING_PATH,
} from 'src/shared/constants'
// Types
import {AppState, Organization} from 'src/types'
import {MeState} from 'src/shared/reducers/me'
// Selectors
import {getOrg} from 'src/organizations/selectors'
interface StateProps {
org: Organization
me: MeState
}
interface DispatchProps {
handleShowOverlay: typeof showOverlay
handleDismissOverlay: typeof dismissOverlay
}
type Props = StateProps & DispatchProps
const UserWidget: FC<Props> = ({
org,
me,
handleShowOverlay,
handleDismissOverlay,
}) => {
if (!org) {
return null
}
const logoutURL = `${CLOUD_URL}${CLOUD_LOGOUT_PATH}`
const handleSwitchOrganizations = (): void => {
handleShowOverlay('switch-organizations', {}, handleDismissOverlay)
}
const logoutLink = (
<>
<FeatureFlag name="regionBasedLoginPage">
<TreeNav.UserItem
id="logout"
label="Logout"
linkElement={className => <Link className={className} to="/logout" />}
/>
</FeatureFlag>
<FeatureFlag name="regionBasedLoginPage" equals={false}>
<CloudExclude>
<TreeNav.UserItem
id="logout"
label="Logout"
linkElement={className => (
<Link className={className} to="/logout" />
)}
/>
</CloudExclude>
<CloudOnly>
<TreeNav.UserItem
id="logout"
label="Logout"
linkElement={className => (
<a className={className} href={logoutURL} />
)}
/>
</CloudOnly>
</FeatureFlag>
</>
)
return (
<TreeNav.User username={me.name} team={org.name}>
<CloudOnly>
<TreeNav.UserItem
id="usage"
label="Usage"
linkElement={className => (
<a
className={className}
href={`${CLOUD_URL}/organizations/${org.id}/${CLOUD_USAGE_PATH}`}
/>
)}
/>
<TreeNav.UserItem
id="billing"
label="Billing"
linkElement={className => (
<a
className={className}
href={`${CLOUD_URL}/organizations/${
org.id
}/${CLOUD_BILLING_PATH}`}
/>
)}
/>
</CloudOnly>
<CloudExclude>
<TreeNav.UserItem
id="switch-orgs"
label="Switch Organizations"
onClick={handleSwitchOrganizations}
/>
<TreeNav.UserItem
id="create-org"
label="Create Organization"
linkElement={className => (
<Link className={className} to="/orgs/new" />
)}
/>
</CloudExclude>
{logoutLink}
</TreeNav.User>
)
}
const mstp = (state: AppState) => {
const org = getOrg(state)
const me = state.me
return {org, me}
}
const mdtp = {
handleShowOverlay: showOverlay,
handleDismissOverlay: dismissOverlay,
}
export default connect<StateProps, DispatchProps>(
mstp,
mdtp
)(UserWidget)

View File

@ -0,0 +1,176 @@
import {IconFont} from '@influxdata/clockface'
import {CLOUD_URL, CLOUD_USERS_PATH} from 'src/shared/constants'
export interface NavSubItem {
id: string
testID: string
label: string
link: string
cloudExclude?: boolean
cloudOnly?: boolean
featureFlag?: string
}
export interface NavItem {
id: string
testID: string
label: string
shortLabel?: string
link: string
icon: IconFont
cloudExclude?: boolean
cloudOnly?: boolean
featureFlag?: string
menu?: NavSubItem[]
activeKeywords: string[]
}
export const generateNavItems = (orgID: string): NavItem[] => {
const orgPrefix = `/orgs/${orgID}`
return [
{
id: 'load-data',
testID: 'nav-item-load-data',
icon: IconFont.DisksNav,
label: 'Load Data',
shortLabel: 'Data',
link: `${orgPrefix}/load-data/buckets`,
activeKeywords: ['load-data'],
menu: [
{
id: 'buckets',
testID: 'nav-subitem-buckets',
label: 'Buckets',
link: `${orgPrefix}/load-data/buckets`,
},
{
id: 'telegrafs',
testID: 'nav-subitem-telegrafs',
label: 'Telegraf',
link: `${orgPrefix}/load-data/telegrafs`,
},
{
id: 'scrapers',
testID: 'nav-subitem-scrapers',
label: 'Scrapers',
link: `${orgPrefix}/load-data/scrapers`,
cloudExclude: true,
},
{
id: 'tokens',
testID: 'nav-subitem-tokens',
label: 'Tokens',
link: `${orgPrefix}/load-data/tokens`,
},
{
id: 'client-libraries',
testID: 'nav-subitem-client-libraries',
label: 'Client Libraries',
link: `${orgPrefix}/load-data/client-libraries`,
},
],
},
{
id: 'data-explorer',
testID: 'nav-item-data-explorer',
icon: IconFont.GraphLine,
label: 'Data Explorer',
shortLabel: 'Explore',
link: `${orgPrefix}/data-explorer`,
activeKeywords: ['data-explorer'],
},
{
id: 'org',
testID: 'nav-item-org',
icon: IconFont.UsersTrio,
label: 'Organization',
shortLabel: 'Org',
link: `${orgPrefix}/members`,
activeKeywords: ['members', 'about'],
menu: [
{
id: 'members',
testID: 'nav-subitem-members',
label: 'Members',
link: `${orgPrefix}/members`,
cloudExclude: true,
},
{
id: 'multi-user-members',
testID: 'nav-subitem-multi-user-members',
label: 'Members',
featureFlag: 'multiUser',
link: `${CLOUD_URL}/organizations/${orgID}/${CLOUD_USERS_PATH}`,
},
{
id: 'about',
testID: 'nav-subitem-about',
label: 'About',
link: `${orgPrefix}/about`,
},
],
},
{
id: 'dashboards',
testID: 'nav-item-dashboards',
icon: IconFont.Dashboards,
label: 'Dashboards',
shortLabel: 'Boards',
link: `${orgPrefix}/dashboards`,
activeKeywords: ['dashboards'],
},
{
id: 'tasks',
testID: 'nav-item-tasks',
icon: IconFont.Calendar,
label: 'Tasks',
link: `${orgPrefix}/tasks`,
activeKeywords: ['tasks'],
},
{
id: 'alerting',
testID: 'nav-item-alerting',
icon: IconFont.Bell,
label: 'Alerts',
link: `${orgPrefix}/alerting`,
activeKeywords: ['alerting'],
menu: [
{
id: 'history',
testID: 'nav-subitem-history',
label: 'Alert History',
link: `${orgPrefix}/alert-history`,
},
],
},
{
id: 'settings',
testID: 'nav-item-settings',
icon: IconFont.WrenchNav,
label: 'Settings',
link: `${orgPrefix}/settings/variables`,
activeKeywords: ['settings'],
menu: [
{
id: 'variables',
testID: 'nav-subitem-variables',
label: 'Variables',
link: `${orgPrefix}/settings/variables`,
},
{
id: 'templates',
testID: 'nav-subitem-templates',
label: 'Templates',
link: `${orgPrefix}/settings/templates`,
},
{
id: 'labels',
testID: 'nav-subitem-labels',
label: 'Labels',
link: `${orgPrefix}/settings/labels`,
},
],
},
]
}

View File

@ -89,7 +89,7 @@ class SideNav extends PureComponent<Props, State> {
const variablesLink = `${orgPrefix}/settings/variables`
const templatesLink = `${orgPrefix}/settings/templates`
const labelsLink = `${orgPrefix}/settings/labels`
const profileLink = `${orgPrefix}/settings/profile`
const profileLink = `${orgPrefix}/settings/about`
// Feedback
const feedbackLink =
'https://docs.google.com/forms/d/e/1FAIpQLSdGJpnIZGotN1VFJPkgZEhrt4t4f6QY1lMgMSRUnMeN3FjCKA/viewform?usp=sf_link'
@ -329,7 +329,7 @@ class SideNav extends PureComponent<Props, State> {
Profile
</Link>
)}
active={getNavItemActivation(['profile'], location.pathname)}
active={getNavItemActivation(['about'], location.pathname)}
key="profile"
/>
</NavMenu.Item>

View File

@ -0,0 +1,188 @@
// Libraries
import React, {PureComponent} from 'react'
import {withRouter, WithRouterProps, Link} from 'react-router'
import {connect} from 'react-redux'
import {get} from 'lodash'
// Components
import {Icon, TreeNav} from '@influxdata/clockface'
import UserWidget from 'src/pageLayout/components/UserWidget'
import NavHeader from 'src/pageLayout/components/NavHeader'
import CloudUpgradeNavBanner from 'src/shared/components/CloudUpgradeNavBanner'
import CloudExclude from 'src/shared/components/cloud/CloudExclude'
import CloudOnly from 'src/shared/components/cloud/CloudOnly'
import {FeatureFlag} from 'src/shared/utils/featureFlag'
// Constants
import {generateNavItems} from 'src/pageLayout/constants/navigationHierarchy'
// Utils
import {getNavItemActivation} from 'src/pageLayout/utils'
// Types
import {AppState, NavBarState} from 'src/types'
// Actions
import {setNavBarState} from 'src/shared/actions/app'
// Decorators
import {ErrorHandling} from 'src/shared/decorators/errors'
interface StateProps {
isHidden: boolean
navBarState: NavBarState
}
interface DispatchProps {
handleSetNavBarState: typeof setNavBarState
}
type Props = StateProps & DispatchProps & WithRouterProps
@ErrorHandling
class TreeSidebar extends PureComponent<Props> {
public render() {
const {
isHidden,
params: {orgID},
navBarState,
handleSetNavBarState,
} = this.props
if (isHidden) {
return null
}
const isExpanded = navBarState === 'expanded'
const handleToggleNavExpansion = (): void => {
if (isExpanded) {
handleSetNavBarState('collapsed')
} else {
handleSetNavBarState('expanded')
}
}
const orgPrefix = `/orgs/${orgID}`
const navItems = generateNavItems(orgID)
return (
<TreeNav
expanded={isExpanded}
headerElement={<NavHeader link={orgPrefix} />}
userElement={<UserWidget />}
onToggleClick={handleToggleNavExpansion}
bannerElement={<CloudUpgradeNavBanner />}
>
{navItems.map(item => {
let navItemElement = (
<TreeNav.Item
key={item.id}
id={item.id}
testID={item.testID}
icon={<Icon glyph={item.icon} />}
label={item.label}
shortLabel={item.shortLabel}
active={getNavItemActivation(
item.activeKeywords,
location.pathname
)}
linkElement={className => (
<Link className={className} to={item.link} />
)}
>
{Boolean(item.menu) && (
<TreeNav.SubMenu>
{item.menu.map(menuItem => {
let navSubItemElement = (
<TreeNav.SubItem
key={menuItem.id}
id={menuItem.id}
testID={menuItem.testID}
active={getNavItemActivation(
[menuItem.id],
location.pathname
)}
label={menuItem.label}
linkElement={className => (
<Link className={className} to={menuItem.link} />
)}
/>
)
if (menuItem.cloudExclude) {
navSubItemElement = (
<CloudExclude key={menuItem.id}>
{navSubItemElement}
</CloudExclude>
)
}
if (menuItem.cloudOnly) {
navSubItemElement = (
<CloudOnly key={menuItem.id}>
{navSubItemElement}
</CloudOnly>
)
}
if (menuItem.featureFlag) {
navSubItemElement = (
<FeatureFlag
key={menuItem.id}
name={menuItem.featureFlag}
>
{navSubItemElement}
</FeatureFlag>
)
}
return navSubItemElement
})}
</TreeNav.SubMenu>
)}
</TreeNav.Item>
)
if (item.cloudExclude) {
navItemElement = (
<CloudExclude key={item.id}>{navItemElement}</CloudExclude>
)
}
if (item.cloudOnly) {
navItemElement = (
<CloudOnly key={item.id}>{navItemElement}</CloudOnly>
)
}
if (item.featureFlag) {
navItemElement = (
<FeatureFlag key={item.id} name={item.featureFlag}>
{navItemElement}
</FeatureFlag>
)
}
return navItemElement
})}
</TreeNav>
)
}
}
const mdtp: DispatchProps = {
handleSetNavBarState: setNavBarState,
}
const mstp = (state: AppState): StateProps => {
const isHidden = get(state, 'app.ephemeral.inPresentationMode', false)
const navBarState = get(state, 'app.persisted.navBarState', 'collapsed')
return {isHidden, navBarState}
}
export default connect<StateProps, DispatchProps>(
mstp,
mdtp
)(withRouter(TreeSidebar))

View File

@ -1,3 +0,0 @@
import Nav from 'src/pageLayout/containers/Nav'
export default Nav

View File

@ -2,12 +2,14 @@ import React, {Component} from 'react'
// Components
import {Page} from '@influxdata/clockface'
import CloudUpgradeButton from 'src/shared/components/CloudUpgradeButton'
class LoadDataHeader extends Component {
public render() {
return (
<Page.Header fullWidth={false}>
<Page.Title title="Load Data" />
<CloudUpgradeButton />
</Page.Header>
)
}

View File

@ -2,12 +2,14 @@ import React, {Component} from 'react'
// Components
import {Page} from '@influxdata/clockface'
import CloudUpgradeButton from 'src/shared/components/CloudUpgradeButton'
class SettingsHeader extends Component {
public render() {
return (
<Page.Header fullWidth={false}>
<Page.Title title="Settings" />
<CloudUpgradeButton />
</Page.Header>
)
}

View File

@ -4,11 +4,13 @@ import _ from 'lodash'
import {withRouter, WithRouterProps} from 'react-router'
// Components
import {Tabs, Orientation, ComponentSize} from '@influxdata/clockface'
import TabbedPageTabs from 'src/shared/tabbedPage/TabbedPageTabs'
// Types
import {TabbedPageTab} from 'src/shared/tabbedPage/TabbedPageTabs'
// Decorators
import {ErrorHandling} from 'src/shared/decorators/errors'
import CloudExclude from 'src/shared/components/cloud/CloudExclude'
interface OwnProps {
activeTab: string
@ -26,60 +28,39 @@ class SettingsNavigation extends PureComponent<Props> {
router.push(`/orgs/${orgID}/settings/${id}`)
}
const tabs = [
const tabs: TabbedPageTab[] = [
{
text: 'Members',
id: 'members',
cloudExclude: true,
featureFlagName: 'treeNav',
featureFlagValue: false,
},
{
text: 'Variables',
id: 'variables',
cloudExclude: false,
},
{
text: 'Templates',
id: 'templates',
cloudExclude: false,
},
{
text: 'Labels',
id: 'labels',
cloudExclude: false,
},
{
text: 'Org Profile',
id: 'profile',
cloudExclude: false,
text: 'Profile',
id: 'about',
featureFlagName: 'treeNav',
featureFlagValue: false,
},
]
return (
<Tabs orientation={Orientation.Horizontal} size={ComponentSize.Large}>
{tabs.map(t => {
if (t.cloudExclude) {
return (
<CloudExclude key={t.id}>
<Tabs.Tab
text={t.text}
id={t.id}
onClick={handleTabClick}
active={t.id === activeTab}
/>
</CloudExclude>
)
}
return (
<Tabs.Tab
key={t.id}
text={t.text}
id={t.id}
onClick={handleTabClick}
active={t.id === activeTab}
/>
)
})}
</Tabs>
<TabbedPageTabs
tabs={tabs}
activeTab={activeTab}
onTabClick={handleTabClick}
/>
)
}
}

View File

@ -4,12 +4,12 @@ import {notify} from 'src/shared/actions/notifications'
import {presentationMode} from 'src/shared/copy/notifications'
import {Dispatch} from 'redux'
import {TimeZone, Theme} from 'src/types'
import {TimeZone, Theme, NavBarState} from 'src/types'
export enum ActionTypes {
EnablePresentationMode = 'ENABLE_PRESENTATION_MODE',
DisablePresentationMode = 'DISABLE_PRESENTATION_MODE',
SetNavBarState = 'SET_NAV_BAR_STATE',
SetAutoRefresh = 'SET_AUTOREFRESH',
SetTimeZone = 'SET_APP_TIME_ZONE',
TemplateControlBarVisibilityToggled = 'TemplateControlBarVisibilityToggledAction',
@ -19,12 +19,11 @@ export enum ActionTypes {
export type Action =
| ReturnType<typeof enablePresentationMode>
| ReturnType<typeof disablePresentationMode>
| ReturnType<typeof setNavBarState>
| ReturnType<typeof setAutoRefresh>
| ReturnType<typeof setTimeZone>
| ReturnType<typeof setTheme>
export const setTheme = (theme: Theme) => ({type: 'SET_THEME', theme} as const)
// ephemeral state action creators
export const enablePresentationMode = () =>
@ -47,6 +46,14 @@ export const delayEnablePresentationMode = () => (
// persistent state action creators
export const setTheme = (theme: Theme) => ({type: 'SET_THEME', theme} as const)
export const setNavBarState = (navBarState: NavBarState) =>
({
type: ActionTypes.SetNavBarState,
navBarState,
} as const)
export const setAutoRefresh = (milliseconds: number) =>
({
type: ActionTypes.SetAutoRefresh,

View File

@ -0,0 +1,27 @@
// Libraries
import React, {FC} from 'react'
import {Link} from 'react-router'
// Components
import {FeatureFlag} from 'src/shared/utils/featureFlag'
import CloudOnly from 'src/shared/components/cloud/CloudOnly'
// Constants
import {CLOUD_URL, CLOUD_CHECKOUT_PATH} from 'src/shared/constants'
const CloudUpgradeButton: FC = () => {
return (
<CloudOnly>
<FeatureFlag name="treeNav">
<Link
className="cf-button cf-button-sm cf-button-success upgrade-payg--button"
to={`${CLOUD_URL}${CLOUD_CHECKOUT_PATH}`}
>
Upgrade Now
</Link>
</FeatureFlag>
</CloudOnly>
)
}
export default CloudUpgradeButton

View File

@ -0,0 +1,36 @@
.cloud-upgrade-banner {
display: block;
}
.cloud-upgrade-banner--button {
text-align: center;
}
a.cloud-upgrade-banner__collapsed {
text-align: center;
border-radius: $cf-radius;
margin: $cf-marg-b 0;
display: none;
width: 100%;
h5 {
font-size: $cf-text-tiny;
font-size: 0.9em;
margin-bottom: $cf-marg-a;
}
.cf-icon {
font-size: 2.75em;
margin-bottom: $cf-marg-a;
}
}
// Collapsed mode
@media screen and (min-width: $cf-nav-menu--breakpoint) {
.cf-tree-nav__collapsed a.cloud-upgrade-banner__collapsed {
display: inline-block;
}
.cf-tree-nav__collapsed .cloud-upgrade-banner {
display: none;
}
}

View File

@ -0,0 +1,58 @@
// Libraries
import React, {FC} from 'react'
import {Link} from 'react-router'
// Components
import {
Panel,
ComponentSize,
Heading,
HeadingElement,
Gradients,
JustifyContent,
Icon,
IconFont,
} from '@influxdata/clockface'
import CloudOnly from 'src/shared/components/cloud/CloudOnly'
// Constants
import {CLOUD_URL, CLOUD_CHECKOUT_PATH} from 'src/shared/constants'
const CloudUpgradeNavBanner: FC = () => {
return (
<>
<CloudOnly>
<Panel
gradient={Gradients.HotelBreakfast}
className="cloud-upgrade-banner"
>
<Panel.Header
size={ComponentSize.ExtraSmall}
justifyContent={JustifyContent.Center}
>
<Heading element={HeadingElement.H5}>
Need more wiggle room?
</Heading>
</Panel.Header>
<Panel.Footer size={ComponentSize.ExtraSmall}>
<Link
className="cf-button cf-button-md cf-button-primary cf-button-stretch cloud-upgrade-banner--button"
to={`${CLOUD_URL}${CLOUD_CHECKOUT_PATH}`}
>
Upgrade Now
</Link>
</Panel.Footer>
</Panel>
<Link
className="cloud-upgrade-banner__collapsed"
to={`${CLOUD_URL}${CLOUD_CHECKOUT_PATH}`}
>
<Icon glyph={IconFont.Star} />
<Heading element={HeadingElement.H5}>Upgrade Now</Heading>
</Link>
</CloudOnly>
</>
)
}
export default CloudUpgradeNavBanner

View File

@ -12,4 +12,9 @@
.load-data--asset-alert {
padding-bottom: 16px;
}
}
// Might need to remove this eventually
.cf-page-header--fixed {
justify-content: space-between;
}

View File

@ -3,7 +3,7 @@ import {PureComponent} from 'react'
import {orderBy} from 'lodash'
// Types
import {Sort} from 'src/clockface/types'
import {Sort} from '@influxdata/clockface'
interface Props<T> {
list: T[]

View File

@ -3,6 +3,7 @@ import {
enablePresentationMode,
disablePresentationMode,
setTheme,
setNavBarState,
setAutoRefresh,
} from 'src/shared/actions/app'
import {TimeZone} from 'src/types'
@ -16,6 +17,7 @@ describe('Shared.Reducers.appReducer', () => {
persisted: {
autoRefresh: 0,
showTemplateControlBar: false,
navBarState: 'expanded',
timeZone: 'Local' as TimeZone,
theme: 'dark',
},
@ -49,6 +51,20 @@ describe('Shared.Reducers.appReducer', () => {
expect(reducedState.persisted.theme).toBe('dark')
})
it('should handle SET_NAV_BAR_STATE to collapsed', () => {
const reducedState = appReducer(initialState, setNavBarState('collapsed'))
expect(reducedState.persisted.navBarState).toBe('collapsed')
})
it('should handle SET_NAV_BAR_STATE to expanded', () => {
Object.assign(initialState, {persisted: {navBarState: 'collapsed'}})
const reducedState = appReducer(initialState, setNavBarState('expanded'))
expect(reducedState.persisted.navBarState).toBe('expanded')
})
it('should handle SET_AUTOREFRESH', () => {
const expectedMs = 15000

View File

@ -3,7 +3,7 @@ import {combineReducers} from 'redux'
// Types
import {ActionTypes, Action} from 'src/shared/actions/app'
import {AUTOREFRESH_DEFAULT_INTERVAL} from 'src/shared/constants'
import {TimeZone} from 'src/types'
import {TimeZone, NavBarState, Theme} from 'src/types'
export interface AppState {
ephemeral: {
@ -13,7 +13,8 @@ export interface AppState {
autoRefresh: number
showTemplateControlBar: boolean
timeZone: TimeZone
theme: 'dark' | 'light'
navBarState: NavBarState
theme: Theme
}
}
@ -26,6 +27,7 @@ const initialState: AppState = {
autoRefresh: AUTOREFRESH_DEFAULT_INTERVAL,
showTemplateControlBar: false,
timeZone: 'Local',
navBarState: 'collapsed',
},
}
@ -80,6 +82,14 @@ const appPersistedReducer = (
return {...state, timeZone}
}
case 'SET_NAV_BAR_STATE': {
const navBarState = action.navBarState
return {
...state,
navBarState,
}
}
default:
return state
}

View File

@ -0,0 +1,66 @@
// Libraries
import React, {FC} from 'react'
import _ from 'lodash'
// Components
import {Tabs, Orientation, ComponentSize} from '@influxdata/clockface'
import CloudExclude from 'src/shared/components/cloud/CloudExclude'
import CloudOnly from 'src/shared/components/cloud/CloudOnly'
import {FeatureFlag} from 'src/shared/utils/featureFlag'
export interface TabbedPageTab {
text: string
id: string
cloudExclude?: boolean
cloudOnly?: boolean
featureFlagName?: string
featureFlagValue?: boolean
}
interface Props {
activeTab: string
tabs: TabbedPageTab[]
onTabClick: (id: string) => void
}
const SettingsNavigation: FC<Props> = ({activeTab, tabs, onTabClick}) => {
return (
<Tabs orientation={Orientation.Horizontal} size={ComponentSize.Large}>
{tabs.map(t => {
let tab = (
<Tabs.Tab
key={t.id}
text={t.text}
id={t.id}
onClick={onTabClick}
active={t.id === activeTab}
/>
)
if (t.cloudExclude) {
tab = <CloudExclude key={t.id}>{tab}</CloudExclude>
}
if (t.cloudOnly) {
tab = <CloudOnly key={t.id}>{tab}</CloudOnly>
}
if (t.featureFlagName) {
tab = (
<FeatureFlag
key={t.id}
name={t.featureFlagName}
equals={t.featureFlagValue}
>
{tab}
</FeatureFlag>
)
}
return tab
})}
</Tabs>
)
}
export default SettingsNavigation

View File

@ -8,6 +8,7 @@ export const OSS_FLAGS = {
customCheckQuery: false,
matchingNotificationRules: false,
regionBasedLoginPage: false,
treeNav: false,
}
export const CLOUD_FLAGS = {
@ -19,6 +20,7 @@ export const CLOUD_FLAGS = {
customCheckQuery: false,
matchingNotificationRules: false,
regionBasedLoginPage: false,
treeNav: false,
}
export const isFlagEnabled = (flagName: string, equals?: string | boolean) => {

View File

@ -75,8 +75,9 @@
@import 'src/me/graphics/ExploreGraphic.scss';
@import 'src/me/graphics/DashboardingGraphic.scss';
@import 'src/me/graphics/CollectorGraphic.scss';
@import 'src/pageLayout/components/RenamablePageTitle.scss';
@import 'src/pageLayout/components/CloudNav.scss';
@import 'src/pageLayout/components/RenamablePageTitle.scss';
@import 'src/pageLayout/components/OrgSwitcherOverlay.scss';
@import 'src/timeMachine/components/SelectorList.scss';
@import 'src/timeMachine/components/Queries.scss';
@import 'src/timeMachine/components/EditorShortcutsTooltip.scss';
@ -110,6 +111,7 @@
@import 'src/shared/components/EventMarkerTooltip.scss';
@import 'src/shared/components/DeleteDataForm/DeleteDataForm.scss';
@import 'src/shared/components/cloud/CloudOnly.scss';
@import 'src/shared/components/CloudUpgradeNavBanner.scss';
@import 'src/notifications/rules/components/NewRuleOverlay.scss';
@import 'src/shared/components/dashed_button/DashedButton.scss';
@import 'src/alerting/components/AlertingIndex.scss';

View File

@ -1,13 +1,14 @@
// Libraries
import React, {PureComponent} from 'react'
// Types
// Components
import {
ComponentColor,
Button,
ComponentStatus,
Page,
} from '@influxdata/clockface'
import CloudUpgradeButton from 'src/shared/components/CloudUpgradeButton'
interface Props {
title: string
@ -23,6 +24,7 @@ export default class TaskHeader extends PureComponent<Props> {
<>
<Page.Header fullWidth={true}>
<Page.Title title={title} />
<CloudUpgradeButton />
</Page.Header>
<Page.ControlBar fullWidth={true}>
<Page.ControlBarRight>

View File

@ -6,6 +6,7 @@ import {withRouter, WithRouterProps} from 'react-router'
// Components
import {Page, IconFont, Sort} from '@influxdata/clockface'
import TaskRunsList from 'src/tasks/components/TaskRunsList'
import CloudUpgradeButton from 'src/shared/components/CloudUpgradeButton'
// Types
import {AppState, RemoteDataState, Task, Run} from 'src/types'
@ -72,6 +73,7 @@ class TaskRunsPage extends PureComponent<Props & WithRouterProps, State> {
<Page titleTag={pageTitleSuffixer(['Task Runs'])}>
<Page.Header fullWidth={false}>
<Page.Title title={this.title} />
<CloudUpgradeButton />
</Page.Header>
<Page.ControlBar fullWidth={false}>
<Page.ControlBarLeft>

View File

@ -10,6 +10,7 @@ import {
Page,
} from '@influxdata/clockface'
import AddResourceDropdown from 'src/shared/components/AddResourceDropdown'
import CloudUpgradeButton from 'src/shared/components/CloudUpgradeButton'
// Types
import {LimitStatus} from 'src/cloud/actions/limits'
@ -37,6 +38,7 @@ export default class TasksHeader extends PureComponent<Props> {
<>
<Page.Header fullWidth={false}>
<Page.Title title="Tasks" />
<CloudUpgradeButton />
</Page.Header>
<Page.ControlBar fullWidth={false}>
<Page.ControlBarLeft>

View File

@ -1,2 +1,3 @@
export type CurrentPage = 'dashboard' | 'not set'
export type Theme = 'light' | 'dark'
export type NavBarState = 'expanded' | 'collapsed'