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:
Matthieu Huin 2021-09-24 21:03:28 +02:00
parent c0985cff39
commit d3f5b9890e
4 changed files with 131 additions and 16 deletions

View File

@ -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,

View File

@ -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

View File

@ -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}

View File

@ -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;