Fix issue switching between dashboards

pull/10616/head
Christopher Henn 2018-06-28 12:04:33 -07:00 committed by Chris Henn
parent 352cf0e0d0
commit 577276f52d
7 changed files with 171 additions and 164 deletions

View File

@ -280,35 +280,6 @@ export const getDashboardsAsync: DashboardsActions.GetDashboardsDispatcher = ():
}
}
// gets update-to-date names of dashboards, but does not dispatch action
// in order to avoid duplicate and out-of-sync state problems in redux
export const getDashboardsNamesAsync: DashboardsActions.GetDashboardsNamesDispatcher = (
sourceID: string
): DashboardsActions.GetDashboardsNamesThunk => async (
dispatch: Dispatch<ErrorsActions.ErrorThrownActionCreator>
): Promise<DashboardsModels.DashboardName[] | void> => {
try {
// TODO: change this from getDashboardsAJAX to getDashboardsNamesAJAX
// to just get dashboard names (and links) as api view call when that
// view API is implemented (issue #3594), rather than getting whole
// dashboard for each
const {
data: {dashboards},
} = (await getDashboardsAJAX()) as AxiosResponse<
DashboardsApis.DashboardsResponse
>
const dashboardsNames = dashboards.map(({id, name}) => ({
id,
name,
link: `/sources/${sourceID}/dashboards/${id}`,
}))
return dashboardsNames
} catch (error) {
console.error(error)
dispatch(errorThrown(error))
}
}
export const getDashboardAsync = (dashboardID: number) => async (
dispatch
): Promise<DashboardsModels.Dashboard | null> => {

View File

@ -32,7 +32,8 @@ interface Props {
zoomedTimeRange: QueriesModels.TimeRange
onCancel: () => void
onSave: (name: string) => Promise<void>
names: DashboardsModels.DashboardName[]
dashboardLinks: DashboardsModels.DashboardSwitcherLink[]
activeDashboardLink?: DashboardsModels.DashboardSwitcherLink
isHidden: boolean
}
@ -145,11 +146,14 @@ class DashboardHeader extends Component<Props> {
}
private get dashboardSwitcher(): JSX.Element {
const {names, activeDashboard} = this.props
const {dashboardLinks, activeDashboardLink} = this.props
if (names && names.length > 1) {
if (dashboardLinks.length > 1) {
return (
<DashboardSwitcher names={names} activeDashboard={activeDashboard} />
<DashboardSwitcher
links={dashboardLinks}
activeLink={activeDashboardLink}
/>
)
}
}

View File

@ -1,104 +0,0 @@
import React, {Component} from 'react'
import PropTypes from 'prop-types'
import {Link} from 'react-router'
import _ from 'lodash'
import classnames from 'classnames'
import OnClickOutside from 'shared/components/OnClickOutside'
import {ErrorHandling} from 'src/shared/decorators/errors'
import FancyScrollbar from 'src/shared/components/FancyScrollbar'
import {DROPDOWN_MENU_MAX_HEIGHT} from 'src/shared/constants/index'
@ErrorHandling
class DashboardSwitcher extends Component {
constructor(props) {
super(props)
this.state = {
isOpen: false,
}
}
handleToggleMenu = () => {
this.setState({isOpen: !this.state.isOpen})
}
handleCloseMenu = () => {
this.setState({isOpen: false})
}
handleClickOutside = () => {
this.setState({isOpen: false})
}
render() {
const {activeDashboard} = this.props
const {isOpen} = this.state
return (
<div
className={classnames('dropdown dashboard-switcher', {open: isOpen})}
>
<button
className="btn btn-square btn-default btn-sm dropdown-toggle"
onClick={this.handleToggleMenu}
>
<span className="icon dash-h" />
</button>
<ul className="dropdown-menu">
<FancyScrollbar
autoHeight={true}
maxHeight={DROPDOWN_MENU_MAX_HEIGHT}
>
{this.sortedList.map(({name, link}) => (
<NameLink
key={link}
name={name}
link={link}
activeName={activeDashboard}
onClose={this.handleCloseMenu}
/>
))}
</FancyScrollbar>
</ul>
</div>
)
}
get sortedList() {
const {names} = this.props
return _.sortBy(names, ({name}) => name.toLowerCase())
}
}
const NameLink = ({name, link, activeName, onClose}) => (
<li
className={classnames('dropdown-item', {
active: name === activeName,
})}
>
<Link to={link} onClick={onClose}>
{name}
</Link>
</li>
)
const {arrayOf, func, shape, string} = PropTypes
DashboardSwitcher.propTypes = {
activeDashboard: string.isRequired,
names: arrayOf(
shape({
link: string.isRequired,
name: string.isRequired,
})
).isRequired,
}
NameLink.propTypes = {
name: string.isRequired,
link: string.isRequired,
activeName: string.isRequired,
onClose: func.isRequired,
}
export default OnClickOutside(DashboardSwitcher)

View File

@ -0,0 +1,89 @@
import React, {PureComponent} from 'react'
import {Link} from 'react-router'
import _ from 'lodash'
import OnClickOutside from 'src/shared/components/OnClickOutside'
import FancyScrollbar from 'src/shared/components/FancyScrollbar'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {DROPDOWN_MENU_MAX_HEIGHT} from 'src/shared/constants/index'
import {DashboardSwitcherLink} from 'src/types/dashboards'
interface Props {
links: DashboardSwitcherLink[]
activeLink?: DashboardSwitcherLink
}
interface State {
isOpen: boolean
}
@ErrorHandling
class DashboardSwitcher extends PureComponent<Props, State> {
constructor(props) {
super(props)
this.state = {isOpen: false}
}
public render() {
const {isOpen} = this.state
const openClass = isOpen ? 'open' : ''
return (
<div className={`dropdown dashboard-switcher ${openClass}`}>
<button
className="btn btn-square btn-default btn-sm dropdown-toggle"
onClick={this.handleToggleMenu}
>
<span className="icon dash-h" />
</button>
<ul className="dropdown-menu">
<FancyScrollbar
autoHeight={true}
maxHeight={DROPDOWN_MENU_MAX_HEIGHT}
>
{this.links}
</FancyScrollbar>
</ul>
</div>
)
}
public handleClickOutside = () => {
this.setState({isOpen: false})
}
private handleToggleMenu = () => {
this.setState({isOpen: !this.state.isOpen})
}
private handleCloseMenu = () => {
this.setState({isOpen: false})
}
private get links(): JSX.Element[] {
const {links, activeLink} = this.props
return _.sortBy(links, ['text', 'key']).map(link => {
let activeClass = ''
if (activeLink && link.key === activeLink.key) {
activeClass = 'active'
}
return (
<li key={link.key} className={`dropdown-item ${activeClass}`}>
<Link to={link.to} onClick={this.handleCloseMenu}>
{link.text}
</Link>
</li>
)
})
}
}
export default OnClickOutside(DashboardSwitcher)

View File

@ -58,7 +58,7 @@ interface DashboardActions {
syncURLQueryParamsFromQueryParamsObject: DashboardsActions.SyncURLQueryFromQueryParamsObjectDispatcher
putDashboard: DashboardsActions.PutDashboardDispatcher
putDashboardByID: DashboardsActions.PutDashboardByIDDispatcher
getDashboardsNamesAsync: DashboardsActions.GetDashboardsNamesDispatcher
getDashboardsAsync: DashboardsActions.GetDashboardsDispatcher
getDashboardWithHydratedAndSyncedTempVarsAsync: DashboardsActions.GetDashboardWithHydratedAndSyncedTempVarsAsyncDispatcher
setTimeRange: DashboardsActions.SetTimeRangeActionCreator
addDashboardCellAsync: DashboardsActions.AddDashboardCellDispatcher
@ -135,7 +135,13 @@ class DashboardPage extends Component<Props, State> {
}
public async componentDidMount() {
const {source, getAnnotationsAsync, timeRange, autoRefresh} = this.props
const {
source,
getAnnotationsAsync,
timeRange,
autoRefresh,
getDashboardsAsync,
} = this.props
const annotationRange = millisecondTimeRange(timeRange)
getAnnotationsAsync(source.links.annotations, annotationRange)
@ -150,7 +156,10 @@ class DashboardPage extends Component<Props, State> {
await this.getDashboard()
this.getDashboardsNames()
// We populate all dashboards in the redux store so that we can consume
// them in `this.dashboardLinks`. See
// https://github.com/influxdata/chronograf/issues/3594
getDashboardsAsync()
}
public componentWillReceiveProps(nextProps: Props) {
@ -212,8 +221,6 @@ class DashboardPage extends Component<Props, State> {
handleHideCellEditorOverlay,
handleClickPresentationButton,
} = this.props
const {dashboardsNames} = this.state
const low = zoomedLower || lower
const up = zoomedUpper || upper
@ -283,7 +290,6 @@ class DashboardPage extends Component<Props, State> {
/>
) : null}
<DashboardHeader
names={dashboardsNames}
dashboard={dashboard}
timeRange={timeRange}
isEditMode={isEditMode}
@ -295,6 +301,8 @@ class DashboardPage extends Component<Props, State> {
onSave={this.handleRenameDashboard}
onCancel={this.handleCancelEditDashboard}
onEditDashboard={this.handleEditDashboard}
dashboardLinks={this.dashboardLinks}
activeDashboardLink={this.activeDashboardLink}
activeDashboard={dashboard ? dashboard.name : ''}
showTemplateControlBar={showTemplateControlBar}
handleChooseAutoRefresh={handleChooseAutoRefresh}
@ -353,19 +361,6 @@ class DashboardPage extends Component<Props, State> {
)
}
private getDashboardsNames = async (): Promise<void> => {
const {
params: {sourceID},
} = this.props
// TODO: remove any once react-redux connect is properly typed
const dashboardsNames = (await this.props.getDashboardsNamesAsync(
sourceID
)) as any
this.setState({dashboardsNames})
}
private inView = (cell: DashboardsModels.Cell): boolean => {
const {scrollTop, windowHeight} = this.state
const bufferValue = 600
@ -445,7 +440,6 @@ class DashboardPage extends Component<Props, State> {
this.props.updateDashboard(newDashboard)
await this.props.putDashboard(newDashboard)
this.getDashboardsNames()
}
private handleDeleteDashboardCell = (cell: DashboardsModels.Cell): void => {
@ -511,6 +505,30 @@ class DashboardPage extends Component<Props, State> {
this.setState({scrollTop: target.scrollTop})
}
private get dashboardLinks(): DashboardsModels.DashboardSwitcherLink[] {
const {dashboards, source} = this.props
return dashboards.map(d => {
return {
key: String(d.id),
text: d.name,
to: `/sources/${source.id}/dashboards/${d.id}`,
}
})
}
private get activeDashboardLink(): DashboardsModels.DashboardSwitcherLink | null {
const {dashboard} = this.props
if (!dashboard) {
return null
}
const {dashboardLinks} = this
return dashboardLinks.find(link => link.key === String(dashboard.id))
}
}
const mstp = (state, {params: {dashboardID}}) => {

View File

@ -168,21 +168,16 @@ class HostPage extends Component {
const {
autoRefresh,
onManualRefresh,
params: {hostID, sourceID},
params: {hostID},
inPresentationMode,
handleChooseAutoRefresh,
handleClickPresentationButton,
} = this.props
const {timeRange, hosts} = this.state
const names = _.map(hosts, ({name}) => ({
name,
link: `/sources/${sourceID}/hosts/${name}`,
}))
const {timeRange} = this.state
return (
<div className="page">
<DashboardHeader
names={names}
timeRange={timeRange}
activeDashboard={hostID}
autoRefresh={autoRefresh}
@ -191,6 +186,8 @@ class HostPage extends Component {
handleChooseAutoRefresh={handleChooseAutoRefresh}
handleChooseTimeRange={this.handleChooseTimeRange}
handleClickPresentationButton={handleClickPresentationButton}
dashboardLinks={this.dashboardLinks}
activeDashboardLink={this.activeDashboardLink}
/>
<FancyScrollbar
className={classnames({
@ -205,6 +202,32 @@ class HostPage extends Component {
</div>
)
}
get dashboardLinks() {
const {
params: {sourceID},
} = this.props
const {hosts} = this.state
if (!sourceID || !hosts) {
return []
}
return Object.values(hosts).map(({name}) => ({
key: name,
text: name,
to: `/sources/${sourceID}/hosts/${name}`,
}))
}
get activeDashboardLink() {
const {
params: {hostID},
} = this.props
const {dashboardLinks} = this
return dashboardLinks.find(d => d.key === hostID)
}
}
const {shape, string, bool, func, number} = PropTypes

View File

@ -128,3 +128,9 @@ export enum ThresholdType {
BG = 'background',
Base = 'base',
}
export interface DashboardSwitcherLink {
key: string
text: string
to: string
}