From 665afcad86781dc950d4c8c49e52111ee31282dd Mon Sep 17 00:00:00 2001 From: Christopher Henn Date: Thu, 28 Jun 2018 12:04:33 -0700 Subject: [PATCH] Fix issue switching between dashboards --- ui/src/dashboards/actions/index.ts | 29 ----- .../dashboards/components/DashboardHeader.tsx | 12 +- .../components/DashboardSwitcher.js | 104 ------------------ .../components/DashboardSwitcher.tsx | 89 +++++++++++++++ .../dashboards/containers/DashboardPage.tsx | 58 ++++++---- ui/src/hosts/containers/HostPage.js | 37 +++++-- ui/src/types/dashboards.ts | 6 + 7 files changed, 171 insertions(+), 164 deletions(-) delete mode 100644 ui/src/dashboards/components/DashboardSwitcher.js create mode 100644 ui/src/dashboards/components/DashboardSwitcher.tsx diff --git a/ui/src/dashboards/actions/index.ts b/ui/src/dashboards/actions/index.ts index 49fa24e97..86db269b0 100644 --- a/ui/src/dashboards/actions/index.ts +++ b/ui/src/dashboards/actions/index.ts @@ -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 -): Promise => { - 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 => { diff --git a/ui/src/dashboards/components/DashboardHeader.tsx b/ui/src/dashboards/components/DashboardHeader.tsx index 877985ced..60408f149 100644 --- a/ui/src/dashboards/components/DashboardHeader.tsx +++ b/ui/src/dashboards/components/DashboardHeader.tsx @@ -32,7 +32,8 @@ interface Props { zoomedTimeRange: QueriesModels.TimeRange onCancel: () => void onSave: (name: string) => Promise - names: DashboardsModels.DashboardName[] + dashboardLinks: DashboardsModels.DashboardSwitcherLink[] + activeDashboardLink?: DashboardsModels.DashboardSwitcherLink isHidden: boolean } @@ -145,11 +146,14 @@ class DashboardHeader extends Component { } 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 ( - + ) } } diff --git a/ui/src/dashboards/components/DashboardSwitcher.js b/ui/src/dashboards/components/DashboardSwitcher.js deleted file mode 100644 index a6d0a8097..000000000 --- a/ui/src/dashboards/components/DashboardSwitcher.js +++ /dev/null @@ -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 ( -
- -
    - - {this.sortedList.map(({name, link}) => ( - - ))} - -
-
- ) - } - - get sortedList() { - const {names} = this.props - return _.sortBy(names, ({name}) => name.toLowerCase()) - } -} - -const NameLink = ({name, link, activeName, onClose}) => ( -
  • - - {name} - -
  • -) - -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) diff --git a/ui/src/dashboards/components/DashboardSwitcher.tsx b/ui/src/dashboards/components/DashboardSwitcher.tsx new file mode 100644 index 000000000..c6c50cd02 --- /dev/null +++ b/ui/src/dashboards/components/DashboardSwitcher.tsx @@ -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 { + constructor(props) { + super(props) + + this.state = {isOpen: false} + } + + public render() { + const {isOpen} = this.state + + const openClass = isOpen ? 'open' : '' + + return ( +
    + +
      + + {this.links} + +
    +
    + ) + } + + 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 ( +
  • + + {link.text} + +
  • + ) + }) + } +} + +export default OnClickOutside(DashboardSwitcher) diff --git a/ui/src/dashboards/containers/DashboardPage.tsx b/ui/src/dashboards/containers/DashboardPage.tsx index 4b0871d9c..3147a7378 100644 --- a/ui/src/dashboards/containers/DashboardPage.tsx +++ b/ui/src/dashboards/containers/DashboardPage.tsx @@ -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 { } 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 { 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 { handleHideCellEditorOverlay, handleClickPresentationButton, } = this.props - const {dashboardsNames} = this.state - const low = zoomedLower || lower const up = zoomedUpper || upper @@ -283,7 +290,6 @@ class DashboardPage extends Component { /> ) : null} { 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 { ) } - private getDashboardsNames = async (): Promise => { - 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 { 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 { 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}}) => { diff --git a/ui/src/hosts/containers/HostPage.js b/ui/src/hosts/containers/HostPage.js index 6205e81d1..d1844a814 100644 --- a/ui/src/hosts/containers/HostPage.js +++ b/ui/src/hosts/containers/HostPage.js @@ -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 (
    ) } + + 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 diff --git a/ui/src/types/dashboards.ts b/ui/src/types/dashboards.ts index c020743ec..1f6ba457c 100644 --- a/ui/src/types/dashboards.ts +++ b/ui/src/types/dashboards.ts @@ -128,3 +128,9 @@ export enum ThresholdType { BG = 'background', Base = 'base', } + +export interface DashboardSwitcherLink { + key: string + text: string + to: string +}