Setting up structure for Auth flow

pull/593/head
Will Piers 2016-11-16 14:26:08 -08:00
parent f3cdf81ab5
commit 1dc53d5759
13 changed files with 74 additions and 628 deletions

36
ui/src/auth/CheckAuth.js Normal file
View File

@ -0,0 +1,36 @@
import React, {PropTypes} from 'react';
import {withRouter} from 'react-router';
const CheckAuth = React.createClass({
propTypes: {
router: PropTypes.shape({
push: PropTypes.func.isRequired,
}).isRequired,
children: PropTypes.node.isRequired,
},
getInitialState() {
return {
loggedIn: false,
};
},
componentDidMount() {
// console.log('checking auth');
// this.setState({
// loggedIn: true,
// });
this.props.router.push('/login');
},
render() {
const {loggedIn} = this.state;
if (!loggedIn) {
return <div>AUTH IS BEING CHECKED</div>;
}
return this.props.children;
},
});
export default withRouter(CheckAuth);

12
ui/src/auth/Login.js Normal file
View File

@ -0,0 +1,12 @@
import React from 'react';
import {withRouter} from 'react-router';
const Login = React.createClass({
render() {
return (
<a className="btn btn-primary" href="/oauth">Click me to log in</a>
);
},
});
export default withRouter(Login);

3
ui/src/auth/index.js Normal file
View File

@ -0,0 +1,3 @@
import Login from './Login';
import CheckAuth from './CheckAuth';
export {Login, CheckAuth};

View File

