Merge pull request #122 from influxdata/feature/source-scoping
Feature/source scopingpull/10616/head
commit
c967a87519
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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}
|
||||
|
|
Loading…
Reference in New Issue