Merge pull request #122 from influxdata/feature/source-scoping

Feature/source scoping
pull/10616/head
Timothy J. Raymond 2016-09-28 15:32:45 -07:00 committed by GitHub
commit c967a87519
15 changed files with 144 additions and 72 deletions

View File

@ -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 (
<div className="enterprise-wrapper--flex">
<SideNavContainer addFlashMessage={this.props.addFlashMessage} currentLocation={this.props.location.pathname} />
<SideNavContainer sourceID={sourceID} addFlashMessage={this.props.addFlashMessage} currentLocation={this.props.location.pathname} />
<div className="page-wrapper">
{this.props.children && React.cloneElement(this.props.children, {
addFlashMessage: this.props.addFlashMessage,

View File

@ -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;
@ -11,6 +11,15 @@ const CheckDataNodes = React.createClass({
propTypes: {
addFlashMessage: func,
children: node,
params: PropTypes.shape({
sourceID: PropTypes.string,
}).isRequired,
router: PropTypes.shape({
push: PropTypes.func.isRequired,
}).isRequired,
location: PropTypes.shape({
pathname: PropTypes.string.isRequired,
}).isRequired,
},
contextTypes: {
@ -25,14 +34,22 @@ 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
const {router, location} = this.props;
return router.push(`/?redirectPath=${location.pathname}`);
}
this.setState({
sources,
source,
isFetching: false,
});
}).catch((err) => {
@ -46,16 +63,10 @@ const CheckDataNodes = React.createClass({
return <div className="page-spinner" />;
}
const {sources} = this.state;
if (!sources.length) {
// this should probably be changed....
return <NoClusterError />;
}
return this.props.children && React.cloneElement(this.props.children, Object.assign({}, this.props, {
sources,
source: this.state.source,
}));
},
});
export default CheckDataNodes;
export default withRouter(CheckDataNodes);

View File

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

View File

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

View File

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

View File

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

View File

@ -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) => {

View File

@ -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 (
<div className="data-explorer-container">
<DataExplorer sources={this.props.sources} explorerID={this.decodeID(base64ExplorerID)} />
<DataExplorer source={this.props.source} explorerID={this.decodeID(base64ExplorerID)} />
</div>
);
},

View File

@ -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() {

View File

@ -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 (
<div className="hosts">

View File

@ -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({
<Provider store={store}>
<Router history={browserHistory}>
<Route path="/signup/admin/:step" component={SignUp} />
<Route path="/" component={App}>
<Route path="/" component={SelectSourcePage} />
<Route path="/sources/:sourceID" component={App}>
<Route component={CheckDataNodes}>
<IndexRoute component={SelectSourcePage} />
<Route path="overview" component={OverviewPage} />
<Route path="queries" component={QueriesPage} />
<Route path="accounts" component={ClusterAccountsPage} />

View File

@ -1,8 +1,20 @@
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,
location: PropTypes.shape({
query: PropTypes.shape({
redirectPath: PropTypes.string,
}).isRequired,
}).isRequired,
},
getInitialState() {
return {
sources: [],
@ -19,8 +31,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.redirectToApp(source);
},
handleNewSource(e) {
@ -32,11 +44,22 @@ export const SelectSourcePage = React.createClass({
password: this.sourcePassword.value,
};
createSource(source).then(() => {
// redirect to /hosts?sourceId=123
// this.redirectToApp(sourceFromServer)
});
},
redirectToApp(source) {
const {redirectPath} = this.props.location.query;
if (!redirectPath) {
return 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.redirectPath;
return (
<div id="select-source-page">
<div className="container">
@ -47,6 +70,7 @@ export const SelectSourcePage = React.createClass({
<h2 className="deluxe">Welcome to Chronograf</h2>
</div>
<div className="panel-body">
{error ? <p className="alert alert-danger">Data source not found or unavailable</p> : null}
<form onSubmit={this.handleSelectSource}>
<div className="form-group col-sm-12">
<h4>Select an InfluxDB server to connect to</h4>
@ -90,4 +114,4 @@ export const SelectSourcePage = React.createClass({
},
});
export default FlashMessages(SelectSourcePage);
export default FlashMessages(withRouter(SelectSourcePage));

View File

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

View File

@ -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 (
<NavBar location={location}>
<div className="sidebar__logo">
<span className="icon cubo-uniform"></span>
</div>
<NavBlock matcher={'users'} icon={"access-key"} link={`/users`}>
<NavHeader link={`/users`} title="Web Admin" />
<NavListItem matcher={'users'} link={`/users`}>Users</NavListItem>
<NavBlock matcher={'users'} icon={"access-key"} link={`${sourcePrefix}/users`}>
<NavHeader link={`${sourcePrefix}/users`} title="Web Admin" />
<NavListItem matcher={'users'} link={`${sourcePrefix}/users`}>Users</NavListItem>
</NavBlock>
<NavBlock matcher="overview" icon="crown" link={`/overview`}>
<NavHeader link={`/overview`} title="Cluster" />
<NavListItem matcher="overview" link={`/overview`}>Overview</NavListItem>
<NavListItem matcher="queries" link={`/queries`}>Queries</NavListItem>
<NavListItem matcher="tasks" link={`/tasks`}>Tasks</NavListItem>
<NavListItem matcher="roles" link={`/roles`}>Roles</NavListItem>
<NavListItem matcher="accounts" link={`/accounts`}>Cluster Accounts</NavListItem>
<NavListItem matcher="manager" link={`/databases/manager/_internal`}>Database Manager</NavListItem>
<NavListItem matcher="retentionpolicies" link={`/databases/retentionpolicies/_internal`}>Retention Policies</NavListItem>
<NavBlock matcher="overview" icon="crown" link={`${sourcePrefix}/overview`}>
<NavHeader link={`${sourcePrefix}/overview`} title="Cluster" />
<NavListItem matcher="overview" link={`${sourcePrefix}/overview`}>Overview</NavListItem>
<NavListItem matcher="queries" link={`${sourcePrefix}/queries`}>Queries</NavListItem>
<NavListItem matcher="tasks" link={`${sourcePrefix}/tasks`}>Tasks</NavListItem>
<NavListItem matcher="roles" link={`${sourcePrefix}/roles`}>Roles</NavListItem>
<NavListItem matcher="accounts" link={`${sourcePrefix}/accounts`}>Cluster Accounts</NavListItem>
<NavListItem matcher="manager" link={`${sourcePrefix}/databases/manager/_internal`}>Database Manager</NavListItem>
<NavListItem matcher="retentionpolicies" link={`${sourcePrefix}/databases/retentionpolicies/_internal`}>Retention Policies</NavListItem>
</NavBlock>
<NavBlock matcher="chronograf" icon="graphline" link={`/chronograf/data_explorer`}>
<NavHeader link={`/chronograf/data_explorer`} title={'Chronograf'} />
<NavListItem matcher={'data_explorer'} link={`/chronograf/data_explorer`}>Data Explorer</NavListItem>
<NavBlock matcher="chronograf" icon="graphline" link={`${sourcePrefix}/chronograf/data_explorer`}>
<NavHeader link={`${sourcePrefix}/chronograf/data_explorer`} title={'Chronograf'} />
<NavListItem matcher={'data_explorer'} link={`${sourcePrefix}/chronograf/data_explorer`}>Data Explorer</NavListItem>
</NavBlock>
<NavBlock matcher="settings" icon="user-outline" link={`/account/settings`}>
<NavHeader link={`/account/settings`} title="My Account" />
<NavListItem matcher="settings" link={`/account/settings`}>Settings</NavListItem>
<NavBlock matcher="settings" icon="user-outline" link={`${sourcePrefix}/account/settings`}>
<NavHeader link={`${sourcePrefix}/account/settings`} title="My Account" />
<NavListItem matcher="settings" link={`${sourcePrefix}/account/settings`}>Settings</NavListItem>
<a className="sidebar__menu-item" href="/logout">Logout</a>
</NavBlock>
<NavBlock matcher="hosts" icon="cpu" link={`/hosts`}>
<NavHeader link={`/hosts`} title="Infrastructure" />
<NavListItem matcher="hosts" link={`/hosts`}>Host List</NavListItem>
<NavBlock matcher="hosts" icon="cpu" link={`${sourcePrefix}/hosts`}>
<NavHeader link={`${sourcePrefix}/hosts`} title="Infrastructure" />
<NavListItem matcher="hosts" link={`${sourcePrefix}/hosts`}>Host List</NavListItem>
</NavBlock>
</NavBar>
);

View File

@ -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 (
<SideNav
sourceID={sourceID}
isAdmin={true}
canViewChronograf={canViewChronograf}
location={currentLocation}