GUI: Add tenant dropdown to top menu
On a non whitelabeled setup, allow a user to jump from one tenant to another without having to go back to the tenants page. On a whitelabeled setup, make the tenant item non-clickable (the click doesn't do anything anyway). Change-Id: I94d27445c65ed5c3f8d02fae9d47d426528d2332
This commit is contained in:
parent
c0985cff39
commit
d3f5b9890e
105
web/src/App.jsx
105
web/src/App.jsx
|
@ -32,6 +32,8 @@ import {
|
|||
ButtonVariant,
|
||||
Dropdown,
|
||||
DropdownItem,
|
||||
DropdownToggle,
|
||||
DropdownSeparator,
|
||||
KebabToggle,
|
||||
Modal,
|
||||
Nav,
|
||||
|
@ -54,6 +56,7 @@ import {
|
|||
import {
|
||||
BellIcon,
|
||||
BookIcon,
|
||||
ChevronDownIcon,
|
||||
CodeIcon,
|
||||
ServiceIcon,
|
||||
UsersIcon,
|
||||
|
@ -67,6 +70,7 @@ import ConfigModal from './containers/config/Config'
|
|||
import logo from './images/logo.svg'
|
||||
import { clearNotification } from './actions/notifications'
|
||||
import { fetchConfigErrorsAction, clearConfigErrorsAction } from './actions/configErrors'
|
||||
import { fetchTenantsIfNeeded } from './actions/tenants'
|
||||
import { routes } from './routes'
|
||||
import { setTenantAction } from './actions/tenant'
|
||||
import { configureAuthFromTenant, configureAuthFromInfo } from './actions/auth'
|
||||
|
@ -81,6 +85,7 @@ class App extends React.Component {
|
|||
configErrorsReady: PropTypes.bool,
|
||||
info: PropTypes.object,
|
||||
tenant: PropTypes.object,
|
||||
tenants: PropTypes.object,
|
||||
timezone: PropTypes.string,
|
||||
location: PropTypes.object,
|
||||
history: PropTypes.object,
|
||||
|
@ -93,6 +98,7 @@ class App extends React.Component {
|
|||
|
||||
state = {
|
||||
showErrors: false,
|
||||
isTenantDropdownOpen: false,
|
||||
}
|
||||
|
||||
renderMenu() {
|
||||
|
@ -199,6 +205,7 @@ class App extends React.Component {
|
|||
} else if (!info.tenant) {
|
||||
// Multi tenant, look for tenant name in url
|
||||
whiteLabel = false
|
||||
this.props.dispatch(fetchTenantsIfNeeded())
|
||||
|
||||
const match = matchPath(
|
||||
this.props.location.pathname, { path: '/t/:tenant' })
|
||||
|
@ -368,6 +375,91 @@ class App extends React.Component {
|
|||
)
|
||||
}
|
||||
|
||||
renderTenantDropdown() {
|
||||
const { tenant, tenants } = this.props
|
||||
const { isTenantDropdownOpen } = this.state
|
||||
|
||||
if (tenant.whiteLabel) {
|
||||
return (
|
||||
<PageHeaderToolsItem>
|
||||
<strong>Tenant</strong> {tenant.name}
|
||||
</PageHeaderToolsItem>
|
||||
)
|
||||
} else {
|
||||
const tenantLink = (_tenant) => {
|
||||
const currentPath = this.props.location.pathname
|
||||
let suffix
|
||||
switch (currentPath) {
|
||||
case '/t/' + tenant.name + '/projects':
|
||||
suffix = '/projects'
|
||||
break
|
||||
case '/t/' + tenant.name + '/jobs':
|
||||
suffix = '/jobs'
|
||||
break
|
||||
case '/t/' + tenant.name + '/labels':
|
||||
suffix = '/labels'
|
||||
break
|
||||
case '/t/' + tenant.name + '/nodes':
|
||||
suffix = '/nodes'
|
||||
break
|
||||
case '/t/' + tenant.name + '/autoholds':
|
||||
suffix = '/autoholds'
|
||||
break
|
||||
case '/t/' + tenant.name + '/builds':
|
||||
suffix = '/builds'
|
||||
break
|
||||
case '/t/' + tenant.name + '/buildsets':
|
||||
suffix = '/buildsets'
|
||||
break
|
||||
case '/t/' + tenant.name + '/status':
|
||||
default:
|
||||
// all other paths point to tenant-specific resources that would most likely result in a 404
|
||||
suffix = '/status'
|
||||
break
|
||||
}
|
||||
return <Link to={'/t/' + _tenant.name + suffix}>{_tenant.name}</Link>
|
||||
}
|
||||
|
||||
const options = tenants.tenants.filter(
|
||||
(_tenant) => (_tenant.name !== tenant.name)
|
||||
).map(
|
||||
(_tenant, idx) => {
|
||||
return (
|
||||
<DropdownItem key={'tenant-dropdown-' + idx} component={tenantLink(_tenant)} />
|
||||
)
|
||||
})
|
||||
options.push(
|
||||
<DropdownSeparator key="tenant-dropdown-separator" />,
|
||||
<DropdownItem
|
||||
key="tenant-dropdown-tenants_page"
|
||||
component={<Link to={tenant.defaultRoute}>Go to tenants page</Link>} />
|
||||
)
|
||||
|
||||
return (tenants.isFetching ?
|
||||
<PageHeaderToolsItem>
|
||||
Loading tenants ...
|
||||
</PageHeaderToolsItem> :
|
||||
<>
|
||||
<PageHeaderToolsItem>
|
||||
<Dropdown
|
||||
isOpen={isTenantDropdownOpen}
|
||||
toggle={
|
||||
<DropdownToggle
|
||||
className={`zuul-menu-dropdown-toggle${isTenantDropdownOpen ? '-expanded' : ''}`}
|
||||
id="tenant-dropdown-toggle-id"
|
||||
onToggle={(isOpen) => { this.setState({ isTenantDropdownOpen: isOpen }) }}
|
||||
toggleIndicator={ChevronDownIcon}
|
||||
>
|
||||
<strong>Tenant</strong> {tenant.name}
|
||||
</DropdownToggle>}
|
||||
onSelect={() => { this.setState({ isTenantDropdownOpen: !isTenantDropdownOpen }) }}
|
||||
dropdownItems={options}
|
||||
/>
|
||||
</PageHeaderToolsItem>
|
||||
</>)
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isKebabDropdownOpen } = this.state
|
||||
const { notifications, configErrors, tenant, info, auth } = this.props
|
||||
|
@ -406,7 +498,7 @@ class App extends React.Component {
|
|||
key="tenant"
|
||||
onClick={event => this.handleTenantLink(event)}
|
||||
>
|
||||
<UsersIcon /> Tenant
|
||||
<UsersIcon /> Tenants
|
||||
</DropdownItem>
|
||||
)
|
||||
}
|
||||
|
@ -445,15 +537,7 @@ class App extends React.Component {
|
|||
</Button>
|
||||
</a>
|
||||
</PageHeaderToolsItem>
|
||||
{tenant.name && (
|
||||
<PageHeaderToolsItem>
|
||||
<Link to={tenant.defaultRoute}>
|
||||
<Button variant={ButtonVariant.plain}>
|
||||
<strong>Tenant</strong> {tenant.name}
|
||||
</Button>
|
||||
</Link>
|
||||
</PageHeaderToolsItem>
|
||||
)}
|
||||
{tenant.name && (this.renderTenantDropdown())}
|
||||
</PageHeaderToolsGroup>
|
||||
<PageHeaderToolsGroup>
|
||||
{/* this kebab dropdown replaces the icon buttons and is hidden for
|
||||
|
@ -521,6 +605,7 @@ export default withRouter(connect(
|
|||
configErrorsReady: state.configErrors.ready,
|
||||
info: state.info,
|
||||
tenant: state.tenant,
|
||||
tenants: state.tenants,
|
||||
timezone: state.timezone,
|
||||
user: state.user,
|
||||
auth: state.auth,
|
||||
|
|
|
@ -135,8 +135,9 @@ it('renders single tenant', async () => {
|
|||
// Link should be white-label scoped
|
||||
const topMenuLinks = application.root.findAllByType(Link)
|
||||
expect(topMenuLinks[0].props.to).toEqual('/status')
|
||||
expect(topMenuLinks[3].props.to.pathname).toEqual('/status')
|
||||
expect(topMenuLinks[4].props.to.pathname).toEqual('/projects')
|
||||
expect(topMenuLinks[1].props.to).toEqual('/openapi')
|
||||
expect(topMenuLinks[2].props.to.pathname).toEqual('/status')
|
||||
expect(topMenuLinks[3].props.to.pathname).toEqual('/projects')
|
||||
// Location should be /status
|
||||
expect(location.pathname).toEqual('/status')
|
||||
// Info should tell white label tenant openstack
|
||||
|
|
|
@ -12,9 +12,9 @@
|
|||
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import Select from 'react-select'
|
||||
import Select, { components } from 'react-select'
|
||||
import moment from 'moment-timezone'
|
||||
import { OutlinedClockIcon } from '@patternfly/react-icons'
|
||||
import { OutlinedClockIcon, ChevronDownIcon } from '@patternfly/react-icons'
|
||||
import { connect } from 'react-redux'
|
||||
import { setTimezoneAction } from '../../actions/timezone'
|
||||
|
||||
|
@ -58,7 +58,7 @@ class SelectTz extends React.Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const textColor = '#d1d1d1'
|
||||
const textColor = '#fff'
|
||||
const containerStyles= {
|
||||
border: 'solid #2b2b2b',
|
||||
borderWidth: '0 0 0 1px',
|
||||
|
@ -83,7 +83,11 @@ class SelectTz extends React.Component {
|
|||
}),
|
||||
dropdownIndicator:(provided) => ({
|
||||
...provided,
|
||||
padding: '3px'
|
||||
color: '#fff',
|
||||
padding: '3px',
|
||||
':hover': {
|
||||
color: '#fff'
|
||||
}
|
||||
}),
|
||||
indicatorSeparator: () => {},
|
||||
menu: (provided) => ({
|
||||
|
@ -93,12 +97,22 @@ class SelectTz extends React.Component {
|
|||
top: '22px',
|
||||
})
|
||||
}
|
||||
|
||||
const DropdownIndicator = (props) => {
|
||||
return (
|
||||
<components.DropdownIndicator {...props}>
|
||||
<ChevronDownIcon />
|
||||
</components.DropdownIndicator>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={containerStyles}>
|
||||
<OutlinedClockIcon/>
|
||||
<Select
|
||||
className="zuul-select-tz"
|
||||
styles={customStyles}
|
||||
components={{ DropdownIndicator }}
|
||||
value={this.state.currentValue}
|
||||
onChange={this.handleChange}
|
||||
options={this.state.availableTz}
|
||||
|
|
|
@ -66,6 +66,21 @@ a.refresh {
|
|||
font-weight: bold;
|
||||
}
|
||||
|
||||
.zuul-menu-dropdown-toggle:before {
|
||||
content: none !important;
|
||||
}
|
||||
|
||||
.zuul-menu-dropdown-toggle:hover {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.zuul-menu-dropdown-toggle-expanded:before {
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
border-top: none;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
/* Remove ugly outline when a Switch is selected */
|
||||
.pf-c-switch {
|
||||
--pf-c-switch__input--focus__toggle--OutlineWidth: 0;
|
||||
|
|
Loading…
Reference in New Issue