@ -1,4 +1,4 @@
import React, {PropTypes} from 'react';
import React from 'react';
import {render} from 'react-dom';
import {Provider} from 'react-redux';
import {Router, Route, browserHistory} from 'react-router';
@ -8,18 +8,16 @@ import AlertsApp from 'src/alerts';
import CheckSources from 'src/CheckSources';
import {HostsPage, HostPage} from 'src/hosts';
import {KubernetesPage} from 'src/kubernetes';
import {CheckAuth, Login} from 'src/auth';
import {KapacitorPage, KapacitorRulePage, KapacitorRulesPage, KapacitorTasksPage} from 'src/kapacitor';
import DataExplorer from 'src/chronograf';
import {CreateSource, SourceForm, ManageSources} from 'src/sources';
import NotFound from 'src/shared/components/NotFound';
import NoClusterError from 'src/shared/components/NoClusterError';
import configureStore from 'src/store/configureStore';
import {getSources} from 'shared/apis';
import 'src/style/enterprise_style/application.scss';
const {number, shape, string, bool} = PropTypes;
const defaultTimeRange = {upper: null, lower: 'now() - 15m'};
const lsTimeRange = window.localStorage.getItem('timeRange');
const parsedTimeRange = JSON.parse(lsTimeRange) || {};
@ -28,38 +26,7 @@ const timeRange = Object.assign(defaultTimeRange, parsedTimeRange);
const store = configureStore({timeRange});
const rootNode = document.getElementById('react-root');
const HTTP_SERVER_ERROR = 500;
const Root = React.createClass({
getInitialState() {
return {
me: {
id: 1,
name: 'Chronograf',
email: 'foo@example.com',
admin: true,
},
isFetching: false,
hasReadPermission: false,
clusterStatus: null,
};
},
childContextTypes: {
me: shape({
id: number.isRequired,
name: string.isRequired,
email: string.isRequired,
admin: bool.isRequired,
}),
},
getChildContext() {
return {
me: this.state.me,
};
},
activeSource(sources) {
const defaultSource = sources.find((s) => s.default);
if (defaultSource && defaultSource.id) {
@ -79,36 +46,31 @@ const Root = React.createClass({
},
render() {
if (this.state.isFetching) {
return null;
}
if (this.state.clusterStatus === HTTP_SERVER_ERROR) {
return <NoClusterError />;
}
return (
<Provider store={store}>
<Router history={browserHistory}>
<Route path="/" component={CreateSource} onEnter={this.redirectToHosts} />
<Route path="/sources/:sourceID" component={App}>
<Route component={CheckSources}>
<Route path="manage-sources" component={ManageSources} />
<Route path="manage-sources/new" component={SourceForm} />
<Route path="manage-sources/:id/edit" component={SourceForm} />
<Route path="chronograf/data-explorer" component={DataExplorer} />
<Route path="chronograf/data-explorer/:base64ExplorerID" component={DataExplorer} />
<Route path="hosts" component={HostsPage} />
<Route path="hosts/:hostID" component={HostPage} />
<Route path="kubernetes" component={KubernetesPage} />
<Route path="kapacitor-config" component={KapacitorPage} />
<Route path="kapacitor-tasks" component={KapacitorTasksPage} />
<Route path="alerts" component={AlertsApp} />
<Route path="alert-rules" component={KapacitorRulesPage} />
<Route path="alert-rules/:ruleID" component={KapacitorRulePage} />
<Route path="alert-rules/new" component={KapacitorRulePage} />
<Route path="/login" component={Login} />
<Route component={CheckAuth}>
<Route path="/" component={CreateSource} onEnter={this.redirectToHosts} />
<Route path="/sources/:sourceID" component={App}>
<Route component={CheckSources}>
<Route path="manage-sources" component={ManageSources} />
<Route path="manage-sources/new" component={SourceForm} />
<Route path="manage-sources/:id/edit" component={SourceForm} />
<Route path="chronograf/data-explorer" component={DataExplorer} />
<Route path="chronograf/data-explorer/:base64ExplorerID" component={DataExplorer} />
<Route path="hosts" component={HostsPage} />
<Route path="hosts/:hostID" component={HostPage} />
<Route path="kubernetes" component={KubernetesPage} />
<Route path="kapacitor-config" component={KapacitorPage} />
<Route path="kapacitor-tasks" component={KapacitorTasksPage} />
<Route path="alerts" component={AlertsApp} />
<Route path="alert-rules" component={KapacitorRulesPage} />
<Route path="alert-rules/:ruleID" component={KapacitorRulePage} />
<Route path="alert-rules/new" component={KapacitorRulePage} />
</Route>
<Route path="*" component={NotFound} />
</Route>
<Route path="*" component={NotFound} />
</Route>
</Router>
</Provider>

View File

@ -1,79 +0,0 @@
import React, {PropTypes} from 'react';
const {arrayOf, number, shape, func, string} = PropTypes;
const AddClusterAccounts = React.createClass({
propTypes: {
clusters: arrayOf(shape({
id: number.isRequired,
cluster_users: arrayOf(shape({
name: string.isRequired,
})),
dipslay_name: string,
cluster_id: string.isRequired,
})).isRequired,
onSelectClusterAccount: func.isRequired,
headerText: string,
},
getDefaultProps() {
return {
headerText: 'Pair With Cluster Accounts',
};
},
handleSelectClusterAccount(e, clusterID) {
this.props.onSelectClusterAccount({
clusterID,
accountName: e.target.value,
});
},
render() {
return (
<div>
{
this.props.clusters.map((cluster, i) => {
return (
<div key={i} className="form-grid">
<div className="form-group col-sm-6">
{i === 0 ? <label>Cluster</label> : null}
<div className="form-control-static">
{cluster.display_name || cluster.cluster_id}
</div>
</div>
<div className="form-group col-sm-6">
{i === 0 ? <label>Account</label> : null}
{this.renderClusterUsers(cluster)}
</div>
</div>
);
})
}
</div>
);
},
renderClusterUsers(cluster) {
if (!cluster.cluster_users) {
return (
<select disabled={true} defaultValue="No cluster accounts" className="form-control" id="cluster-account">
<option>No cluster accounts</option>
</select>
);
}
return (
<select onChange={(e) => this.handleSelectClusterAccount(e, cluster.cluster_id)} className="form-control">
<option value="">No Association</option>
{
cluster.cluster_users.map((cu) => {
return <option value={cu.name} key={cu.name}>{cu.name}</option>;
})
}
</select>
);
},
});
export default AddClusterAccounts;

View File

@ -1,124 +0,0 @@
import React, {PropTypes} from 'react';
const CLUSTER_WIDE_PERMISSIONS = ["CreateDatabase", "AddRemoveNode", "ManageShard", "DropDatabase", "CopyShard", "Rebalance"];
const AddPermissionModal = React.createClass({
propTypes: {
activeCluster: PropTypes.string.isRequired,
permissions: PropTypes.arrayOf(PropTypes.shape({
name: PropTypes.string.isRequired,
displayName: PropTypes.string.isRequired,
description: PropTypes.string.isRequired,
})),
databases: PropTypes.arrayOf(PropTypes.string.isRequired).isRequired,
onAddPermission: PropTypes.func.isRequired,
},
getInitialState() {
return {
selectedPermission: null,
selectedDatabase: '',
};
},
handlePermissionClick(permission) {
this.setState({
selectedPermission: permission,
selectedDatabase: '',
});
},
handleDatabaseChange(e) {
this.setState({selectedDatabase: e.target.value});
},
handleSubmit(e) {
e.preventDefault();
this.props.onAddPermission({
name: this.state.selectedPermission,
resources: [this.state.selectedDatabase],
});
$('#addPermissionModal').modal('hide'); // eslint-disable-line no-undef
},
render() {
const {permissions} = this.props;
return (
<div className="modal fade" id="addPermissionModal" tabIndex="-1" role="dialog">
<div className="modal-dialog">
<div className="modal-content">
<div className="modal-header">
<button type="button" className="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
<h4 className="modal-title">Select a Permission to Add</h4>
</div>
<form onSubmit={this.handleSubmit}>
<div className="modal-body">
<div className="well permission-list">
<ul>
{permissions.map((perm) => {
return (
<li key={perm.name}>
<input onClick={() => this.handlePermissionClick(perm.name)} type="radio" name="permissionName" value={`${perm.name}`} id={`permission-${perm.name}`}></input>
<label htmlFor={`permission-${perm.name}`}>
{perm.displayName}
<br/>
<span className="permission-description">{perm.description}</span>
</label>
</li>
);
})}
</ul>
</div>
{this.renderOptions()}
</div>
{this.renderFooter()}
</form>
</div>
</div>
</div>
);
},
renderFooter() {
return (
<div className="modal-footer">
<button className="btn btn-default" data-dismiss="modal">Cancel</button>
<input disabled={!this.state.selectedPermission} className="btn btn-success" type="submit" value="Add Permission"></input>
</div>
);
},
renderOptions() {
return (
<div>
{this.state.selectedPermission ? this.renderDatabases() : null}
</div>
);
},
renderDatabases() {
const isClusterWide = CLUSTER_WIDE_PERMISSIONS.includes(this.state.selectedPermission);
if (!this.props.databases.length || isClusterWide) {
return null;
}
return (
<div>
<div className="form-grid">
<div className="form-group col-md-12">
<label htmlFor="#permissions-database">Limit Permission to...</label>
<select onChange={this.handleDatabaseChange} className="form-control" name="database" id="permissions-database">
<option value={''}>All Databases</option>
{this.props.databases.map((databaseName, i) => <option key={i}>{databaseName}</option>)}
</select>
</div>
</div>
</div>
);
},
});
export default AddPermissionModal;

View File

@ -1,24 +0,0 @@
import React from 'react';
const {node} = React.PropTypes;
const ClusterError = React.createClass({
propTypes: {
children: node.isRequired,
},
render() {
return (
<div className="container-fluid">
<div className="row">
<div className="col-sm-6 col-sm-offset-3">
<div className="panel panel-error panel-summer">
{this.props.children}
</div>
</div>
</div>
</div>
);
},
});
export default ClusterError;

View File

@ -1,21 +0,0 @@
import React from 'react';
import ClusterError from './ClusterError';
const InsufficientPermissions = React.createClass({
render() {
return (
<ClusterError>
<div className="panel-heading text-center">
<h2 className="deluxe">
{`Your account has insufficient permissions`}
</h2>
</div>
<div className="panel-body text-center">
<h3 className="deluxe">Talk to your admin to get additional permissions for access</h3>
</div>
</ClusterError>
);
},
});
export default InsufficientPermissions;

View File

@ -1,35 +0,0 @@
import React from 'react';
import errorCopy from 'hson!shared/copy/errors.hson';
const NoClusterError = React.createClass({
render() {
return (
<div>
<div className="container">
<div className="row">
<div className="col-sm-6 col-sm-offset-3">
<div className="panel panel-error panel-summer">
<div className="panel-heading text-center">
<h2 className="deluxe">
{errorCopy.noCluster.head}
</h2>
</div>
<div className="panel-body text-center">
<h3 className="deluxe">How to resolve:</h3>
<p>
{errorCopy.noCluster.body}
</p>
<div className="text-center">
<button className="btn btn-center btn-success" onClick={() => window.location.reload()}>My Cluster Is Back Up</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
);
},
});
export default NoClusterError;

View File

@ -1,27 +0,0 @@
import React from 'react';
const NoClusterLinksError = React.createClass({
render() {
return (
<div className="container-fluid">
<div className="row">
<div className="col-sm-6 col-sm-offset-3">
<div className="panel panel-error panel-summer">
<div className="panel-heading text-center">
<h2 className="deluxe">
This user is not associated with any cluster accounts!
</h2>
</div>
<div className="panel-body text-center">
<p>Many features in Chronograf require your user to be associated with a cluster account.</p>
<p>Ask an administrator to associate your user with a cluster account.</p>
</div>
</div>
</div>
</div>
</div>
);
},
});
export default NoClusterLinksError;

View File

@ -1,76 +0,0 @@
import React, {PropTypes} from 'react';
const {arrayOf, shape, string} = PropTypes;
const PermissionsTable = React.createClass({
propTypes: {
permissions: PropTypes.arrayOf(shape({
name: string.isRequired,
displayName: string.isRequired,
description: string.isRequired,
resources: arrayOf(string.isRequired).isRequired,
})).isRequired,
showAddResource: PropTypes.bool,
onRemovePermission: PropTypes.func,
},
getDefaultProps() {
return {
permissions: [],
showAddResource: false,
};
},
handleAddResourceClick() {
// TODO
},
handleRemovePermission(permission) {
this.props.onRemovePermission(permission);
},
render() {
if (!this.props.permissions.length) {
return (
<div className="generic-empty-state">
<span className="icon alert-triangle"></span>
<h4>This Role has no Permissions</h4>
</div>
);
}
return (
<div className="panel-body">
<table className="table permissions-table">
<tbody>
{this.props.permissions.map((p) => (
<tr key={p.name}>
<td>{p.displayName}</td>
<td>
{p.resources.map((resource, i) => <div key={i} className="pill">{resource === '' ? 'All Databases' : resource}</div>)}
{this.props.showAddResource ? (
<div onClick={this.handleAddResourceClick} className="pill-add" data-toggle="modal" data-target="#addPermissionModal">
<span className="icon plus"></span>
</div>
) : null}
</td>
{this.props.onRemovePermission ? (
<td className="remove-permission">
<button
onClick={() => this.handleRemovePermission(p)}
type="button"
className="btn btn-sm btn-link-danger">
Remove
</button>
</td>
) : null}
</tr>
))}
</tbody>
</table>
</div>
);
},
});
export default PermissionsTable;

View File

@ -1,86 +0,0 @@
import React, {PropTypes} from 'react';
import {Link} from 'react-router';
import PermissionsTable from 'src/shared/components/PermissionsTable';
const {arrayOf, bool, func, shape, string} = PropTypes;
const RolePanels = React.createClass({
propTypes: {
roles: arrayOf(shape({
name: string.isRequired,
users: arrayOf(string.isRequired).isRequired,
permissions: arrayOf(shape({
name: string.isRequired,
displayName: string.isRequired,
description: string.isRequired,
resources: arrayOf(string.isRequired).isRequired,
})).isRequired,
})).isRequired,
showUserCount: bool,
onRemoveAccountFromRole: func,
},
getDefaultProps() {
return {
showUserCount: false,
};
},
render() {
const {roles} = this.props;
if (!roles.length) {
return (
<div className="panel panel-default">
<div className="panel-body">
<div className="generic-empty-state">
<span className="icon alert-triangle"></span>
<h4>This user has no roles</h4>
</div>
</div>
</div>
);
}
return (
<div className="panel-group sub-page" role="tablist">
{roles.map((role) => {
const id = role.name.replace(/[^\w]/gi, '');
return (
<div key={role.name} className="panel panel-default">
<div className="panel-heading" role="tab" id={`heading${id}`}>
<h4 className="panel-title u-flex u-ai-center u-jc-space-between">
<a className="collapsed" role="button" data-toggle="collapse" href={`#collapse-role-${id}`}>
<span className="caret"></span>
{role.name}
</a>
<div>
{this.props.showUserCount ? <p>{role.users ? role.users.length : 0} Users</p> : null}
{this.props.onRemoveAccountFromRole ? (
<button
onClick={() => this.props.onRemoveAccountFromRole(role)}
data-toggle="modal"
data-target="#removeAccountFromRoleModal"
type="button"
className="btn btn-sm btn-link">
Remove
</button>
) : null}
<Link to={`/roles/${encodeURIComponent(role.name)}`} className="btn btn-xs btn-link">
Go To Role
</Link>
</div>
</h4>
</div>
<div id={`collapse-role-${id}`} className="panel-collapse collapse" role="tabpanel">
<PermissionsTable permissions={role.permissions} />
</div>
</div>
);
})}
</div>
);
},
});
export default RolePanels;

View File

@ -1,95 +0,0 @@
import React, {PropTypes} from 'react';
import {Link} from 'react-router';
import classNames from 'classnames';
const {func, shape, arrayOf, string} = PropTypes;
const UsersTable = React.createClass({
propTypes: {
users: arrayOf(shape({}).isRequired).isRequired,
activeCluster: string.isRequired,
onUserToDelete: func.isRequired,
me: shape({}).isRequired,
deleteText: string,
},
getDefaultProps() {
return {
deleteText: 'Delete',
};
},
handleSelectUserToDelete(user) {
this.props.onUserToDelete(user);
},
render() {
const {users, activeCluster, me} = this.props;
if (!users.length) {
return (
<div className="generic-empty-state">
<span className="icon user-outline"/>
<h4>No users</h4>
</div>
);
}
return (
<table className="table v-center users-table">
<tbody>
<tr>
<th></th>
<th>Name</th>
<th>Admin</th>
<th>Email</th>
<th></th>
</tr>
{
users.map((user) => {
const isMe = me.id === user.id;
return (
<tr key={user.id}>
<td></td>
<td>
<span>
<Link to={`/clusters/${activeCluster}/users/${user.id}`} title={`Go to ${user.name}'s profile`}>{user.name}</Link>
{isMe ? <em> (You) </em> : null}
</span>
</td>
<td className="admin-column">{this.renderAdminIcon(user.admin)}</td>
<td>{user.email}</td>
<td>
{this.renderDeleteButton(user)}
</td>
</tr>
);
})
}
</tbody>
</table>
);
},
renderAdminIcon(isAdmin) {
return <span className={classNames("icon", {"checkmark text-color-success": isAdmin, "remove text-color-danger": !isAdmin})}></span>;
},
renderDeleteButton(user) {
if (this.props.me.id === user.id) {
return <button type="button" className="btn btn-sm btn-link-danger disabled" title={`Cannot ${this.props.deleteText} Yourself`}>{this.props.deleteText}</button>;
}
return (
<button
onClick={() => this.handleSelectUserToDelete({id: user.id, name: user.name})}
type="button"
data-toggle="modal"
data-target="#deleteUsersModal"
className="btn btn-sm btn-link-danger"
>
{this.props.deleteText}
</button>
);
},
});
export default UsersTable;