From 1a98b7302c735bdcffb5c4454efd5aa5224c681f Mon Sep 17 00:00:00 2001 From: Will Piers Date: Mon, 26 Sep 2016 13:08:25 -0700 Subject: [PATCH 1/5] Scope pages by sourceID --- ui/src/App.js | 7 ++- ui/src/CheckDataNodes.js | 12 +++-- ui/src/chronograf/actions/view/index.js | 6 +-- ui/src/chronograf/components/DatabaseList.js | 14 ++++-- ui/src/chronograf/components/FieldList.js | 10 ++-- .../chronograf/components/MeasurementList.js | 12 +++-- ui/src/chronograf/components/Visualization.js | 10 ++-- ui/src/chronograf/containers/App.js | 7 ++- ui/src/chronograf/containers/DataExplorer.js | 12 +++-- ui/src/hosts/containers/HostsPage.js | 13 +++++- ui/src/index.js | 6 +-- .../containers/SelectSourcePage.js | 17 +++++-- ui/src/side_nav/components/NavItems.js | 2 +- ui/src/side_nav/components/SideNav.js | 46 ++++++++++--------- ui/src/side_nav/containers/SideNavApp.js | 4 +- 15 files changed, 114 insertions(+), 64 deletions(-) diff --git a/ui/src/App.js b/ui/src/App.js index e0cd5e7d7d..28bd7c5dac 100644 --- a/ui/src/App.js +++ b/ui/src/App.js @@ -9,12 +9,17 @@ const App = React.createClass({ location: PropTypes.shape({ pathname: PropTypes.string, }), + params: PropTypes.shape({ + sourceID: PropTypes.string.isRequired, + }).isRequired, }, render() { + const {sourceID} = this.props.params; + return (
- +
{this.props.children && React.cloneElement(this.props.children, { addFlashMessage: this.props.addFlashMessage, diff --git a/ui/src/CheckDataNodes.js b/ui/src/CheckDataNodes.js index 83ec93caf5..d69bd6a3d5 100644 --- a/ui/src/CheckDataNodes.js +++ b/ui/src/CheckDataNodes.js @@ -11,6 +11,9 @@ const CheckDataNodes = React.createClass({ propTypes: { addFlashMessage: func, children: node, + params: PropTypes.shape({ + sourceID: PropTypes.string, + }).isRequired, }, contextTypes: { @@ -46,14 +49,17 @@ const CheckDataNodes = React.createClass({ return
; } + const {sourceID} = this.props.params; const {sources} = this.state; - if (!sources.length) { - // this should probably be changed.... + const source = sources.find((s) => s.id === sourceID); + if (!source) { + // the id in the address bar doesn't match a source we know about + // ask paul? go to source selection page? return ; } return this.props.children && React.cloneElement(this.props.children, Object.assign({}, this.props, { - sources, + source, })); }, }); diff --git a/ui/src/chronograf/actions/view/index.js b/ui/src/chronograf/actions/view/index.js index 0ffb55f2fb..6ffadd9eed 100644 --- a/ui/src/chronograf/actions/view/index.js +++ b/ui/src/chronograf/actions/view/index.js @@ -227,11 +227,11 @@ function loadExplorer(explorer) { }; } -export function fetchExplorers({sourceLink, userID, explorerID, push}) { +export function fetchExplorers({source, userID, explorerID, push}) { return (dispatch) => { dispatch({type: 'FETCH_EXPLORERS'}); AJAX({ - url: `${sourceLink}/users/${userID}/explorations`, + url: `${source.links.self}/users/${userID}/explorations`, }).then(({data: {explorations}}) => { const explorers = explorations.map(parseRawExplorer); dispatch(loadExplorers(explorers)); @@ -249,7 +249,7 @@ export function fetchExplorers({sourceLink, userID, explorerID, push}) { if (!explorerID) { const explorer = _.maxBy(explorers, (ex) => ex.updated_at); dispatch(loadExplorer(explorer)); - push(`/chronograf/data_explorer/${btoa(explorer.link.href)}`); + push(`/sources/${source.id}/chronograf/data_explorer/${btoa(explorer.link.href)}`); return; } diff --git a/ui/src/chronograf/components/DatabaseList.js b/ui/src/chronograf/components/DatabaseList.js index 957b8bca45..fde47ff5d7 100644 --- a/ui/src/chronograf/components/DatabaseList.js +++ b/ui/src/chronograf/components/DatabaseList.js @@ -13,7 +13,11 @@ const DatabaseList = React.createClass({ }, contextTypes: { - sources: PropTypes.arrayOf(PropTypes.shape().isRequired).isRequired, + source: PropTypes.shape({ + links: PropTypes.shape({ + proxy: PropTypes.string.isRequired, + }).isRequired, + }).isRequired, }, getInitialState() { @@ -23,16 +27,16 @@ const DatabaseList = React.createClass({ }, componentDidMount() { - const {sources} = this.context; - const source = sources[0].links.proxy; - showDatabases(source).then((resp) => { + const {source} = this.context; + const proxy = source.links.proxy; + showDatabases(proxy).then((resp) => { const {errors, databases} = showDatabasesParser(resp.data); if (errors.length) { // do something } const namespaces = []; - showRetentionPolicies(source, databases).then((res) => { + showRetentionPolicies(proxy, databases).then((res) => { res.data.results.forEach((result, index) => { const {errors: errs, retentionPolicies} = showRetentionPoliciesParser(result); if (errs.length) { diff --git a/ui/src/chronograf/components/FieldList.js b/ui/src/chronograf/components/FieldList.js index 67556d3f47..eef6fd2660 100644 --- a/ui/src/chronograf/components/FieldList.js +++ b/ui/src/chronograf/components/FieldList.js @@ -19,7 +19,11 @@ const FieldList = React.createClass({ }, contextTypes: { - sources: PropTypes.arrayOf(PropTypes.shape().isRequired).isRequired, + source: PropTypes.shape({ + links: PropTypes.shape({ + proxy: PropTypes.string.isRequired, + }).isRequired, + }).isRequired, }, getInitialState() { @@ -34,8 +38,8 @@ const FieldList = React.createClass({ return; } - const {sources} = this.context; - const proxySource = sources[0].links.proxy; + const {source} = this.context; + const proxySource = source.links.proxy; showFieldKeys(proxySource, database, measurement).then((resp) => { const {errors, fieldSets} = showFieldKeysParser(resp.data); if (errors.length) { diff --git a/ui/src/chronograf/components/MeasurementList.js b/ui/src/chronograf/components/MeasurementList.js index 5bae7b451f..b807ca009a 100644 --- a/ui/src/chronograf/components/MeasurementList.js +++ b/ui/src/chronograf/components/MeasurementList.js @@ -15,7 +15,11 @@ const MeasurementList = React.createClass({ }, contextTypes: { - sources: PropTypes.arrayOf(PropTypes.shape().isRequired).isRequired, + source: PropTypes.shape({ + links: PropTypes.shape({ + proxy: PropTypes.string.isRequired, + }).isRequired, + }).isRequired, }, getInitialState() { @@ -30,9 +34,9 @@ const MeasurementList = React.createClass({ return; } - const {sources} = this.context; - const source = sources[0].links.proxy; - showMeasurements(source, this.props.query.database).then((resp) => { + const {source} = this.context; + const proxy = source.links.proxy; + showMeasurements(proxy, this.props.query.database).then((resp) => { const {errors, measurementSets} = showMeasurementsParser(resp.data); if (errors.length) { // TODO: display errors in the UI. diff --git a/ui/src/chronograf/components/Visualization.js b/ui/src/chronograf/components/Visualization.js index a17b92c278..6cbe71f78c 100644 --- a/ui/src/chronograf/components/Visualization.js +++ b/ui/src/chronograf/components/Visualization.js @@ -19,7 +19,11 @@ const Visualization = React.createClass({ }, contextTypes: { - sources: arrayOf(shape()).isRequired, + source: shape({ + links: shape({ + proxy: string.isRequired, + }).isRequired, + }).isRequired, }, getInitialState() { @@ -42,8 +46,8 @@ const Visualization = React.createClass({ render() { const {queryConfigs, timeRange, isActive, name} = this.props; - const {sources} = this.context; - const proxyLink = sources[0].links.proxy; + const {source} = this.context; + const proxyLink = source.links.proxy; const {isGraphInView} = this.state; const statements = queryConfigs.map((query) => { diff --git a/ui/src/chronograf/containers/App.js b/ui/src/chronograf/containers/App.js index 6046425ef9..2c36fd6d10 100644 --- a/ui/src/chronograf/containers/App.js +++ b/ui/src/chronograf/containers/App.js @@ -6,13 +6,12 @@ import DataExplorer from './DataExplorer'; const App = React.createClass({ propTypes: { - sources: PropTypes.arrayOf(PropTypes.shape({ + source: PropTypes.shape({ links: PropTypes.shape({ proxy: PropTypes.string.isRequired, self: PropTypes.string.isRequired, }).isRequired, }).isRequired, - ).isRequired, fetchExplorers: PropTypes.func.isRequired, router: PropTypes.shape({ push: PropTypes.func.isRequired, @@ -25,7 +24,7 @@ const App = React.createClass({ componentDidMount() { const {base64ExplorerID} = this.props.params; this.props.fetchExplorers({ - sourceLink: this.props.sources[0].links.self, + source: this.props.source, userID: 1, // TODO: get the userID explorerID: base64ExplorerID ? this.decodeID(base64ExplorerID) : null, push: this.props.router.push, @@ -36,7 +35,7 @@ const App = React.createClass({ const {base64ExplorerID} = this.props.params; return (
- +
); }, diff --git a/ui/src/chronograf/containers/DataExplorer.js b/ui/src/chronograf/containers/DataExplorer.js index bd1e70d367..342af6ff1a 100644 --- a/ui/src/chronograf/containers/DataExplorer.js +++ b/ui/src/chronograf/containers/DataExplorer.js @@ -15,7 +15,12 @@ import { const DataExplorer = React.createClass({ propTypes: { - sources: PropTypes.arrayOf(PropTypes.shape()).isRequired, + source: PropTypes.shape({ + links: PropTypes.shape({ + proxy: PropTypes.string.isRequired, + self: PropTypes.string.isRequired, + }).isRequired, + }).isRequired, explorers: PropTypes.shape({}).isRequired, explorerID: PropTypes.string, timeRange: PropTypes.shape({ @@ -30,17 +35,16 @@ const DataExplorer = React.createClass({ }, childContextTypes: { - sources: PropTypes.arrayOf(PropTypes.shape({ + source: PropTypes.shape({ links: PropTypes.shape({ proxy: PropTypes.string.isRequired, self: PropTypes.string.isRequired, }).isRequired, }).isRequired, - ).isRequired, }, getChildContext() { - return {sources: this.props.sources}; + return {source: this.props.source}; }, getInitialState() { diff --git a/ui/src/hosts/containers/HostsPage.js b/ui/src/hosts/containers/HostsPage.js index 475e95a877..af9151a404 100644 --- a/ui/src/hosts/containers/HostsPage.js +++ b/ui/src/hosts/containers/HostsPage.js @@ -4,11 +4,20 @@ import HostsTable from '../components/HostsTable'; export const HostsPage = React.createClass({ propTypes: { - sources: PropTypes.arrayOf(React.PropTypes.object), + source: PropTypes.shape({ + id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + type: PropTypes.string.isRequired, // 'influx-enterprise' + username: PropTypes.string.isRequired, + links: PropTypes.shape({ + proxy: PropTypes.string.isRequired, + }).isRequired, + }).isRequired, }, render() { - const {sources} = this.props; + const {source} = this.props; + const sources = [source]; return (
diff --git a/ui/src/index.js b/ui/src/index.js index ffc5c51f1d..97d1c8b242 100644 --- a/ui/src/index.js +++ b/ui/src/index.js @@ -1,7 +1,7 @@ import React, {PropTypes} from 'react'; import {render} from 'react-dom'; import {Provider} from 'react-redux'; -import {Router, Route, browserHistory, IndexRoute} from 'react-router'; +import {Router, Route, browserHistory} from 'react-router'; import App from 'src/App'; import CheckDataNodes from 'src/CheckDataNodes'; @@ -120,9 +120,9 @@ const Root = React.createClass({ - + + - diff --git a/ui/src/select_source/containers/SelectSourcePage.js b/ui/src/select_source/containers/SelectSourcePage.js index 083b076a08..5341c43c38 100644 --- a/ui/src/select_source/containers/SelectSourcePage.js +++ b/ui/src/select_source/containers/SelectSourcePage.js @@ -1,8 +1,15 @@ -import React from 'react'; +import React, {PropTypes} from 'react'; +import {withRouter} from 'react-router'; import FlashMessages from 'shared/components/FlashMessages'; import {createSource, getSources} from 'shared/apis'; export const SelectSourcePage = React.createClass({ + propTypes: { + router: PropTypes.shape({ + push: PropTypes.func.isRequired, + }).isRequired, + }, + getInitialState() { return { sources: [], @@ -19,8 +26,8 @@ export const SelectSourcePage = React.createClass({ handleSelectSource(e) { e.preventDefault(); - // const source = this.state.sources.find((s) => s.name === this.selectedSource.value); - // redirect to /hosts?sourceId=source.id + const source = this.state.sources.find((s) => s.name === this.selectedSource.value); + this.props.router.push(`/sources/${source.id}/hosts`); }, handleNewSource(e) { @@ -32,7 +39,7 @@ export const SelectSourcePage = React.createClass({ password: this.sourcePassword.value, }; createSource(source).then(() => { - // redirect to /hosts?sourceId=123 + // this.props.router.push(`/sources/${source.id}/hosts`); }); }, @@ -90,4 +97,4 @@ export const SelectSourcePage = React.createClass({ }, }); -export default FlashMessages(SelectSourcePage); +export default FlashMessages(withRouter(SelectSourcePage)); diff --git a/ui/src/side_nav/components/NavItems.js b/ui/src/side_nav/components/NavItems.js index 3e2c248050..75d7248f5b 100644 --- a/ui/src/side_nav/components/NavItems.js +++ b/ui/src/side_nav/components/NavItems.js @@ -101,7 +101,7 @@ const NavBar = React.createClass({ }, render() { - const children = React.Children.map((this.props.children), (child) => { + const children = React.Children.map(this.props.children, (child) => { if (child && child.type === NavBlock) { return React.cloneElement(child, { location: this.props.location, diff --git a/ui/src/side_nav/components/SideNav.js b/ui/src/side_nav/components/SideNav.js index db39bc4988..8bff3b4917 100644 --- a/ui/src/side_nav/components/SideNav.js +++ b/ui/src/side_nav/components/SideNav.js @@ -5,42 +5,44 @@ const {string} = PropTypes; const SideNav = React.createClass({ propTypes: { location: string.isRequired, + sourceID: string.isRequired, }, render() { - const {location} = this.props; + const {location, sourceID} = this.props; + const sourcePrefix = `/sources/${sourceID}`; return (
- - - Users + + + Users - - - Overview - Queries - Tasks - Roles - Cluster Accounts - Database Manager - Retention Policies + + + Overview + Queries + Tasks + Roles + Cluster Accounts + Database Manager + Retention Policies - - - Data Explorer + + + Data Explorer - - - Settings + + + Settings Logout - - - Host List + + + Host List
); diff --git a/ui/src/side_nav/containers/SideNavApp.js b/ui/src/side_nav/containers/SideNavApp.js index 55c760aa88..e4bdc43a8f 100644 --- a/ui/src/side_nav/containers/SideNavApp.js +++ b/ui/src/side_nav/containers/SideNavApp.js @@ -6,6 +6,7 @@ const SideNavApp = React.createClass({ propTypes: { currentLocation: string.isRequired, addFlashMessage: func.isRequired, + sourceID: string.isRequired, }, contextTypes: { @@ -20,11 +21,12 @@ const SideNavApp = React.createClass({ }, render() { - const {currentLocation} = this.props; + const {currentLocation, sourceID} = this.props; const {canViewChronograf} = this.context; return ( Date: Mon, 26 Sep 2016 14:15:28 -0700 Subject: [PATCH 2/5] Redirect to source page if we receive a bad source --- ui/src/CheckDataNodes.js | 29 ++++++++++--------- .../containers/SelectSourcePage.js | 7 +++++ 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/ui/src/CheckDataNodes.js b/ui/src/CheckDataNodes.js index d69bd6a3d5..a3a44f5f09 100644 --- a/ui/src/CheckDataNodes.js +++ b/ui/src/CheckDataNodes.js @@ -1,5 +1,5 @@ import React, {PropTypes} from 'react'; -import NoClusterError from 'shared/components/NoClusterError'; +import {withRouter} from 'react-router'; import {getSources} from 'src/shared/apis'; const {bool, number, string, node, func, shape} = PropTypes; @@ -14,6 +14,9 @@ const CheckDataNodes = React.createClass({ params: PropTypes.shape({ sourceID: PropTypes.string, }).isRequired, + router: PropTypes.shape({ + push: PropTypes.func.isRequired, + }).isRequired, }, contextTypes: { @@ -28,14 +31,21 @@ const CheckDataNodes = React.createClass({ getInitialState() { return { isFetching: true, - sources: [], + source: null, }; }, componentDidMount() { getSources().then(({data: {sources}}) => { + const {sourceID} = this.props.params; + const source = sources.find((s) => s.id === sourceID); + + if (!source) { // would be great to check source.status or similar here + return this.props.router.push(`/?error="bad id: ${sourceID}"`); + } + this.setState({ - sources, + source, isFetching: false, }); }).catch((err) => { @@ -49,19 +59,10 @@ const CheckDataNodes = React.createClass({ return
; } - const {sourceID} = this.props.params; - const {sources} = this.state; - const source = sources.find((s) => s.id === sourceID); - if (!source) { - // the id in the address bar doesn't match a source we know about - // ask paul? go to source selection page? - return ; - } - return this.props.children && React.cloneElement(this.props.children, Object.assign({}, this.props, { - source, + source: this.state.source, })); }, }); -export default CheckDataNodes; +export default withRouter(CheckDataNodes); diff --git a/ui/src/select_source/containers/SelectSourcePage.js b/ui/src/select_source/containers/SelectSourcePage.js index 5341c43c38..11441442a9 100644 --- a/ui/src/select_source/containers/SelectSourcePage.js +++ b/ui/src/select_source/containers/SelectSourcePage.js @@ -8,6 +8,11 @@ export const SelectSourcePage = React.createClass({ router: PropTypes.shape({ push: PropTypes.func.isRequired, }).isRequired, + location: PropTypes.shape({ + query: PropTypes.shape({ + error: PropTypes.string, + }).isRequired, + }).isRequired, }, getInitialState() { @@ -44,6 +49,7 @@ export const SelectSourcePage = React.createClass({ }, render() { + const error = !!this.props.location.query.error; return (
@@ -54,6 +60,7 @@ export const SelectSourcePage = React.createClass({

Welcome to Chronograf

+ {error ?

bad id bro

: null}

Select an InfluxDB server to connect to

From 63028457af704230621f74f3a2fe4e2b5f392948 Mon Sep 17 00:00:00 2001 From: Will Piers Date: Mon, 26 Sep 2016 15:56:23 -0700 Subject: [PATCH 3/5] Fix redirect after choosing source --- ui/src/CheckDataNodes.js | 6 +++++- .../containers/SelectSourcePage.js | 18 ++++++++++++++---- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/ui/src/CheckDataNodes.js b/ui/src/CheckDataNodes.js index a3a44f5f09..538022a6a6 100644 --- a/ui/src/CheckDataNodes.js +++ b/ui/src/CheckDataNodes.js @@ -17,6 +17,9 @@ const CheckDataNodes = React.createClass({ router: PropTypes.shape({ push: PropTypes.func.isRequired, }).isRequired, + location: PropTypes.shape({ + pathname: PropTypes.string.isRequired, + }).isRequired, }, contextTypes: { @@ -41,7 +44,8 @@ const CheckDataNodes = React.createClass({ const source = sources.find((s) => s.id === sourceID); if (!source) { // would be great to check source.status or similar here - return this.props.router.push(`/?error="bad id: ${sourceID}"`); + const {router, location} = this.props; + return router.push(`/?redirectPath=${location.pathname}`); } this.setState({ diff --git a/ui/src/select_source/containers/SelectSourcePage.js b/ui/src/select_source/containers/SelectSourcePage.js index 11441442a9..6a1344d036 100644 --- a/ui/src/select_source/containers/SelectSourcePage.js +++ b/ui/src/select_source/containers/SelectSourcePage.js @@ -10,7 +10,7 @@ export const SelectSourcePage = React.createClass({ }).isRequired, location: PropTypes.shape({ query: PropTypes.shape({ - error: PropTypes.string, + redirectPath: PropTypes.string, }).isRequired, }).isRequired, }, @@ -32,7 +32,7 @@ export const SelectSourcePage = React.createClass({ handleSelectSource(e) { e.preventDefault(); const source = this.state.sources.find((s) => s.name === this.selectedSource.value); - this.props.router.push(`/sources/${source.id}/hosts`); + this.redirectToApp(source); }, handleNewSource(e) { @@ -44,12 +44,22 @@ export const SelectSourcePage = React.createClass({ password: this.sourcePassword.value, }; createSource(source).then(() => { - // this.props.router.push(`/sources/${source.id}/hosts`); + // this.redirectToApp(sourceFromServer) }); }, + redirectToApp(source) { + const {redirectPath} = this.props.location.query; + if (!redirectPath) { + this.props.router.push(`/sources/${source.id}/hosts`); + } + + const fixedPath = redirectPath.replace(/\/sources\/[^/]*/, `/sources/${source.id}`); + return this.props.router.push(fixedPath); + }, + render() { - const error = !!this.props.location.query.error; + const error = !!this.props.location.query.redirectPath; return (
From 232cbc3ac25f138adac04362780aeccb5678e7a7 Mon Sep 17 00:00:00 2001 From: Will Piers Date: Wed, 28 Sep 2016 11:44:18 -0700 Subject: [PATCH 4/5] Bugfix on source selection page Missed a return --- ui/src/select_source/containers/SelectSourcePage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/select_source/containers/SelectSourcePage.js b/ui/src/select_source/containers/SelectSourcePage.js index 6a1344d036..edb431b48e 100644 --- a/ui/src/select_source/containers/SelectSourcePage.js +++ b/ui/src/select_source/containers/SelectSourcePage.js @@ -51,7 +51,7 @@ export const SelectSourcePage = React.createClass({ redirectToApp(source) { const {redirectPath} = this.props.location.query; if (!redirectPath) { - this.props.router.push(`/sources/${source.id}/hosts`); + return this.props.router.push(`/sources/${source.id}/hosts`); } const fixedPath = redirectPath.replace(/\/sources\/[^/]*/, `/sources/${source.id}`); From 212a60cf9a9b6a51aa5f1a2928781b5372e96499 Mon Sep 17 00:00:00 2001 From: Tim Raymond Date: Wed, 28 Sep 2016 15:24:06 -0700 Subject: [PATCH 5/5] Improve error message copy for missing data source --- ui/src/select_source/containers/SelectSourcePage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/select_source/containers/SelectSourcePage.js b/ui/src/select_source/containers/SelectSourcePage.js index edb431b48e..6bb56c2ff2 100644 --- a/ui/src/select_source/containers/SelectSourcePage.js +++ b/ui/src/select_source/containers/SelectSourcePage.js @@ -70,7 +70,7 @@ export const SelectSourcePage = React.createClass({

Welcome to Chronograf

- {error ?

bad id bro

: null} + {error ?

Data source not found or unavailable

: null}

Select an InfluxDB server to connect to