Merge pull request #1117 from influxdata/feature/remove-archive

Remove archived enterprise web content
pull/1116/head
Chris Goller 2017-03-29 15:24:59 -05:00 committed by GitHub
commit cd8c1c2a5f
43 changed files with 0 additions and 3814 deletions

View File

@ -1,133 +0,0 @@
import React, {PropTypes} from 'react';
import {Link} from 'react-router';
const RoleClusterAccounts = React.createClass({
propTypes: {
clusterID: PropTypes.string.isRequired,
users: PropTypes.arrayOf(PropTypes.string.isRequired),
onRemoveClusterAccount: PropTypes.func.isRequired,
},
getDefaultProps() {
return {users: []};
},
getInitialState() {
return {
searchText: '',
};
},
handleSearch(searchText) {
this.setState({searchText});
},
handleRemoveClusterAccount(user) {
this.props.onRemoveClusterAccount(user);
},
render() {
const users = this.props.users.filter((user) => {
const name = user.toLowerCase();
const searchText = this.state.searchText.toLowerCase();
return name.indexOf(searchText) > -1;
});
return (
<div className="panel panel-default">
<div className="panel-body cluster-accounts">
{this.props.users.length ? <SearchBar onSearch={this.handleSearch} searchText={this.state.searchText} /> : null}
{this.props.users.length ? (
<TableBody
users={users}
clusterID={this.props.clusterID}
onRemoveClusterAccount={this.handleRemoveClusterAccount}
/>
) : (
<div className="generic-empty-state">
<span className="icon alert-triangle"></span>
<h4>No Cluster Accounts found</h4>
</div>
)}
</div>
</div>
);
},
});
const TableBody = React.createClass({
propTypes: {
users: PropTypes.arrayOf(PropTypes.string.isRequired),
clusterID: PropTypes.string.isRequired,
onRemoveClusterAccount: PropTypes.func.isRequired,
},
render() {
return (
<table className="table v-center users-table">
<tbody>
<tr>
<th></th>
<th>Username</th>
<th></th>
</tr>
{this.props.users.map((user) => {
return (
<tr key={user} data-row-id={user}>
<td></td>
<td>
<Link to={`/clusters/${this.props.clusterID}/accounts/${user}`} className="btn btn-xs btn-link">
{user}
</Link>
</td>
<td>
<button
title="Remove cluster account from role"
onClick={() => this.props.onRemoveClusterAccount(user)}
type="button"
data-toggle="modal"
data-target="#removeAccountFromRoleModal"
className="btn btn-sm btn-link-danger"
>
Remove From Role
</button>
</td>
</tr>
);
})}
</tbody>
</table>
);
},
});
const SearchBar = React.createClass({
propTypes: {
onSearch: PropTypes.func.isRequired,
searchText: PropTypes.string.isRequired,
},
handleChange() {
this.props.onSearch(this._searchText.value);
},
render() {
return (
<div className="users__search-widget input-group">
<div className="input-group-addon">
<span className="icon search" aria-hidden="true"></span>
</div>
<input
type="text"
className="form-control"
placeholder="Find User"
value={this.props.searchText}
ref={(ref) => this._searchText = ref}
onChange={this.handleChange}
/>
</div>
);
},
});
export default RoleClusterAccounts;

View File

@ -1,74 +0,0 @@
import React, {PropTypes} from 'react';
import {Link} from 'react-router';
const RoleHeader = React.createClass({
propTypes: {
selectedRole: PropTypes.shape(),
roles: PropTypes.arrayOf(PropTypes.shape({
name: PropTypes.string,
})).isRequired,
clusterID: PropTypes.string.isRequired,
activeTab: PropTypes.string,
},
getDefaultProps() {
return {
selectedRole: '',
};
},
render() {
return (
<div className="enterprise-header">
<div className="enterprise-header__container">
<div className="enterprise-header__left">
<div className="dropdown minimal-dropdown">
<button className="dropdown-toggle" type="button" id="roleSelection" data-toggle="dropdown">
<span className="button-text">{this.props.selectedRole.name}</span>
<span className="caret"></span>
</button>
<ul className="dropdown-menu" aria-labelledby="dropdownMenu1">
{this.props.roles.map((role) => (
<li key={role.name}>
<Link to={`/clusters/${this.props.clusterID}/roles/${encodeURIComponent(role.name)}`} className="role-option">
{role.name}
</Link>
</li>
))}
<li role="separator" className="divider"></li>
<li>
<Link to={`/clusters/${this.props.clusterID}/roles`} className="role-option">
All Roles
</Link>
</li>
</ul>
</div>
</div>
<div className="enterprise-header__right">
<button className="btn btn-sm btn-default" data-toggle="modal" data-target="#deleteRoleModal">Delete Role</button>
{this.props.activeTab === 'Permissions' ? (
<button
className="btn btn-sm btn-primary"
data-toggle="modal"
data-target="#addPermissionModal"
>
Add Permission
</button>
) : null}
{this.props.activeTab === 'Cluster Accounts' ? (
<button
className="btn btn-sm btn-primary"
data-toggle="modal"
data-target="#addClusterAccountModal"
>
Add Cluster Account
</button>
) : null}
</div>
</div>
</div>
);
},
});
export default RoleHeader;

View File

@ -1,110 +0,0 @@
import React, {PropTypes} from 'react';
import {Tab, Tabs, TabPanel, TabPanels, TabList} from 'src/shared/components/Tabs';
import RoleHeader from '../components/RoleHeader';
import RoleClusterAccounts from '../components/RoleClusterAccounts';
import PermissionsTable from 'src/shared/components/PermissionsTable';
import AddPermissionModal from 'src/shared/components/AddPermissionModal';
import AddClusterAccountModal from '../components/modals/AddClusterAccountModal';
import DeleteRoleModal from '../components/modals/DeleteRoleModal';
const {arrayOf, string, shape, func} = PropTypes;
const TABS = ['Permissions', 'Cluster Accounts'];
const RolePage = React.createClass({
propTypes: {
// All permissions to populate the "Add permission" modal
allPermissions: arrayOf(shape({
displayName: string.isRequired,
name: string.isRequired,
description: string.isRequired,
})),
// All roles to populate the navigation dropdown
roles: arrayOf(shape({})),
role: shape({
id: string,
name: string.isRequired,
permissions: arrayOf(shape({
displayName: string.isRequired,
name: string.isRequired,
description: string.isRequired,
resources: arrayOf(string.isRequired).isRequired,
})),
}),
databases: arrayOf(string.isRequired),
clusterID: string.isRequired,
roleSlug: string.isRequired,
onRemoveClusterAccount: func.isRequired,
onDeleteRole: func.isRequired,
onAddPermission: func.isRequired,
onAddClusterAccount: func.isRequired,
onRemovePermission: func.isRequired,
},
getInitialState() {
return {activeTab: TABS[0]};
},
handleActivateTab(activeIndex) {
this.setState({activeTab: TABS[activeIndex]});
},
render() {
const {role, roles, allPermissions, databases, clusterID,
onDeleteRole, onRemoveClusterAccount, onAddPermission, onRemovePermission, onAddClusterAccount} = this.props;
return (
<div id="role-edit-page" className="js-role-edit">
<RoleHeader
roles={roles}
selectedRole={role}
clusterID={clusterID}
activeTab={this.state.activeTab}
/>
<div className="container-fluid">
<div className="row">
<div className="col-md-12">
<Tabs onSelect={this.handleActivateTab}>
<TabList>
<Tab>{TABS[0]}</Tab>
<Tab>{TABS[1]}</Tab>
</TabList>
<TabPanels>
<TabPanel>
<PermissionsTable
permissions={role.permissions}
showAddResource={true}
onRemovePermission={onRemovePermission}
/>
</TabPanel>
<TabPanel>
<RoleClusterAccounts
clusterID={clusterID}
users={role.users}
onRemoveClusterAccount={onRemoveClusterAccount}
/>
</TabPanel>
</TabPanels>
</Tabs>
</div>
</div>
</div>
<DeleteRoleModal onDeleteRole={onDeleteRole} roleName={role.name} />
<AddPermissionModal
permissions={allPermissions}
activeCluster={clusterID}
databases={databases}
onAddPermission={onAddPermission}
/>
<AddClusterAccountModal
clusterID={clusterID}
onAddClusterAccount={onAddClusterAccount}
roleClusterAccounts={role.users}
role={role}
/>
</div>
);
},
});
export default RolePage;

View File

@ -1,55 +0,0 @@
import React, {PropTypes} from 'react';
import CreateRoleModal from './modals/CreateRoleModal';
import RolePanels from 'src/shared/components/RolePanels';
const RolesPage = React.createClass({
propTypes: {
roles: PropTypes.arrayOf(PropTypes.shape({
name: PropTypes.string,
users: PropTypes.arrayOf(PropTypes.string),
permissions: PropTypes.arrayOf(PropTypes.shape({
name: PropTypes.string.isRequired,
displayName: PropTypes.string.isRequired,
description: PropTypes.string.isRequired,
resources: PropTypes.arrayOf(PropTypes.string.isRequired).isRequired,
})),
})).isRequired,
onCreateRole: PropTypes.func.isRequired,
clusterID: PropTypes.string.isRequired,
},
handleCreateRole(roleName) {
this.props.onCreateRole(roleName);
},
render() {
return (
<div id="role-index-page">
<div className="enterprise-header">
<div className="enterprise-header__container">
<div className="enterprise-header__left">
<h1>Access Control</h1>
</div>
<div className="enterprise-header__right">
<button className="btn btn-sm btn-primary" data-toggle="modal" data-target="#createRoleModal">Create Role</button>
</div>
</div>
</div>
<div className="container-fluid">
<div className="row">
<div className="col-md-12">
<h3 className="deluxe fake-panel-title match-search">All Roles</h3>
<div className="panel-group sub-page roles" id="role-page" role="tablist">
<RolePanels roles={this.props.roles} clusterID={this.props.clusterID} showUserCount={true} />
</div>
</div>
</div>
</div>
<CreateRoleModal onConfirm={this.handleCreateRole} />
</div>
);
},
});
export default RolesPage;

View File

@ -1,110 +0,0 @@
import React, {PropTypes} from 'react';
import AddClusterAccounts from 'src/shared/components/AddClusterAccounts';
import {getClusterAccounts} from 'src/shared/apis';
const {arrayOf, func, string, shape} = PropTypes;
// Allows a user to add a cluster account to a role. Very similar to other features
// (e.g. adding cluster accounts to a web user), the main difference being that
// we'll only give users the option to select users from the active cluster instead of
// from all clusters.
const AddClusterAccountModal = React.createClass({
propTypes: {
clusterID: string.isRequired,
onAddClusterAccount: func.isRequired,
// Cluster accounts that already belong to a role so we can filter
// the list of available options.
roleClusterAccounts: arrayOf(string),
role: shape({
name: PropTypes.string,
}),
},
getDefaultProps() {
return {roleClusterAccounts: []};
},
getInitialState() {
return {
selectedAccount: null,
clusterAccounts: [],
error: null,
isFetching: true,
};
},
componentDidMount() {
getClusterAccounts(this.props.clusterID).then((resp) => {
this.setState({clusterAccounts: resp.data.users});
}).catch(() => {
this.setState({error: 'An error occured.'});
}).then(() => {
this.setState({isFetching: false});
});
},
handleSubmit(e) {
e.preventDefault();
this.props.onAddClusterAccount(this.state.selectedAccount);
$('#addClusterAccountModal').modal('hide'); // eslint-disable-line no-undef
},
handleSelectClusterAccount({accountName}) {
this.setState({
selectedAccount: accountName,
});
},
render() {
if (this.state.isFetching) {
return null;
}
const {role} = this.props;
// Temporary hack while https://github.com/influxdata/enterprise/issues/948 is resolved.
// We want to use the /api/int/v1/clusters endpoint and just pick the
// Essentially we're taking the raw output from /user and morphing whatthe `AddClusterAccounts`
// modal expects (a cluster with fields defined by the enterprise web database)
const availableClusterAccounts = this.state.clusterAccounts.filter((account) => {
return !this.props.roleClusterAccounts.includes(account.name);
});
const cluster = {
id: 0, // Only used as a `key` prop
cluster_users: availableClusterAccounts,
cluster_id: this.props.clusterID,
};
return (
<div className="modal fade in" id="addClusterAccountModal" 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">Add Cluster Account to <strong>{role.name}</strong></h4>
</div>
<form onSubmit={this.handleSubmit}>
<div className="modal-body">
<div className="alert alert-info">
<span className="icon star"></span>
<p><strong>NOTE:</strong> Cluster Accounts added to a Role inherit all the permissions associated with that Role.</p>
</div>
<AddClusterAccounts
clusters={[cluster]}
onSelectClusterAccount={this.handleSelectClusterAccount}
/>
</div>
<div className="modal-footer">
<button className="btn btn-default" data-dismiss="modal">Cancel</button>
<input disabled={!this.state.selectedAccount} className="btn btn-success" type="submit" value="Confirm" />
</div>
</form>
</div>
</div>
</div>
);
},
});
export default AddClusterAccountModal;

View File

@ -1,51 +0,0 @@
import React, {PropTypes} from 'react';
const CreateRoleModal = React.createClass({
propTypes: {
onConfirm: PropTypes.func.isRequired,
},
handleSubmit(e) {
e.preventDefault();
if (this.roleName.value === '') {
return;
}
this.props.onConfirm(this.roleName.value);
this.roleName.value = '';
$('#createRoleModal').modal('hide'); // eslint-disable-line no-undef
},
render() {
return (
<div className="modal fade in" id="createRoleModal" 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">Create Role</h4>
</div>
<form onSubmit={this.handleSubmit}>
<div className="modal-body">
<div className="form-grid padding-top">
<div className="form-group col-md-8 col-md-offset-2">
<label htmlFor="roleName" className="sr-only">Name this Role</label>
<input ref={(n) => this.roleName = n}name="roleName" type="text" className="form-control input-lg" id="roleName" placeholder="Name this Role" required={true} />
</div>
</div>
</div>
<div className="modal-footer">
<button type="button" className="btn btn-default" data-dismiss="modal">Cancel</button>
<input className="btn btn-success" type="submit" value="Create" />
</div>
</form>
</div>
</div>
</div>
);
},
});
export default CreateRoleModal;

View File

@ -1,40 +0,0 @@
import React, {PropTypes} from 'react';
const {string, func} = PropTypes;
const DeleteRoleModal = React.createClass({
propTypes: {
roleName: string.isRequired,
onDeleteRole: func.isRequired,
},
handleConfirm() {
$('#deleteRoleModal').modal('hide'); // eslint-disable-line no-undef
this.props.onDeleteRole();
},
render() {
return (
<div className="modal fade" id="deleteRoleModal" 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">
Are you sure you want to delete <strong>{this.props.roleName}</strong>?
</h4>
</div>
<div className="modal-footer">
<button className="btn btn-default" type="button" data-dismiss="modal">Cancel</button>
<button onClick={this.handleConfirm} className="btn btn-danger" value="Delete">Delete</button>
</div>
</div>
</div>
</div>
);
},
});
export default DeleteRoleModal;

View File

@ -1,192 +0,0 @@
import React, {PropTypes} from 'react';
import {withRouter} from 'react-router';
import RolePage from '../components/RolePage';
import {showDatabases} from 'src/shared/apis/metaQuery';
import showDatabasesParser from 'shared/parsing/showDatabases';
import {buildRoles, buildAllPermissions} from 'src/shared/presenters';
import {
getRoles,
removeAccountsFromRole,
addAccountsToRole,
deleteRole,
addPermissionToRole,
removePermissionFromRole,
} from 'src/shared/apis';
import _ from 'lodash';
export const RolePageContainer = React.createClass({
propTypes: {
params: PropTypes.shape({
clusterID: PropTypes.string.isRequired,
roleSlug: PropTypes.string.isRequired,
}).isRequired,
router: React.PropTypes.shape({
push: React.PropTypes.func.isRequired,
}).isRequired,
addFlashMessage: PropTypes.func,
dataNodes: PropTypes.arrayOf(PropTypes.string.isRequired),
},
getInitialState() {
return {
role: {},
roles: [],
databases: [],
isFetching: true,
};
},
componentDidMount() {
const {clusterID, roleSlug} = this.props.params;
this.getRole(clusterID, roleSlug);
},
componentWillReceiveProps(nextProps) {
if (this.props.params.roleSlug !== nextProps.params.roleSlug) {
this.setState(this.getInitialState());
this.getRole(nextProps.params.clusterID, nextProps.params.roleSlug);
}
},
getRole(clusterID, roleName) {
this.setState({isFetching: true});
Promise.all([
getRoles(clusterID, roleName),
showDatabases(this.props.dataNodes, this.props.params.clusterID),
]).then(([rolesResp, dbResp]) => {
// Fetch databases for adding permissions/resources
const {errors, databases} = showDatabasesParser(dbResp.data);
if (errors.length) {
this.props.addFlashMessage({
type: 'error',
text: `InfluxDB error: ${errors[0]}`,
});
}
const roles = buildRoles(rolesResp.data.roles);
const activeRole = roles.find(role => role.name === roleName);
this.setState({
role: activeRole,
roles,
databases,
isFetching: false,
});
}).catch(err => {
this.setState({isFetching: false});
console.error(err.toString()); // eslint-disable-line no-console
this.props.addFlashMessage({
type: 'error',
text: `Unable to fetch role! Please try refreshing the page.`,
});
});
},
handleRemoveClusterAccount(username) {
const {clusterID, roleSlug} = this.props.params;
removeAccountsFromRole(clusterID, roleSlug, [username]).then(() => {
this.setState({
role: Object.assign({}, this.state.role, {
users: _.reject(this.state.role.users, (user) => user === username),
}),
});
this.props.addFlashMessage({
type: 'success',
text: 'Cluster account removed from role!',
});
}).catch(err => {
this.addErrorNotification(err);
});
},
handleDeleteRole() {
const {clusterID, roleSlug} = this.props.params;
deleteRole(clusterID, roleSlug).then(() => {
// TODO: add success notification when we're implementing them higher in the tree.
// Right now the notification just gets swallowed when we transition to a new route.
this.props.router.push(`/roles`);
}).catch(err => {
console.error(err.toString()); // eslint-disable-line no-console
this.addErrorNotification(err);
});
},
handleAddPermission(permission) {
const {clusterID, roleSlug} = this.props.params;
addPermissionToRole(clusterID, roleSlug, permission).then(() => {
this.getRole(clusterID, roleSlug);
}).then(() => {
this.props.addFlashMessage({
type: 'success',
text: 'Added permission to role!',
});
}).catch(err => {
this.addErrorNotification(err);
});
},
handleRemovePermission(permission) {
const {clusterID, roleSlug} = this.props.params;
removePermissionFromRole(clusterID, roleSlug, permission).then(() => {
this.setState({
role: Object.assign({}, this.state.role, {
permissions: _.reject(this.state.role.permissions, (p) => p.name === permission.name),
}),
});
this.props.addFlashMessage({
type: 'success',
text: 'Removed permission from role!',
});
}).catch(err => {
this.addErrorNotification(err);
});
},
handleAddClusterAccount(clusterAccountName) {
const {clusterID, roleSlug} = this.props.params;
addAccountsToRole(clusterID, roleSlug, [clusterAccountName]).then(() => {
this.getRole(clusterID, roleSlug);
}).then(() => {
this.props.addFlashMessage({
type: 'success',
text: 'Added cluster account to role!',
});
}).catch(err => {
this.addErrorNotification(err);
});
},
addErrorNotification(err) {
const text = _.result(err, ['response', 'data', 'error', 'toString'], 'An error occurred.');
this.props.addFlashMessage({
type: 'error',
text,
});
},
render() {
if (this.state.isFetching) {
return <div className="page-spinner" />;
}
const {clusterID, roleSlug} = this.props.params;
const {role, roles, databases} = this.state;
return (
<RolePage
role={role}
roles={roles}
allPermissions={buildAllPermissions()}
databases={databases}
roleSlug={roleSlug}
clusterID={clusterID}
onRemoveClusterAccount={this.handleRemoveClusterAccount}
onDeleteRole={this.handleDeleteRole}
onAddPermission={this.handleAddPermission}
onRemovePermission={this.handleRemovePermission}
onAddClusterAccount={this.handleAddClusterAccount}
/>
);
},
});
export default withRouter(RolePageContainer);

View File

@ -1,71 +0,0 @@
import React, {PropTypes} from 'react';
import {getRoles, createRole} from 'src/shared/apis';
import {buildRoles} from 'src/shared/presenters';
import RolesPage from '../components/RolesPage';
import _ from 'lodash';
export const RolesPageContainer = React.createClass({
propTypes: {
params: PropTypes.shape({
clusterID: PropTypes.string.isRequired,
}).isRequired,
addFlashMessage: PropTypes.func,
},
getInitialState() {
return {
roles: [],
};
},
componentDidMount() {
this.fetchRoles();
},
fetchRoles() {
getRoles(this.props.params.clusterID).then((resp) => {
this.setState({
roles: buildRoles(resp.data.roles),
});
}).catch((err) => {
console.error(err.toString()); // eslint-disable-line no-console
this.props.addFlashMessage({
type: 'error',
text: `Unable to fetch roles! Please try refreshing the page.`,
});
});
},
handleCreateRole(roleName) {
createRole(this.props.params.clusterID, roleName)
// TODO: this should be an optimistic update, but we can't guarantee that we'll
// get an error when a user tries to make a duplicate role (we don't want to
// display a role twice). See https://github.com/influxdata/plutonium/issues/538
.then(this.fetchRoles)
.then(() => {
this.props.addFlashMessage({
type: 'success',
text: 'Role created!',
});
})
.catch((err) => {
const text = _.result(err, ['response', 'data', 'error', 'toString'], 'An error occurred.');
this.props.addFlashMessage({
type: 'error',
text,
});
});
},
render() {
return (
<RolesPage
roles={this.state.roles}
onCreateRole={this.handleCreateRole}
clusterID={this.props.params.clusterID}
/>
);
},
});
export default RolesPageContainer;

View File

@ -1,3 +0,0 @@
import RolesPageContainer from './containers/RolesPageContainer';
import RolePageContainer from './containers/RolePageContainer';
export {RolesPageContainer, RolePageContainer};

View File

@ -1,156 +0,0 @@
import React, {PropTypes} from 'react';
const {shape, string, arrayOf, func} = PropTypes;
const AddRoleModal = React.createClass({
propTypes: {
account: shape({
name: string.isRequired,
hash: string,
permissions: arrayOf(shape({
name: string.isRequired,
displayName: string.isRequired,
description: string.isRequired,
resources: arrayOf(string.isRequired).isRequired,
})).isRequired,
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,
}),
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,
})),
onAddRoleToAccount: func.isRequired,
},
getInitialState() {
return {
selectedRole: this.props.roles[0],
};
},
handleChangeRole(e) {
this.setState({selectedRole: this.props.roles.find((role) => role.name === e.target.value)});
},
handleSubmit(e) {
e.preventDefault();
$('#addRoleModal').modal('hide'); // eslint-disable-line no-undef
this.props.onAddRoleToAccount(this.state.selectedRole);
},
render() {
const {account, roles} = this.props;
const {selectedRole} = this.state;
if (!roles.length) {
return (
<div className="modal fade" id="addRoleModal" tabIndex="-1" role="dialog">
<div className="modal-dialog modal-lg">
<div className="modal-content">
<div className="modal-header">
<h4>This cluster account already belongs to all roles.</h4>
</div>
</div>
</div>
</div>
);
}
return (
<div className="modal fade" id="addRoleModal" tabIndex="-1" role="dialog">
<form onSubmit={this.handleSubmit} className="form">
<div className="modal-dialog modal-lg">
<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">Add <strong>{account.name}</strong> to a new Role</h4>
</div>
<div className="modal-body">
<div className="row">
<div className="col-xs-6 col-xs-offset-3">
<label htmlFor="roles-select">Available Roles</label>
<select id="roles-select" onChange={this.handleChangeRole} value={selectedRole.name} className="form-control input-lg" name="roleName">
{roles.map((role) => {
return <option key={role.name} >{role.name}</option>;
})}
</select>
<br/>
</div>
<div className="col-xs-10 col-xs-offset-1">
<h4>Permissions</h4>
<div className="well well-white">
{this.renderRoleTable()}
</div>
</div>
</div>
</div>
<div className="modal-footer">
<button className="btn btn-default" data-dismiss="modal">Cancel</button>
<input className="btn btn-success" type="submit" value="Add to Role" />
</div>
</div>
</div>
</form>
</div>
);
},
renderRoleTable() {
return (
<table className="table permissions-table">
<tbody>
{this.renderPermissions()}
</tbody>
</table>
);
},
renderPermissions() {
const role = this.state.selectedRole;
if (!role.permissions.length) {
return (
<tr className="role-row">
<td>
<div className="generic-empty-state">
<span className="icon alert-triangle"></span>
<h4>This Role has no Permissions</h4>
</div>
</td>
</tr>
);
}
return role.permissions.map((p) => {
return (
<tr key={p.name} className="role-row">
<td>{p.displayName}</td>
<td>
{p.resources.map((resource, i) => (
<div key={i} className="pill">{resource === '' ? 'All Databases' : resource}</div>
))}
</td>
</tr>
);
});
},
});
export default AddRoleModal;

View File

@ -1,83 +0,0 @@
import React, {PropTypes} from 'react';
const AttachWebUsers = React.createClass({
propTypes: {
users: PropTypes.arrayOf(PropTypes.shape()).isRequired,
account: PropTypes.string.isRequired,
onConfirm: PropTypes.func.isRequired,
},
getInitialState() {
return {
selectedUsers: [],
};
},
handleConfirm() {
$('#addWebUsers').modal('hide'); // eslint-disable-line no-undef
this.props.onConfirm(this.state.selectedUsers);
// uncheck all the boxes?
},
handleSelection(e) {
const checked = e.target.checked;
const id = parseInt(e.target.dataset.id, 10);
const user = this.props.users.find((u) => u.id === id);
const newSelectedUsers = this.state.selectedUsers.slice(0);
if (checked) {
newSelectedUsers.push(user);
} else {
const userIndex = newSelectedUsers.find(u => u.id === id);
newSelectedUsers.splice(userIndex, 1);
}
this.setState({selectedUsers: newSelectedUsers});
},
render() {
return (
<div className="modal fade" id="addWebUsers" 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">
Link Web Users to <strong>{this.props.account}</strong>
</h4>
</div>
<div className="row">
<div className="col-xs-10 col-xs-offset-1">
<h4>Web Users</h4>
<div className="well well-white">
<table className="table v-center">
<tbody>
{ // TODO: style this and make it select / collect users
this.props.users.map((u) => {
return (
<tr key={u.name}>
<td><input onChange={this.handleSelection} data-id={u.id} type="checkbox" /></td>
<td>{u.name}</td>
</tr>
);
})
}
</tbody>
</table>
</div>
</div>
</div>
<div className="modal-footer">
<button className="btn btn-default" type="button" data-dismiss="modal">Cancel</button>
<button onClick={this.handleConfirm} className="btn btn-success">Link Users</button>
</div>
</div>
</div>
</div>
);
},
});
export default AttachWebUsers;

View File

@ -1,93 +0,0 @@
import React, {PropTypes} from 'react';
const {string, func, bool} = PropTypes;
const ClusterAccountDetails = React.createClass({
propTypes: {
name: string.isRequired,
onUpdatePassword: func.isRequired,
showDelete: bool,
},
getDefaultProps() {
return {
showDelete: true,
};
},
getInitialState() {
return {
passwordsMatch: true,
};
},
handleSubmit(e) {
e.preventDefault();
const password = this.password.value;
const confirmation = this.confirmation.value;
const passwordsMatch = password === confirmation;
if (!passwordsMatch) {
return this.setState({passwordsMatch});
}
this.props.onUpdatePassword(password);
},
render() {
return (
<div id="settings-page">
<div className="panel panel-default">
<div className="panel-body">
<form onSubmit={this.handleSubmit}>
{this.renderPasswordMismatch()}
<div className="form-group col-sm-12">
<label htmlFor="name">Name</label>
<input disabled={true} className="form-control input-lg" type="name" id="name" name="name" value={this.props.name}/>
</div>
<div className="form-group col-sm-6">
<label htmlFor="password">Password</label>
<input ref={(password) => this.password = password} className="form-control input-lg" type="password" id="password" name="password"/>
</div>
<div className="form-group col-sm-6">
<label htmlFor="password-confirmation">Confirm Password</label>
<input ref={(confirmation) => this.confirmation = confirmation} className="form-control input-lg" type="password" id="password-confirmation" name="confirmation"/>
</div>
<div className="form-group col-sm-6 col-sm-offset-3">
<button className="btn btn-next btn-success btn-lg btn-block" type="submit">Reset Password</button>
</div>
</form>
</div>
</div>
{this.props.showDelete ? (
<div className="panel panel-default delete-account">
<div className="panel-body">
<div className="col-sm-3">
<button
className="btn btn-next btn-danger btn-lg"
type="submit"
data-toggle="modal"
data-target="#deleteClusterAccountModal">
Delete Account
</button>
</div>
<div className="col-sm-9">
<h4>Delete this cluster account</h4>
<p>Beware! We won't be able to recover a cluster account once you've deleted it.</p>
</div>
</div>
</div>
) : null}
</div>
);
},
renderPasswordMismatch() {
if (this.state.passwordsMatch) {
return null;
}
return <div>Passwords do not match</div>;
},
});
export default ClusterAccountDetails;

View File

@ -1,238 +0,0 @@
import React, {PropTypes} from 'react';
import RolePanels from 'src/shared/components/RolePanels';
import PermissionsTable from 'src/shared/components/PermissionsTable';
import UsersTable from 'shared/components/UsersTable';
import ClusterAccountDetails from '../components/ClusterAccountDetails';
import AddRoleModal from '../components/AddRoleModal';
import AddPermissionModal from 'shared/components/AddPermissionModal';
import AttachWebUsers from '../components/AttachWebUsersModal';
import RemoveAccountFromRoleModal from '../components/RemoveAccountFromRoleModal';
import RemoveWebUserModal from '../components/RemoveUserFromAccountModal';
import DeleteClusterAccountModal from '../components/DeleteClusterAccountModal';
import {Tab, TabList, TabPanels, TabPanel, Tabs} from 'shared/components/Tabs';
const {shape, string, func, arrayOf, number, bool} = PropTypes;
const TABS = ['Roles', 'Permissions', 'Account Details', 'Web Users'];
export const ClusterAccountEditPage = React.createClass({
propTypes: {
// All permissions to populate the "Add permission" modal
allPermissions: arrayOf(shape({
displayName: string.isRequired,
name: string.isRequired,
description: string.isRequired,
})),
clusterID: string.isRequired,
accountID: string.isRequired,
account: shape({
name: string.isRequired,
hash: string,
permissions: arrayOf(shape({
name: string.isRequired,
displayName: string.isRequired,
description: string.isRequired,
resources: arrayOf(string.isRequired).isRequired,
})).isRequired,
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,
}),
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,
})),
databases: arrayOf(string.isRequired),
assignedWebUsers: arrayOf(shape({
id: number.isRequired,
name: string.isRequired,
email: string.isRequired,
admin: bool.isRequired,
})),
unassignedWebUsers: arrayOf(shape({
id: number.isRequired,
name: string.isRequired,
email: string.isRequired,
admin: bool.isRequired,
})),
me: shape(),
onUpdatePassword: func.isRequired,
onRemoveAccountFromRole: func.isRequired,
onRemoveWebUserFromAccount: func.isRequired,
onAddRoleToAccount: func.isRequired,
onAddPermission: func.isRequired,
onRemovePermission: func.isRequired,
onAddWebUsersToAccount: func.isRequired,
onDeleteAccount: func.isRequired,
},
getInitialState() {
return {
roleToRemove: {},
userToRemove: {},
activeTab: TABS[0],
};
},
handleActivateTab(activeIndex) {
this.setState({activeTab: TABS[activeIndex]});
},
handleRemoveAccountFromRole(role) {
this.setState({roleToRemove: role});
},
handleUserToRemove(userToRemove) {
this.setState({userToRemove});
},
getUnassignedRoles() {
return this.props.roles.filter(role => {
return !this.props.account.roles.map(r => r.name).includes(role.name);
});
},
render() {
const {clusterID, accountID, account, databases, onAddPermission, me,
assignedWebUsers, unassignedWebUsers, onAddWebUsersToAccount, onRemovePermission, onDeleteAccount} = this.props;
if (!account || !Object.keys(me).length) {
return null; // TODO: 404?
}
return (
<div id="user-edit-page">
<div className="enterprise-header">
<div className="enterprise-header__container">
<div className="enterprise-header__left">
<h1>
{accountID}&nbsp;<span className="label label-warning">Cluster Account</span>
</h1>
</div>
{this.renderActions()}
</div>
</div>
<div className="container-fluid">
<div className="row">
<div className="col-md-12">
<Tabs onSelect={this.handleActivateTab}>
<TabList>
{TABS.map(tab => <Tab key={tab}>{tab}</Tab>)}
</TabList>
<TabPanels>
<TabPanel>
<RolePanels
roles={account.roles}
clusterID={clusterID}
onRemoveAccountFromRole={this.handleRemoveAccountFromRole}
/>
</TabPanel>
<TabPanel>
<PermissionsTable permissions={account.permissions} onRemovePermission={onRemovePermission} />
</TabPanel>
<TabPanel>
<ClusterAccountDetails
showDelete={me.cluster_links.every(cl => cl.cluster_user !== account.name)}
name={account.name}
onUpdatePassword={this.props.onUpdatePassword}
/>
</TabPanel>
<TabPanel>
<div className="panel panel-default">
<div className="panel-body">
<UsersTable
onUserToDelete={this.handleUserToRemove}
activeCluster={clusterID}
users={assignedWebUsers}
me={me}
deleteText="Unlink" />
</div>
</div>
</TabPanel>
</TabPanels>
</Tabs>
</div>
</div>
</div>
<AddPermissionModal
activeCluster={clusterID}
permissions={this.props.allPermissions}
databases={databases}
onAddPermission={onAddPermission}
/>
<RemoveAccountFromRoleModal
roleName={this.state.roleToRemove.name}
onConfirm={() => this.props.onRemoveAccountFromRole(this.state.roleToRemove)}
/>
<AddRoleModal
account={account}
roles={this.getUnassignedRoles()}
onAddRoleToAccount={this.props.onAddRoleToAccount}
/>
<RemoveWebUserModal
account={accountID}
onRemoveWebUser={() => this.props.onRemoveWebUserFromAccount(this.state.userToRemove)}
user={this.state.userToRemove.name}
/>
<AttachWebUsers
account={accountID}
users={unassignedWebUsers}
onConfirm={onAddWebUsersToAccount}
/>
<DeleteClusterAccountModal
account={account}
webUsers={assignedWebUsers}
onConfirm={onDeleteAccount}
/>
</div>
);
},
renderActions() {
const {activeTab} = this.state;
return (
<div className="enterprise-header__right">
{activeTab === 'Roles' ? (
<button
className="btn btn-sm btn-primary"
data-toggle="modal"
data-target="#addRoleModal">
Add to Role
</button>
) : null}
{activeTab === 'Permissions' ? (
<button
className="btn btn-sm btn-primary"
data-toggle="modal"
data-target="#addPermissionModal">
Add Permissions
</button>
) : null}
{activeTab === 'Web Users' ? (
<button
className="btn btn-sm btn-primary"
data-toggle="modal"
data-target="#addWebUsers">
Link Web Users
</button>
) : null}
</div>
);
},
});
export default ClusterAccountEditPage;

View File

@ -1,48 +0,0 @@
import React, {PropTypes} from 'react';
import PageHeader from '../components/PageHeader';
import ClusterAccountsTable from '../components/ClusterAccountsTable';
const ClusterAccountsPage = React.createClass({
propTypes: {
clusterID: PropTypes.string.isRequired,
users: PropTypes.arrayOf(PropTypes.shape({
name: PropTypes.string,
roles: PropTypes.arrayOf(PropTypes.shape({
name: PropTypes.string.isRequired,
})),
})),
roles: PropTypes.arrayOf(PropTypes.shape({
name: PropTypes.shape,
})),
onDeleteAccount: PropTypes.func.isRequired,
onCreateAccount: PropTypes.func.isRequired,
me: PropTypes.shape(),
},
render() {
const {clusterID, users, roles, onCreateAccount, me} = this.props;
return (
<div id="cluster-accounts-page" data-cluster-id={clusterID}>
<PageHeader
roles={roles}
activeCluster={clusterID}
onCreateAccount={onCreateAccount} />
<div className="container-fluid">
<div className="row">
<div className="col-md-12">
<ClusterAccountsTable
users={users}
clusterID={clusterID}
onDeleteAccount={this.props.onDeleteAccount}
me={me}
/>
</div>
</div>
</div>
</div>
);
},
});
export default ClusterAccountsPage;

View File

@ -1,161 +0,0 @@
import React, {PropTypes} from 'react';
import {Link} from 'react-router';
const ClusterAccountsTable = React.createClass({
propTypes: {
clusterID: PropTypes.string.isRequired,
users: PropTypes.arrayOf(PropTypes.shape({
name: PropTypes.string.isRequired,
roles: PropTypes.arrayOf(PropTypes.shape({
name: PropTypes.string.isRequired,
})).isRequired,
})),
onDeleteAccount: PropTypes.func.isRequired,
me: PropTypes.shape(),
},
getInitialState() {
return {
searchText: '',
};
},
handleSearch(searchText) {
this.setState({searchText});
},
handleDeleteAccount(user) {
this.props.onDeleteAccount(user);
},
render() {
const users = this.props.users.filter((user) => {
const name = user.name.toLowerCase();
const searchText = this.state.searchText.toLowerCase();
return name.indexOf(searchText) > -1;
});
return (
<div className="panel panel-minimal">
<div className="panel-heading u-flex u-jc-space-between u-ai-center">
<h2 className="panel-title">Cluster Accounts</h2>
<SearchBar onSearch={this.handleSearch} searchText={this.state.searchText} />
</div>
<div className="panel-body">
<TableBody
users={users}
clusterID={this.props.clusterID}
onDeleteAccount={this.handleDeleteAccount}
me={this.props.me}
/>
</div>
</div>
);
},
});
const TableBody = React.createClass({
propTypes: {
users: PropTypes.arrayOf(PropTypes.shape({
name: PropTypes.string.isRequired,
roles: PropTypes.arrayOf(PropTypes.shape({
name: PropTypes.string.isRequired,
})).isRequired,
})),
clusterID: PropTypes.string.isRequired,
onDeleteAccount: PropTypes.func.isRequired,
me: PropTypes.shape(),
},
render() {
if (!this.props.users.length) {
return (
<div className="generic-empty-state">
<span className="icon alert-triangle"></span>
<h4>No Cluster Accounts</h4>
</div>
);
}
return (
<table className="table v-center users-table">
<tbody>
<tr>
<th>Username</th>
<th>Roles</th>
<th></th>
</tr>
{this.props.users.map((user) => {
return (
<tr key={user.name} data-test="user-row">
<td>
<Link to={`/clusters/${this.props.clusterID}/accounts/${user.name}`} >
{user.name}
</Link>
</td>
<td>{user.roles.map((r) => r.name).join(', ')}</td>
<td>
{this.renderDeleteAccount(user)}
</td>
</tr>
);
})}
</tbody>
</table>
);
},
renderDeleteAccount(clusterAccount) {
const currentUserIsAssociatedWithAccount = this.props.me.cluster_links.some(cl => (
cl.cluster_user === clusterAccount.name
));
const title = currentUserIsAssociatedWithAccount ?
'You can\'t remove a cluster account that you are associated with.'
: 'Delete cluster account';
return (
<button
onClick={() => this.props.onDeleteAccount(clusterAccount)}
title={title}
type="button"
data-toggle="modal"
data-target="#deleteClusterAccountModal"
className="btn btn-sm btn-link"
disabled={currentUserIsAssociatedWithAccount}>
Delete
</button>
);
},
});
const SearchBar = React.createClass({
propTypes: {
onSearch: PropTypes.func.isRequired,
searchText: PropTypes.string.isRequired,
},
handleChange() {
this.props.onSearch(this._searchText.value);
},
render() {
return (
<div className="users__search-widget input-group">
<div className="input-group-addon">
<span className="icon search" aria-hidden="true"></span>
</div>
<input
type="text"
className="form-control"
placeholder="Find User"
value={this.props.searchText}
ref={(ref) => this._searchText = ref}
onChange={this.handleChange}
/>
</div>
);
},
});
export default ClusterAccountsTable;

View File

@ -1,68 +0,0 @@
import React, {PropTypes} from 'react';
const CreateAccountModal = React.createClass({
propTypes: {
onCreateAccount: PropTypes.func.isRequired,
roles: PropTypes.arrayOf(PropTypes.shape({
name: PropTypes.shape,
})),
},
handleConfirm(e) {
e.preventDefault();
const name = this.name.value;
const password = this.password.value;
const role = this.accountRole.value;
$('#createAccountModal').modal('hide'); // eslint-disable-line no-undef
this.props.onCreateAccount(name, password, role);
},
render() {
return (
<div className="modal fade" id="createAccountModal" 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">Create Cluster Account</h4>
</div>
<form onSubmit={this.handleConfirm} data-test="cluster-account-form">
<div className="modal-body">
<div className="row">
<div className="form-group col-xs-6">
<label htmlFor="account-name">Username</label>
<input ref={(r) => this.name = r} className="form-control" type="text" id="account-name" data-test="account-name" required={true} />
</div>
<div className="form-group col-xs-6">
<label htmlFor="account-password">Password</label>
<input ref={(r) => this.password = r} className="form-control" type="password" id="account-password" data-test="account-password" required={true} />
</div>
</div>
<div className="row">
<div className="form-group col-xs-6">
<label htmlFor="account-role">Role</label>
<select ref={(r) => this.accountRole = r} id="account-role" className="form-control input-lg">
{this.props.roles.map((r, i) => {
return <option key={i}>{r.name}</option>;
})}
</select>
</div>
</div>
</div>
<div className="modal-footer">
<button className="btn btn-default" type="button" data-dismiss="modal">Cancel</button>
<button className="btn btn-danger js-delete-users" type="submit">Create Account</button>
</div>
</form>
</div>
</div>
</div>
);
},
});
export default CreateAccountModal;

View File

@ -1,51 +0,0 @@
import React, {PropTypes} from 'react';
const DeleteClusterAccountModal = React.createClass({
propTypes: {
onConfirm: PropTypes.func.isRequired,
account: PropTypes.shape({
name: PropTypes.string,
}),
webUsers: PropTypes.arrayOf(PropTypes.shape()), // TODO
},
handleConfirm() {
this.props.onConfirm();
},
render() {
return (
<div className="modal fade" id="deleteClusterAccountModal" 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">{`Are you sure you want to delete ${this.props.account && this.props.account.name}?`}</h4>
</div>
{this.props.webUsers.length ? (
<div className="modal-body">
<h5>
The following web users are associated with this cluster account will need to be reassigned
to another cluster account to continue using many of EnterpriseWeb's features:
</h5>
<ul>
{this.props.webUsers.map(webUser => {
return <li key={webUser.id}>{webUser.email}</li>;
})}
</ul>
</div>
) : null}
<div className="modal-footer">
<button className="btn btn-default" type="button" data-dismiss="modal">Cancel</button>
<button className="btn btn-danger js-delete-users" onClick={this.handleConfirm} type="button" data-dismiss="modal">Confirm</button>
</div>
</div>
</div>
</div>
);
},
});
export default DeleteClusterAccountModal;

View File

@ -1,37 +0,0 @@
import React, {PropTypes} from 'react';
const DeleteUserModal = React.createClass({
propTypes: {
onConfirm: PropTypes.func.isRequired,
user: PropTypes.shape({
name: PropTypes.string,
}),
},
handleConfirm() {
this.props.onConfirm(this.props.user);
},
render() {
return (
<div className="modal fade" id="deleteUserModal" 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">{this.props.user ? `Are you sure you want to delete ${this.props.user.name}?` : 'Are you sure?'}</h4>
</div>
<div className="modal-footer">
<button className="btn btn-default" type="button" data-dismiss="modal">Cancel</button>
<button className="btn btn-danger js-delete-users" onClick={this.handleConfirm} type="button" data-dismiss="modal">Delete</button>
</div>
</div>
</div>
</div>
);
},
});
export default DeleteUserModal;

View File

@ -1,37 +0,0 @@
import React, {PropTypes} from 'react';
import CreateAccountModal from './CreateAccountModal';
const Header = React.createClass({
propTypes: {
onCreateAccount: PropTypes.func,
roles: PropTypes.arrayOf(PropTypes.shape({
name: PropTypes.shape,
})),
},
render() {
const {roles, onCreateAccount} = this.props;
return (
<div id="cluster-accounts-page">
<div className="enterprise-header">
<div className="enterprise-header__container">
<div className="enterprise-header__left">
<h1>
Access Control
</h1>
</div>
<div className="enterprise-header__right">
<button className="btn btn-sm btn-primary" data-toggle="modal" data-target="#createAccountModal" data-test="create-cluster-account">
Create Cluster Account
</button>
</div>
</div>
</div>
<CreateAccountModal roles={roles} onCreateAccount={onCreateAccount} />
</div>
);
},
});
export default Header;

View File

@ -1,38 +0,0 @@
import React, {PropTypes} from 'react';
const RemoveAccountFromRoleModal = React.createClass({
propTypes: {
roleName: PropTypes.string,
onConfirm: PropTypes.func.isRequired,
},
handleConfirm() {
$('#removeAccountFromRoleModal').modal('hide'); // eslint-disable-line no-undef
this.props.onConfirm();
},
render() {
return (
<div className="modal fade" id="removeAccountFromRoleModal" 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">
Are you sure you want to remove <strong>{this.props.roleName}</strong> from this cluster account?
</h4>
</div>
<div className="modal-footer">
<button className="btn btn-default" type="button" data-dismiss="modal">Cancel</button>
<button onClick={this.handleConfirm} className="btn btn-danger" value="Remove">Remove</button>
</div>
</div>
</div>
</div>
);
},
});
export default RemoveAccountFromRoleModal;

View File

@ -1,43 +0,0 @@
import React, {PropTypes} from 'react';
const {string, func} = PropTypes;
const RemoveWebUserModal = React.createClass({
propTypes: {
user: string,
onRemoveWebUser: func.isRequired,
account: string.isRequired,
},
handleConfirm() {
$('#deleteUsersModal').modal('hide'); // eslint-disable-line no-undef
this.props.onRemoveWebUser();
},
render() {
return (
<div className="modal fade" id="deleteUsersModal" 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">
Are you sure you want to remove
<strong> {this.props.user} </strong> from
<strong> {this.props.account} </strong> ?
</h4>
</div>
<div className="modal-footer">
<button className="btn btn-default" type="button" data-dismiss="modal">Cancel</button>
<button onClick={this.handleConfirm} className="btn btn-danger">Remove</button>
</div>
</div>
</div>
</div>
);
},
});
export default RemoveWebUserModal;

View File

@ -1,278 +0,0 @@
import React, {PropTypes} from 'react';
import _ from 'lodash';
import {withRouter} from 'react-router';
import ClusterAccountEditPage from '../components/ClusterAccountEditPage';
import {buildClusterAccounts, buildRoles, buildAllPermissions, buildPermission} from 'src/shared/presenters';
import {showDatabases} from 'src/shared/apis/metaQuery';
import showDatabasesParser from 'shared/parsing/showDatabases';
import {
addPermissionToAccount,
removePermissionFromAccount,
deleteUserClusterLink,
getUserClusterLinks,
getClusterAccount,
getWebUsers,
getRoles,
addWebUsersToClusterAccount,
updateClusterAccountPassword,
removeAccountsFromRole,
addAccountsToRole,
meShow,
deleteClusterAccount,
getWebUsersByClusterAccount,
} from 'shared/apis';
const {shape, string, func, arrayOf} = PropTypes;
export const ClusterAccountContainer = React.createClass({
propTypes: {
dataNodes: arrayOf(string.isRequired),
params: shape({
clusterID: string.isRequired,
accountID: string.isRequired,
}).isRequired,
router: shape({
push: func.isRequired,
}).isRequired,
addFlashMessage: func,
},
getInitialState() {
return {
account: null,
roles: [],
databases: [],
assignedWebUsers: [],
unassignedWebUsers: [],
me: {},
};
},
componentDidMount() {
const {accountID, clusterID} = this.props.params;
const {dataNodes} = this.props;
Promise.all([
getClusterAccount(clusterID, accountID),
getRoles(clusterID),
showDatabases(dataNodes, clusterID),
getWebUsersByClusterAccount(clusterID, accountID),
getWebUsers(clusterID),
meShow(),
]).then(([
{data: {users}},
{data: {roles}},
{data: dbs},
{data: assignedWebUsers},
{data: allUsers},
{data: me},
]) => {
const account = buildClusterAccounts(users, roles)[0];
const presentedRoles = buildRoles(roles);
this.setState({
account,
assignedWebUsers,
roles: presentedRoles,
databases: showDatabasesParser(dbs).databases,
unassignedWebUsers: _.differenceBy(allUsers, assignedWebUsers, (u) => u.id),
me,
});
}).catch(err => {
this.props.addFlashMessage({
type: 'error',
text: `An error occured. Please try refreshing the page. ${err.message}`,
});
});
},
handleUpdatePassword(password) {
updateClusterAccountPassword(this.props.params.clusterID, this.state.account.name, password).then(() => {
this.props.addFlashMessage({
type: 'success',
text: 'Password successfully updated :)',
});
}).catch(() => {
this.props.addFlashMessage({
type: 'error',
text: 'There was a problem updating password :(',
});
});
},
handleAddPermission({name, resources}) {
const {clusterID} = this.props.params;
const {account} = this.state;
addPermissionToAccount(clusterID, account.name, name, resources).then(() => {
const newPermissions = account.permissions.map(p => p.name).includes(name) ?
account.permissions
: account.permissions.concat(buildPermission(name, resources));
this.setState({
account: Object.assign({}, account, {permissions: newPermissions}),
}, () => {
this.props.addFlashMessage({
type: 'success',
text: 'Permission successfully added :)',
});
});
}).catch(() => {
this.props.addFlashMessage({
type: 'error',
text: 'There was a problem adding the permission :(',
});
});
},
handleRemovePermission(permission) {
const {clusterID} = this.props.params;
const {account} = this.state;
removePermissionFromAccount(clusterID, account.name, permission).then(() => {
this.setState({
account: Object.assign({}, this.state.account, {
permissions: _.reject(this.state.account.permissions, (p) => p.name === permission.name),
}),
});
this.props.addFlashMessage({
type: 'success',
text: 'Removed permission from cluster account!',
});
}).catch(err => {
const text = _.result(err, ['response', 'data', 'error'], 'An error occurred.');
this.props.addFlashMessage({
type: 'error',
text,
});
});
},
handleRemoveAccountFromRole(role) {
const {clusterID, accountID} = this.props.params;
removeAccountsFromRole(clusterID, role.name, [accountID]).then(() => {
this.setState({
account: Object.assign({}, this.state.account, {
roles: this.state.account.roles.filter(r => r.name !== role.name),
}),
});
this.props.addFlashMessage({
type: 'success',
text: 'Cluster account removed from role!',
});
}).catch(err => {
this.props.addFlashMessage({
type: 'error',
text: `An error occured. ${err.message}.`,
});
});
},
handleRemoveWebUserFromAccount(user) {
const {clusterID} = this.props.params;
// TODO: update this process to just include a call to
// deleteUserClusterLinkByUserID which is currently in development
getUserClusterLinks(clusterID).then(({data}) => {
const clusterLinkToDelete = data.find((cl) => cl.cluster_id === clusterID && cl.user_id === user.id);
deleteUserClusterLink(clusterID, clusterLinkToDelete.id).then(() => {
this.setState({assignedWebUsers: this.state.assignedWebUsers.filter(u => u.id !== user.id)});
this.props.addFlashMessage({
type: 'success',
text: `${user.name} removed from this cluster account`,
});
}).catch((err) => {
console.error(err); // eslint-disable-line no-console
this.props.addFlashMessage({
type: 'error',
text: 'Something went wrong while removing this user',
});
});
});
},
handleAddRoleToAccount(role) {
const {clusterID, accountID} = this.props.params;
addAccountsToRole(clusterID, role.name, [accountID]).then(() => {
this.setState({
account: Object.assign({}, this.state.account, {
roles: this.state.account.roles.concat(role),
}),
});
this.props.addFlashMessage({
type: 'success',
text: 'Cluster account added to role!',
});
}).catch(err => {
this.props.addFlashMessage({
type: 'error',
text: `An error occured. ${err.message}.`,
});
});
},
handleAddWebUsersToAccount(users) {
const {clusterID, accountID} = this.props.params;
const userIDs = users.map((u) => {
return {
user_id: u.id,
};
});
addWebUsersToClusterAccount(clusterID, accountID, userIDs).then(() => {
this.setState({assignedWebUsers: this.state.assignedWebUsers.concat(users)});
this.props.addFlashMessage({
type: 'success',
text: `Web users added to ${accountID}`,
});
}).catch((err) => {
console.error(err); // eslint-disable-line no-console
this.props.addFlashMessage({
type: 'error',
text: `Something went wrong`,
});
});
},
handleDeleteAccount() {
const {clusterID, accountID} = this.props.params;
deleteClusterAccount(clusterID, accountID).then(() => {
this.props.router.push(`/accounts`);
this.props.addFlashMessage({
type: 'success',
text: 'Cluster account deleted!',
});
}).catch(err => {
this.props.addFlashMessage({
type: 'error',
text: `An error occured. ${err.message}.`,
});
});
},
render() {
const {clusterID, accountID} = this.props.params;
const {account, databases, roles, me} = this.state;
return (
<ClusterAccountEditPage
clusterID={clusterID}
accountID={accountID}
databases={databases}
account={account}
roles={roles}
assignedWebUsers={this.state.assignedWebUsers}
unassignedWebUsers={this.state.unassignedWebUsers}
allPermissions={buildAllPermissions()}
me={me}
onAddPermission={this.handleAddPermission}
onRemovePermission={this.handleRemovePermission}
onUpdatePassword={this.handleUpdatePassword}
onRemoveAccountFromRole={this.handleRemoveAccountFromRole}
onRemoveWebUserFromAccount={this.handleRemoveWebUserFromAccount}
onAddRoleToAccount={this.handleAddRoleToAccount}
onAddWebUsersToAccount={this.handleAddWebUsersToAccount}
onDeleteAccount={this.handleDeleteAccount}
/>
);
},
});
export default withRouter(ClusterAccountContainer);

View File

@ -1,157 +0,0 @@
import React, {PropTypes} from 'react';
import ClusterAccountsPage from '../components/ClusterAccountsPage';
import DeleteClusterAccountModal from '../components/DeleteClusterAccountModal';
import {buildClusterAccounts} from 'src/shared/presenters';
import {
getClusterAccounts,
getRoles,
deleteClusterAccount,
getWebUsersByClusterAccount,
meShow,
addUsersToRole,
createClusterAccount,
} from 'src/shared/apis';
import _ from 'lodash';
export const ClusterAccountsPageContainer = React.createClass({
propTypes: {
params: PropTypes.shape({
clusterID: PropTypes.string.isRequired,
}).isRequired,
addFlashMessage: PropTypes.func.isRequired,
},
getInitialState() {
return {
users: [],
roles: [],
// List of associated web users to display when deleting a cluster account.
webUsers: [],
// This is an unfortunate solution to using bootstrap to open modals.
// The modal will have already been rendered in this component by the
// time a user chooses "Remove" from one of the rows in the users table.
userToDelete: null,
};
},
componentDidMount() {
const {clusterID} = this.props.params;
Promise.all([
getClusterAccounts(clusterID),
getRoles(clusterID),
meShow(),
]).then(([accountsResp, rolesResp, me]) => {
this.setState({
users: buildClusterAccounts(accountsResp.data.users, rolesResp.data.roles),
roles: rolesResp.data.roles,
me: me.data,
});
});
},
// Ensures the modal will remove the correct user. TODO: our own modals
handleDeleteAccount(account) {
getWebUsersByClusterAccount(this.props.params.clusterID, account.name).then(resp => {
this.setState({
webUsers: resp.data,
userToDelete: account,
});
}).catch(err => {
console.error(err.toString()); // eslint-disable-line no-console
this.props.addFlashMessage({
type: 'error',
text: 'An error occured while trying to remove a cluster account.',
});
});
},
handleDeleteConfirm() {
const {name} = this.state.userToDelete;
deleteClusterAccount(this.props.params.clusterID, name).then(() => {
this.props.addFlashMessage({
type: 'success',
text: 'Cluster account deleted!',
});
this.setState({
users: _.reject(this.state.users, (user) => user.name === name),
});
}).catch((err) => {
console.error(err.toString()); // eslint-disable-line no-console
this.props.addFlashMessage({
type: 'error',
text: 'An error occured while trying to remove a cluster account.',
});
});
},
handleCreateAccount(name, password, roleName) {
const {clusterID} = this.props.params;
const {users, roles} = this.state;
createClusterAccount(clusterID, name, password).then(() => {
addUsersToRole(clusterID, roleName, [name]).then(() => {
this.props.addFlashMessage({
type: 'success',
text: `User ${name} added with the ${roleName} role`,
});
// add user to role
const newRoles = roles.map((role) => {
if (role.name !== roleName) {
return role;
}
return Object.assign({}, role, {
users: role.users ? role.users.concat(name) : [name],
});
});
const newUser = buildClusterAccounts([{name}], newRoles);
this.setState({
roles: newRoles,
users: users.concat(newUser),
});
}).catch((err) => {
console.error(err.toString()); // eslint-disable-line no-console
this.props.addFlashMessage({
type: 'error',
text: `An error occured while assigning ${name} to the ${roleName} role`,
});
});
}).catch((err) => {
const msg = _.get(err, 'response.data.error', '');
console.error(err.toString()); // eslint-disable-line no-console
this.props.addFlashMessage({
type: 'error',
text: `An error occured creating user ${name}. ${msg}`,
});
});
},
render() {
const {clusterID} = this.props.params;
const {users, me, roles} = this.state;
return (
<div>
<ClusterAccountsPage
users={users}
roles={roles}
clusterID={clusterID}
onDeleteAccount={this.handleDeleteAccount}
onCreateAccount={this.handleCreateAccount}
me={me}
/>
<DeleteClusterAccountModal
account={this.state.userToDelete}
webUsers={this.state.webUsers}
onConfirm={this.handleDeleteConfirm}
/>
</div>
);
},
});
export default ClusterAccountsPageContainer;

View File

@ -1,4 +0,0 @@
import ClusterAccountsPage from './containers/ClusterAccountsPageContainer';
import ClusterAccountPage from './containers/ClusterAccountContainer';
export {ClusterAccountsPage, ClusterAccountPage};

View File

@ -1,97 +0,0 @@
import React, {PropTypes} from 'react';
const {arrayOf, number, func} = PropTypes;
const CreateDatabase = React.createClass({
propTypes: {
replicationFactors: arrayOf(number.isRequired).isRequired,
onCreateDatabase: func.isRequired,
},
getInitialState() {
return {
rpName: '',
database: '',
duration: '24h',
replicaN: '1',
};
},
handleRpNameChange(e) {
this.setState({rpName: e.target.value});
},
handleDatabaseNameChange(e) {
this.setState({database: e.target.value});
},
handleSelectDuration(e) {
this.setState({duration: e.target.value});
},
handleSelectReplicaN(e) {
this.setState({replicaN: e.target.value});
},
handleSubmit() {
const {rpName, database, duration, replicaN} = this.state;
this.props.onCreateDatabase({rpName, database, duration, replicaN});
},
render() {
const {database, rpName, duration, replicaN} = this.state;
return (
<div className="modal fade" id="dbModal" tabIndex="-1" role="dialog" aria-labelledby="myModalLabel">
<form data-remote="true" onSubmit={this.handleSubmit} >
<div className="modal-dialog" role="document">
<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" id="myModalLabel">Create Database</h4>
</div>
<div className="modal-body">
<div id="form-errors"></div>
<div className="form-group col-sm-12">
<label htmlFor="name">Database Name</label>
<input onChange={this.handleDatabaseNameChange} value={database} required={true} className="form-control input-lg" type="text" id="name" name="name"/>
</div>
<div className="form-group col-sm-6">
<label htmlFor="retention-policy">Retention Policy Name</label>
<input onChange={this.handleRpNameChange} value={rpName} required={true} className="form-control input-lg" type="text" id="retention-policy" name="retention-policy"/>
</div>
<div className="form-group col-sm-3">
<label htmlFor="duration" data-toggle="tooltip" data-placement="top" title="How long InfluxDB stores data">Duration</label>
<select onChange={this.handleSelectDuration} defaultValue={duration} className="form-control input-lg" name="duration" id="exampleSelect" required={true}>
<option value="24h">1 Day</option>
<option value="168h">7 Days</option>
<option value="720h">30 Days</option>
<option value="8670h">365 Days</option>
</select>
</div>
<div className="form-group col-sm-3">
<label htmlFor="replication-factor" data-toggle="tooltip" data-placement="top" title="How many copies of the data InfluxDB stores">Replication Factor</label>
<select onChange={this.handleSelectReplicaN} defaultValue={replicaN} className="form-control input-lg" name="replication-factor" id="replication-factor" required={true}>
{
this.props.replicationFactors.map((rp) => {
return <option key={rp}>{rp}</option>;
})
}
</select>
</div>
</div>
<div className="modal-footer">
<button type="button" className="btn btn-default" data-dismiss="modal">Cancel</button>
<button type="submit" className="btn btn-success">Create</button>
</div>
</div>
</div>
</form>
</div>
);
},
});
export default CreateDatabase;

View File

@ -1,141 +0,0 @@
import React, {PropTypes} from 'react';
import {Link} from 'react-router';
import CreateDatabase from './CreateDatabase';
const {number, string, shape, arrayOf, func} = PropTypes;
const DatabaseManager = React.createClass({
propTypes: {
database: string.isRequired,
databases: arrayOf(shape({})).isRequired,
dbStats: shape({
diskBytes: string.isRequired,
numMeasurements: number.isRequired,
numSeries: number.isRequired,
}),
users: arrayOf(shape({
id: number,
name: string.isRequired,
roles: string.isRequired,
})).isRequired,
queries: arrayOf(string).isRequired,
replicationFactors: arrayOf(number).isRequired,
onClickDatabase: func.isRequired,
onCreateDatabase: func.isRequired,
},
render() {
const {database, databases, dbStats, queries, users,
replicationFactors, onClickDatabase, onCreateDatabase} = this.props;
return (
<div className="page-wrapper database-manager">
<div className="enterprise-header">
<div className="enterprise-header__container">
<div className="enterprise-header__left">
<div className="dropdown minimal-dropdown">
<button className="dropdown-toggle" type="button" id="dropdownMenu1" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
<span className="button-text">{database}</span>
<span className="caret"></span>
</button>
<ul className="dropdown-menu" aria-labelledby="dropdownMenu1">
{
databases.map((db) => {
return <li onClick={() => onClickDatabase(db.Name)} key={db.Name}><Link to={`/databases/manager/${db.Name}`}>{db.Name}</Link></li>;
})
}
</ul>
</div>
</div>
<div className="enterprise-header__right">
<button className="btn btn-sm btn-primary" data-toggle="modal" data-target="#dbModal">Create Database</button>
</div>
</div>
</div>
<div className="container-fluid">
<div className="row">
<div className="col-sm-12 col-md-4">
<div className="panel panel-minimal">
<div className="panel-heading">
<h2 className="panel-title">Database Stats</h2>
</div>
<div className="panel-body">
<div className="db-manager-stats">
<div>
<h4>{dbStats.diskBytes}</h4>
<p>On Disk</p>
</div>
<div>
<h4>{dbStats.numMeasurements}</h4>
<p>Measurements</p>
</div>
<div>
<h4>{dbStats.numSeries}</h4>
<p>Series</p>
</div>
</div>
</div>
</div>
</div>
<div className="col-sm-12 col-md-8">
<div className="panel panel-minimal">
<div className="panel-heading">
<h2 className="panel-title">Users</h2>
</div>
<div className="panel-body">
<table className="table v-center margin-bottom-zero">
<thead>
<tr>
<th>Name</th>
<th>Role</th>
</tr>
</thead>
<tbody>
{
users.map((user) => {
return (
<tr key={user.name}>
<td><Link title="Manage Access" to={`/accounts/${user.name}`}>{user.name}</Link></td>
<td>{user.roles}</td>
</tr>
);
})
}
</tbody>
</table>
</div>
</div>
</div>
</div>
<div className="row">
<div className="col-md-12">
<div className="panel panel-minimal">
<div className="panel-heading">
<h2 className="panel-title">Continuous Queries Associated</h2>
</div>
<div className="panel-body continuous-queries">
{
queries.length ? queries.map((query, i) => <pre key={i}><code>{query}</code></pre>) :
(
<div className="continuous-queries__empty">
<img src="/assets/images/continuous-query-empty.svg" />
<h4>No queries to display</h4>
</div>
)
}
</div>
</div>
</div>
</div>
</div>
<CreateDatabase onCreateDatabase={onCreateDatabase} replicationFactors={replicationFactors}/>
</div>
);
},
});
export default DatabaseManager;

View File

@ -1,77 +0,0 @@
import React, {PropTypes} from 'react';
import {getDatabaseManager, createDatabase} from 'shared/apis/index';
import DatabaseManager from '../components/DatabaseManager';
const {shape, string} = PropTypes;
const DatabaseManagerApp = React.createClass({
propTypes: {
params: shape({
clusterID: string.isRequired,
database: string.isRequired,
}).isRequired,
},
componentDidMount() {
this.getData();
},
getInitialState() {
return {
databases: [],
dbStats: {
diskBytes: '',
numMeasurements: 0,
numSeries: 0,
},
users: [],
queries: [],
replicationFactors: [],
selectedDatabase: null,
};
},
getData(selectedDatabase) {
const {clusterID, database} = this.props.params;
getDatabaseManager(clusterID, selectedDatabase || database)
.then(({data}) => {
this.setState({
databases: data.databases,
dbStats: data.databaseStats,
users: data.users,
queries: data.queries || [],
replicationFactors: data.replicationFactors,
});
});
},
handleClickDatabase(selectedDatabase) {
this.getData(selectedDatabase);
this.setState({selectedDatabase});
},
handleCreateDatabase(db) {
createDatabase(db);
},
render() {
const {databases, dbStats, queries, users, replicationFactors} = this.state;
const {clusterID, database} = this.props.params;
return (
<DatabaseManager
clusterID={clusterID}
database={database}
databases={databases}
dbStats={dbStats}
queries={queries}
users={users}
replicationFactors={replicationFactors}
onClickDatabase={this.handleClickDatabase}
onCreateDatabase={this.handleCreateDatabase}
/>
);
},
});
export default DatabaseManagerApp;

View File

@ -1,2 +0,0 @@
import DatabaseManagerApp from './containers/DatabaseManagerApp';
export default DatabaseManagerApp;

View File

@ -1,187 +0,0 @@
import React, {PropTypes} from 'react';
import flatten from 'lodash/flatten';
import reject from 'lodash/reject';
import uniqBy from 'lodash/uniqBy';
import {
showDatabases,
showQueries,
killQuery,
} from 'shared/apis/metaQuery';
import showDatabasesParser from 'shared/parsing/showDatabases';
import showQueriesParser from 'shared/parsing/showQueries';
const times = [
{test: /ns/, magnitude: 0},
{test: /^\d*u/, magnitude: 1},
{test: /^\d*ms/, magnitude: 2},
{test: /^\d*s/, magnitude: 3},
{test: /^\d*m\d*s/, magnitude: 4},
{test: /^\d*h\d*m\d*s/, magnitude: 5},
];
export const QueriesPage = React.createClass({
propTypes: {
dataNodes: PropTypes.arrayOf(PropTypes.string.isRequired).isRequired,
addFlashMessage: PropTypes.func,
params: PropTypes.shape({
clusterID: PropTypes.string.isRequired,
}),
},
getInitialState() {
return {
queries: [],
queryIDToKill: null,
};
},
componentDidMount() {
this.updateQueries();
const updateInterval = 5000;
this.intervalID = setInterval(this.updateQueries, updateInterval);
},
componentWillUnmount() {
clearInterval(this.intervalID);
},
updateQueries() {
const {dataNodes, addFlashMessage, params} = this.props;
showDatabases(dataNodes, params.clusterID).then((resp) => {
const {databases, errors} = showDatabasesParser(resp.data);
if (errors.length) {
errors.forEach((message) => addFlashMessage({type: 'error', text: message}));
return;
}
const fetches = databases.map((db) => showQueries(dataNodes, db, params.clusterID));
Promise.all(fetches).then((queryResponses) => {
const allQueries = [];
queryResponses.forEach((queryResponse) => {
const result = showQueriesParser(queryResponse.data);
if (result.errors.length) {
result.erorrs.forEach((message) => this.props.addFlashMessage({type: 'error', text: message}));
}
allQueries.push(...result.queries);
});
const queries = uniqBy(flatten(allQueries), (q) => q.id);
// sorting queries by magnitude, so generally longer queries will appear atop the list
const sortedQueries = queries.sort((a, b) => {
const aTime = times.find((t) => a.duration.match(t.test));
const bTime = times.find((t) => b.duration.match(t.test));
return +aTime.magnitude <= +bTime.magnitude;
});
this.setState({
queries: sortedQueries,
});
});
});
},
render() {
const {queries} = this.state;
return (
<div>
<div className="enterprise-header">
<div className="enterprise-header__container">
<div className="enterprise-header__left">
<h1>
Queries
</h1>
</div>
</div>
</div>
<div className="container-fluid">
<div className="row">
<div className="col-md-12">
<div className="panel panel-minimal">
<div className="panel-body">
<table className="table v-center">
<thead>
<tr>
<th>Database</th>
<th>Query</th>
<th>Running</th>
<th></th>
</tr>
</thead>
<tbody>
{queries.map((q) => {
return (
<tr key={q.id}>
<td>{q.database}</td>
<td><code>{q.query}</code></td>
<td>{q.duration}</td>
<td className="text-right">
<button className="btn btn-xs btn-link-danger" onClick={this.handleKillQuery} data-toggle="modal" data-query-id={q.id} data-target="#killModal">
Kill
</button>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<div className="modal fade" id="killModal" tabIndex="-1" role="dialog" aria-labelledby="myModalLabel">
<div className="modal-dialog" role="document">
<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" id="myModalLabel">Are you sure you want to kill this query?</h4>
</div>
<div className="modal-footer">
<button type="button" className="btn btn-default" data-dismiss="modal">No</button>
<button type="button" className="btn btn-danger" data-dismiss="modal" onClick={this.handleConfirmKillQuery}>Yes, kill it!</button>
</div>
</div>
</div>
</div>
</div>
);
},
handleKillQuery(e) {
e.stopPropagation();
const id = e.target.dataset.queryId;
this.setState({
queryIDToKill: id,
});
},
handleConfirmKillQuery() {
const {queryIDToKill} = this.state;
if (queryIDToKill === null) {
return;
}
// optimitstic update
const {queries} = this.state;
this.setState({
queries: reject(queries, (q) => +q.id === +queryIDToKill),
});
// kill the query over http
const {dataNodes, params} = this.props;
killQuery(dataNodes, queryIDToKill, params.clusterID).then(() => {
this.setState({
queryIDToKill: null,
});
});
},
});
export default QueriesPage;

View File

@ -1,2 +0,0 @@
import QueriesPage from './containers/QueriesPage';
export default QueriesPage;

View File

@ -1,70 +0,0 @@
import React, {PropTypes} from 'react';
export default React.createClass({
propTypes: {
onCreate: PropTypes.func.isRequired,
dataNodes: PropTypes.arrayOf(PropTypes.string).isRequired,
},
render() {
return (
<div className="modal fade" id="rpModal" tabIndex={-1} role="dialog" aria-labelledby="myModalLabel">
<div className="modal-dialog" role="document">
<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" id="myModalLabel">Create Retention Policy</h4>
</div>
<form onSubmit={this.handleSubmit}>
<div className="modal-body">
<div className="form-group col-md-12">
<label htmlFor="rpName">Name Retention Pollicy</label>
<input ref={(r) => this.rpName = r} type="text" className="form-control" id="rpName" placeholder="Name" required={true}/>
</div>
<div className="form-group col-md-6">
<label htmlFor="durationSelect">Select Duration</label>
<select ref={(r) => this.duration = r} className="form-control" id="durationSelect">
<option value="1d">1 Day</option>
<option value="7d">7 Days</option>
<option value="30d">30 Days</option>
<option value="365d">365 Days</option>
</select>
</div>
<div className="form-group col-md-6">
<label htmlFor="replicationFactor">Replication Factor</label>
<select ref={(r) => this.replicationFactor = r} className="form-control" id="replicationFactor">
{
this.props.dataNodes.map((node, i) => <option key={node}>{i + 1}</option>)
}
</select>
</div>
</div>
<div className="modal-footer">
<button type="button" className="btn btn-default" data-dismiss="modal">Cancel</button>
<button ref="submitButton" type="submit" className="btn btn-success">Create</button>
</div>
</form>
</div>
</div>
</div>
);
},
handleSubmit(e) {
e.preventDefault();
const rpName = this.rpName.value;
const duration = this.duration.value;
const replicationFactor = this.replicationFactor.value;
// Not using data-dimiss="modal" becuase it doesn't play well with HTML5 validations.
$('#rpModal').modal('hide'); // eslint-disable-line no-undef
this.props.onCreate({
rpName,
duration,
replicationFactor,
});
},
});

View File

@ -1,74 +0,0 @@
import React, {PropTypes} from 'react';
const DropShardModal = React.createClass({
propTypes: {
onConfirm: PropTypes.func.isRequired,
},
getInitialState() {
return {error: null, text: ''};
},
componentDidMount() {
// Using this unfortunate hack because this modal is still using bootstrap,
// and this component is never removed once being mounted -- meaning it doesn't
// start with a new initial state when it gets closed/reopened. A better
// long term solution is just to handle modals in ReactLand.
$('#dropShardModal').on('hide.bs.modal', () => { // eslint-disable-line no-undef
this.setState({error: null, text: ''});
});
},
handleConfirmationTextChange(e) {
this.setState({text: e.target.value});
},
render() {
return (
<div className="modal fade" id="dropShardModal" tabIndex={-1} role="dialog" aria-labelledby="myModalLabel">
<div className="modal-dialog" role="document">
<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" id="myModalLabel">Are you sure?</h4>
</div>
<form onSubmit={this.handleSubmit}>
<div className="modal-body">
{this.state.error ?
<div className="alert alert-danger" role="alert">{this.state.error}</div>
: null}
<div className="form-group col-md-12">
<label htmlFor="confirmation">All of the data on this shard will be removed permanently. Please Type 'delete' to confirm.</label>
<input onChange={this.handleConfirmationTextChange} value={this.state.text} type="text" className="form-control" id="confirmation" />
</div>
</div>
<div className="modal-footer">
<button type="button" className="btn btn-default" data-dismiss="modal">Cancel</button>
<button ref="submitButton" type="submit" className="btn btn-danger">Delete</button>
</div>
</form>
</div>
</div>
</div>
);
},
handleSubmit(e) {
e.preventDefault();
if (this.state.text.toLowerCase() !== 'delete') {
this.setState({error: "Please confirm by typing 'delete'"});
return;
}
// Hiding the modal directly because we have an extra confirmation step,
// bootstrap will close the modal immediately after clicking 'Delete'.
$('#dropShardModal').modal('hide'); // eslint-disable-line no-undef
this.props.onConfirm();
},
});
export default DropShardModal;

View File

@ -1,34 +0,0 @@
import React, {PropTypes} from 'react';
export default React.createClass({
propTypes: {
databases: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types
selectedDatabase: PropTypes.string.isRequired,
onChooseDatabase: PropTypes.func.isRequired,
},
render() {
return (
<div className="enterprise-header">
<div className="enterprise-header__container">
<div className="enterprise-header__left">
<div className="dropdown minimal-dropdown">
<button className="dropdown-toggle" type="button" id="dropdownMenu1" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
{this.props.selectedDatabase}
<span className="caret" />
</button>
<ul className="dropdown-menu" aria-labelledby="dropdownMenu1">
{this.props.databases.map((d) => {
return <li key={d} onClick={() => this.props.onChooseDatabase(d)}><a href="#">{d}</a></li>;
})}
</ul>
</div>
</div>
<div className="enterprise-header__right">
<button className="btn btn-sm btn-primary" data-toggle="modal" data-target="#rpModal">Create Retention Policy</button>
</div>
</div>
</div>
);
},
});

View File

@ -1,63 +0,0 @@
import React, {PropTypes} from 'react';
import RetentionPolicyCard from './RetentionPolicyCard';
const {string, arrayOf, shape, func} = PropTypes;
export default React.createClass({
propTypes: {
retentionPolicies: arrayOf(shape()).isRequired,
shardDiskUsage: shape(),
shards: shape().isRequired,
selectedDatabase: string.isRequired,
onDropShard: func.isRequired,
},
render() {
const {shardDiskUsage, retentionPolicies, onDropShard, shards, selectedDatabase} = this.props;
return (
<div className="row">
<div className="col-md-12">
<h3 className="deluxe fake-panel-title">Retention Policies</h3>
<div className="panel-group retention-policies" id="accordion" role="tablist" aria-multiselectable="true">
{retentionPolicies.map((rp, i) => {
const ss = shards[`${selectedDatabase}..${rp.name}`] || [];
/**
* We use the `/show-shards` endpoint as 'source of truth' for active shards in the cluster.
* Disk usage has to be fetched directly from InfluxDB, which means we'll have stale shard
* data (the results will often include disk usage for shards that have been removed). This
* ensures we only use active shards when we calculate disk usage.
*/
const newDiskUsage = {};
ss.forEach((shard) => {
(shardDiskUsage[shard.shardId] || []).forEach((d) => {
if (!shard.owners.map((o) => o.tcpAddr).includes(d.nodeID)) {
return;
}
if (newDiskUsage[shard.shardId]) {
newDiskUsage[shard.shardId].push(d);
} else {
newDiskUsage[shard.shardId] = [d];
}
});
});
return (
<RetentionPolicyCard
key={rp.name}
onDelete={() => {}}
rp={rp}
shards={ss}
index={i}
shardDiskUsage={newDiskUsage}
onDropShard={onDropShard}
/>
);
})}
</div>
</div>
</div>
);
},
});

View File

@ -1,140 +0,0 @@
import React, {PropTypes} from 'react';
import classNames from 'classnames';
import moment from 'moment';
import DropShardModal from './DropShardModal';
import {formatBytes, formatRPDuration} from 'utils/formatting';
/* eslint-disable no-magic-numbers */
const {func, string, shape, number, bool, arrayOf, objectOf} = PropTypes;
export default React.createClass({
propTypes: {
onDropShard: func.isRequired,
rp: shape({
name: string.isRequired,
duration: string.isRequired,
isDefault: bool.isRequired,
replication: number,
shardGroupDuration: string,
}).isRequired,
shards: arrayOf(shape({
database: string.isRequired,
startTime: string.isRequired,
endTime: string.isRequired,
retentionPolicy: string.isRequired,
shardId: string.isRequired,
shardGroup: string.isRequired,
})),
shardDiskUsage: objectOf(
arrayOf(
shape({
diskUsage: number.isRequired,
nodeID: string.isRequired,
}),
),
),
index: number, // Required to make bootstrap JS work.
},
formatTimestamp(timestamp) {
return moment(timestamp).format('YYYY-MM-DD:H');
},
render() {
const {index, rp, shards, shardDiskUsage} = this.props;
const diskUsage = shards.reduce((sum, shard) => {
// Check if we don't have any disk usage for a shard. This happens most often
// with a new cluster before any disk usage has a chance to be recorded.
if (!shardDiskUsage[shard.shardId]) {
return sum;
}
return sum + shardDiskUsage[shard.shardId].reduce((shardSum, shardInfo) => {
return shardSum + shardInfo.diskUsage;
}, 0);
}, 0);
return (
<div className="panel panel-default">
<div className="panel-heading" role="tab" id={`heading${index}`}>
<h4 className="panel-title js-rp-card-header u-flex u-ai-center u-jc-space-between">
<a className={index === 0 ? "" : "collapsed"} role="button" data-toggle="collapse" data-parent="#accordion" href={`#collapse${index}`} aria-expanded="true" aria-controls={`collapse${index}`}>
<span className="caret" /> {rp.name}
</a>
<span>
<p className="rp-duration">{formatRPDuration(rp.duration)} {rp.isDefault ? '(Default)' : null}</p>
<p className="rp-disk-usage">{formatBytes(diskUsage)}</p>
</span>
</h4>
</div>
<div id={`collapse${index}`} className={classNames("panel-collapse collapse", {'in': index === 0})} role="tabpanel" aria-labelledby={`heading${index}`}>
<div className="panel-body">
{this.renderShardTable()}
</div>
</div>
<DropShardModal onConfirm={this.handleDropShard} />
</div>
);
},
renderShardTable() {
const {shards, shardDiskUsage} = this.props;
if (!shards.length) {
return <div>No shards.</div>;
}
return (
<table className="table shard-table">
<thead>
<tr>
<th>Shard ID</th>
<th>Time Range</th>
<th>Disk Usage</th>
<th>Nodes</th>
<th />
</tr>
</thead>
<tbody>
{shards.map((shard, index) => {
const diskUsages = shardDiskUsage[shard.shardId] || [];
return (
<tr key={index}>
<td>{shard.shardId}</td>
<td>{this.formatTimestamp(shard.startTime)} {this.formatTimestamp(shard.endTime)}</td>
<td>
{diskUsages.length ? diskUsages.map((s) => {
const diskUsageForShard = formatBytes(s.diskUsage) || 'n/a';
return <p key={s.nodeID}>{diskUsageForShard}</p>;
})
: 'n/a'}
</td>
<td>
{diskUsages.length ? diskUsages.map((s) => <p key={s.nodeID}>{s.nodeID}</p>) : 'n/a'}
</td>
<td className="text-right">
<button data-toggle="modal" data-target="#dropShardModal" onClick={() => this.openConfirmationModal(shard)} className="btn btn-danger btn-sm" title="Drop Shard"><span className="icon trash js-drop-shard" /></button>
</td>
</tr>
);
})}
</tbody>
</table>
);
},
openConfirmationModal(shard) {
this.setState({shardIdToDelete: shard.shardId});
},
handleDropShard() {
const shard = this.props.shards.filter((s) => s.shardId === this.state.shardIdToDelete)[0];
this.props.onDropShard(shard);
this.setState({shardIdToDelete: null});
},
});
/* eslint-enable no-magic-numbers */

View File

@ -1,212 +0,0 @@
import React, {PropTypes} from 'react';
import _ from 'lodash';
import RetentionPoliciesHeader from '../components/RetentionPoliciesHeader';
import RetentionPoliciesList from '../components/RetentionPoliciesList';
import CreateRetentionPolicyModal from '../components/CreateRetentionPolicyModal';
import {
showDatabases,
showRetentionPolicies,
showShards,
createRetentionPolicy,
dropShard,
} from 'shared/apis/metaQuery';
import {fetchShardDiskBytesForDatabase} from 'shared/apis/stats';
import parseShowDatabases from 'shared/parsing/showDatabases';
import parseShowRetentionPolicies from 'shared/parsing/showRetentionPolicies';
import parseShowShards from 'shared/parsing/showShards';
import {diskBytesFromShardForDatabase} from 'shared/parsing/diskBytes';
const RetentionPoliciesApp = React.createClass({
propTypes: {
dataNodes: PropTypes.arrayOf(PropTypes.string.isRequired).isRequired,
params: PropTypes.shape({
clusterID: PropTypes.string.isRequired,
}).isRequired,
addFlashMessage: PropTypes.func,
},
getInitialState() {
return {
// Simple list of databases
databases: [],
// A list of retention policy objects for the currently selected database
retentionPolicies: [],
/**
* Disk usage/node locations for all shards across a database, keyed by shard ID.
* e.g. if shard 10 was replicated across two data nodes:
* {
* 10: [
* {nodeID: 'localhost:8088', diskUsage: 12312414},
* {nodeID: 'localhost:8188', diskUsage: 12312414},
* ],
* ...
* }
*/
shardDiskUsage: {},
// All shards across all databases, keyed by database and retention policy. e.g.:
// 'telegraf..default': [
// <shard>,
// <shard>
// ]
shards: {},
selectedDatabase: null,
isFetching: true,
};
},
componentDidMount() {
showDatabases(this.props.dataNodes, this.props.params.clusterID).then((resp) => {
const result = parseShowDatabases(resp.data);
if (!result.databases.length) {
this.props.addFlashMessage({
text: 'No databases found',
type: 'error',
});
return;
}
const selectedDatabase = result.databases[0];
this.setState({
databases: result.databases,
selectedDatabase,
});
this.fetchInfoForDatabase(selectedDatabase);
}).catch((err) => {
console.error(err); // eslint-disable-line no-console
this.addGenericErrorMessage(err.toString());
});
},
fetchInfoForDatabase(database) {
this.setState({isFetching: true});
Promise.all([
this.fetchRetentionPoliciesAndShards(database),
this.fetchDiskUsage(database),
]).then(([rps, shardDiskUsage]) => {
const {retentionPolicies, shards} = rps;
this.setState({
shardDiskUsage,
retentionPolicies,
shards,
});
}).catch((err) => {
console.error(err); // eslint-disable-line no-console
this.addGenericErrorMessage(err.toString());
}).then(() => {
this.setState({isFetching: false});
});
},
addGenericErrorMessage(errMessage) {
const defaultMsg = 'Something went wrong! Try refreshing your browser and email support@influxdata.com if the problem persists.';
this.props.addFlashMessage({
text: errMessage || defaultMsg,
type: 'error',
});
},
fetchRetentionPoliciesAndShards(database) {
const shared = {};
return showRetentionPolicies(this.props.dataNodes, database, this.props.params.clusterID).then((resp) => {
shared.retentionPolicies = resp.data.results.map(parseShowRetentionPolicies);
return showShards(this.props.params.clusterID);
}).then((resp) => {
const shards = parseShowShards(resp.data);
return {shards, retentionPolicies: shared.retentionPolicies[0].retentionPolicies};
});
},
fetchDiskUsage(database) {
const {dataNodes, params: {clusterID}} = this.props;
return fetchShardDiskBytesForDatabase(dataNodes, database, clusterID).then((resp) => {
return diskBytesFromShardForDatabase(resp.data).shardData;
});
},
handleChooseDatabase(database) {
this.setState({selectedDatabase: database, retentionPolicies: []});
this.fetchInfoForDatabase(database);
},
handleCreateRetentionPolicy({rpName, duration, replicationFactor}) {
const params = {
database: this.state.selectedDatabase,
host: this.props.dataNodes,
rpName,
duration,
replicationFactor,
clusterID: this.props.params.clusterID,
};
createRetentionPolicy(params).then(() => {
this.props.addFlashMessage({
text: 'Retention policy created successfully!',
type: 'success',
});
this.fetchInfoForDatabase(this.state.selectedDatabase);
}).catch((err) => {
this.addGenericErrorMessage(err.toString());
});
},
render() {
if (this.state.isFetching) {
return <div className="page-spinner" />;
}
const {selectedDatabase, shards, shardDiskUsage} = this.state;
return (
<div className="page-wrapper retention-policies">
<RetentionPoliciesHeader
databases={this.state.databases}
selectedDatabase={selectedDatabase}
onChooseDatabase={this.handleChooseDatabase}
/>
<div className="container-fluid">
<RetentionPoliciesList
retentionPolicies={this.state.retentionPolicies}
selectedDatabase={selectedDatabase}
shards={shards}
shardDiskUsage={shardDiskUsage}
onDropShard={this.handleDropShard}
/>
</div>
<CreateRetentionPolicyModal onCreate={this.handleCreateRetentionPolicy} dataNodes={this.props.dataNodes} />
</div>
);
},
handleDropShard(shard) {
const {dataNodes, params} = this.props;
dropShard(dataNodes, shard, params.clusterID).then(() => {
const key = `${this.state.selectedDatabase}..${shard.retentionPolicy}`;
const shardsForRP = this.state.shards[key];
const nextShards = _.reject(shardsForRP, (s) => s.shardId === shard.shardId);
const shards = Object.assign({}, this.state.shards);
shards[key] = nextShards;
this.props.addFlashMessage({
text: `Dropped shard ${shard.shardId}`,
type: 'success',
});
this.setState({shards});
}).catch(() => {
this.addGenericErrorMessage();
});
},
});
export default RetentionPoliciesApp;

View File

@ -1,2 +0,0 @@
import RetentionPoliciesApp from './containers/RetentionPoliciesApp';
export default RetentionPoliciesApp;

View File

@ -1,79 +0,0 @@
import React, {PropTypes} from 'react';
const CreateClusterAdmin = React.createClass({
propTypes: {
onCreateClusterAdmin: PropTypes.func.isRequired,
},
getInitialState() {
return {
passwordsMatch: true,
};
},
handleSubmit(e) {
e.preventDefault();
const username = this.username.value;
const password = this.password.value;
const confirmation = this.confirmation.value;
if (password !== confirmation) {
return this.setState({
passwordsMatch: false,
});
}
this.props.onCreateClusterAdmin(username, password);
},
render() {
const {passwordsMatch} = this.state;
return (
<div id="signup-page">
<div className="container">
<div className="row">
<div className="col-md-8 col-md-offset-2">
<div className="panel panel-summer">
<div className="panel-heading text-center">
<div className="signup-progress-circle step2of3">2/3</div>
<h2 className="deluxe">Welcome to InfluxEnterprise</h2>
</div>
<div className="panel-body">
{passwordsMatch ? null : this.renderValidationError()}
<h4>Create a Cluster Administrator account.</h4>
<p>Users assigned to the Cluster Administrator account have all cluster permissions.</p>
<form onSubmit={this.handleSubmit}>
<div className="form-group col-sm-12">
<label htmlFor="username">Account Name</label>
<input ref={(username) => this.username = username} className="form-control input-lg" type="text" id="username" required={true} placeholder="Ex. ClusterAdmin"/>
</div>
<div className="form-group col-sm-6">
<label htmlFor="password">Password</label>
<input ref={(pass) => this.password = pass} className="form-control input-lg" type="password" id="password" required={true}/>
</div>
<div className="form-group col-sm-6">
<label htmlFor="confirmation">Confirm Password</label>
<input ref={(conf) => this.confirmation = conf} className="form-control input-lg" type="password" id="confirmation" required={true} />
</div>
<div className="form-group col-sm-6 col-sm-offset-3">
<button className="btn btn-lg btn-success btn-block" type="submit">Next</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
);
},
renderValidationError() {
return <div>Your passwords don't match! Please make sure they match.</div>;
},
});
export default CreateClusterAdmin;

View File

@ -1,125 +0,0 @@
import React, {PropTypes} from 'react';
import ClusterAccounts from 'shared/components/AddClusterAccounts';
import {getClusters} from 'shared/apis';
const CreateWebAdmin = React.createClass({
propTypes: {
onCreateWebAdmin: PropTypes.func.isRequired,
},
getInitialState() {
return {
clusters: [],
clusterLinks: {},
passwordsMatch: true,
};
},
componentDidMount() {
getClusters().then(({data}) => {
this.setState({clusters: data});
});
},
handleSubmit(e) {
e.preventDefault();
const firstName = this.firstName.value;
const lastName = this.lastName.value;
const email = this.email.value;
const password = this.password.value;
const confirmation = this.confirmation.value;
if (password !== confirmation) {
return this.setState({passwordsMatch: false});
}
this.props.onCreateWebAdmin(firstName, lastName, email, password, confirmation, this.getClusterLinks());
},
handleSelectClusterAccount({clusterID, accountName}) {
const clusterLinks = Object.assign({}, this.state.clusterLinks, {
[clusterID]: accountName,
});
this.setState({
clusterLinks,
});
},
getClusterLinks() {
return Object.keys(this.state.clusterLinks).map((clusterID) => {
return {
cluster_id: clusterID,
cluster_user: this.state.clusterLinks[clusterID],
};
});
},
render() {
const {clusters, passwordsMatch, clusterLinks} = this.state;
return (
<div id="signup-page">
<div className="container">
<div className="row">
<div className="col-md-8 col-md-offset-2">
<div className="panel panel-summer">
<div className="panel-heading text-center">
<div className="signup-progress-circle step3of3">3/3</div>
<h2 className="deluxe">Welcome to InfluxEnterprise</h2>
</div>
<div className="panel-body">
{passwordsMatch ? null : this.renderValidationError()}
<h4>Create a Web Administrator user.</h4>
<h5>A Web Administrator has all web console permissions.</h5>
<p>
After filling out the form with your name, email, and password, assign yourself to the Cluster Administrator account that you
created in the previous step. This ensures that you have all web console permissions and all cluster permissions.
</p>
<form onSubmit={this.handleSubmit}>
<div className="row">
<div className="form-group col-sm-6">
<label htmlFor="first-name">First Name</label>
<input ref={(firstName) => this.firstName = firstName} className="form-control input-lg" type="text" id="first-name" required={true} />
</div>
<div className="form-group col-sm-6">
<label htmlFor="last-name">Last Name</label>
<input ref={(lastName) => this.lastName = lastName} className="form-control input-lg" type="text" id="last-name" required={true} />
</div>
</div>
<div className="row">
<div className="form-group col-sm-12">
<label htmlFor="email">Email</label>
<input ref={(email) => this.email = email} className="form-control input-lg" type="text" id="email" required={true} />
</div>
</div>
<div className="row">
<div className="form-group col-sm-6">
<label htmlFor="password">Password</label>
<input ref={(password) => this.password = password} className="form-control input-lg" type="password" id="password" required={true} />
</div>
<div className="form-group col-sm-6">
<label htmlFor="confirmation">Confirm Password</label>
<input ref={(confirmation) => this.confirmation = confirmation} className="form-control input-lg" type="password" id="confirmation" required={true} />
</div>
</div>
{clusters.length ? <ClusterAccounts clusters={clusters} onSelectClusterAccount={this.handleSelectClusterAccount} /> : null}
<div className="form-group col-sm-6 col-sm-offset-3">
<button disabled={!Object.keys(clusterLinks).length} className="btn btn-lg btn-success btn-block" type="submit">Enter App</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
);
},
renderValidationError() {
return <div>Your passwords don't match!</div>;
},
});
export default CreateWebAdmin;

View File

@ -1,48 +0,0 @@
import React, {PropTypes} from 'react';
const NameCluster = React.createClass({
propTypes: {
onNameCluster: PropTypes.func.isRequired,
},
handleSubmit(e) {
e.preventDefault();
this.props.onNameCluster(this.clusterName.value);
},
render() {
return (
<div id="signup-page">
<div className="container">
<div className="row">
<div className="col-md-8 col-md-offset-2">
<div className="panel panel-summer">
<div className="panel-heading text-center">
<div className="signup-progress-circle step1of3">1/3</div>
<h2 className="deluxe">Welcome to InfluxEnterprise</h2>
<p>
</p>
</div>
<div className="panel-body">
<form onSubmit={this.handleSubmit}>
<div className="form-group col-sm-12">
<h4>What do you want to call your cluster?</h4>
<label htmlFor="cluster-name">Cluster Name (you can change this later)</label>
<input ref={(name) => this.clusterName = name} className="form-control input-lg" type="text" id="cluster-name" placeholder="Ex. MyCluster"/>
</div>
<div className="form-group col-sm-6 col-sm-offset-3">
<button className="btn btn-lg btn-block btn-success" type="submit">Next</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
);
},
});
export default NameCluster;

View File

@ -1,33 +0,0 @@
import React from 'react';
const NoCluster = React.createClass({
handleSubmit() {
window.location.reload();
},
render() {
return (
<div id="signup-page">
<div className="container">
<div className="row">
<div className="col-md-8 col-md-offset-2">
<div className="panel panel-summer">
<div className="panel-heading text-center">
<h2 className="deluxe">Welcome to Enterprise</h2>
<p>
Looks like you don't have your cluster set up.
</p>
</div>
<div className="panel-body">
<button className="btn btn-lg btn-success btn-block" onClick={this.handleSubmit}>Try Again</button>
</div>
</div>
</div>
</div>
</div>
</div>
);
},
});
export default NoCluster;

View File

@ -1,97 +0,0 @@
import React, {PropTypes} from 'react';
import CreateClusterAdmin from './components/CreateClusterAdmin';
import CreateWebAdmin from './components/CreateWebAdmin';
import NameCluster from './components/NameCluster';
import NoCluster from './components/NoCluster';
import {withRouter} from 'react-router';
import {
createWebAdmin,
getClusters,
createClusterUserAtSetup,
updateClusterAtSetup,
} from 'shared/apis';
const SignUpApp = React.createClass({
propTypes: {
params: PropTypes.shape({
step: PropTypes.string.isRequired,
}).isRequired,
router: PropTypes.shape({
push: PropTypes.func.isRequired,
replace: PropTypes.func.isRequired,
}).isRequired,
},
getInitialState() {
return {
clusterDisplayName: null,
clusterIDs: null,
activeClusterID: null,
clusterUser: '',
};
},
componentDidMount() {
getClusters().then(({data: clusters}) => {
const clusterIDs = clusters.map((c) => c.cluster_id); // TODO: handle when the first cluster is down...
this.setState({
clusterIDs,
activeClusterID: clusterIDs[0],
});
});
},
handleNameCluster(clusterDisplayName) {
this.setState({clusterDisplayName}, () => {
this.props.router.replace('/signup/admin/2');
});
},
handleCreateClusterAdmin(username, password) {
const {activeClusterID, clusterDisplayName} = this.state;
createClusterUserAtSetup(activeClusterID, username, password).then(() => {
updateClusterAtSetup(activeClusterID, clusterDisplayName).then(() => {
this.setState({clusterUser: username}, () => {
this.props.router.replace('/signup/admin/3');
});
});
});
},
handleCreateWebAdmin(firstName, lastName, email, password, confirmation, clusterLinks) {
createWebAdmin({firstName, lastName, email, password, confirmation, clusterLinks}).then(() => {
window.location.replace('/');
});
},
render() {
const {params: {step}, router} = this.props;
const {clusterDisplayName, clusterIDs} = this.state;
if (!['1', '2', '3'].includes(step)) {
router.replace('/signup/admin/1');
}
if (clusterIDs === null) {
return null; // spinner?
}
if (!clusterIDs.length) {
return <NoCluster />;
}
if (step === '1' || !clusterDisplayName) {
return <NameCluster onNameCluster={this.handleNameCluster} />;
}
if (step === '2') {
return <CreateClusterAdmin onCreateClusterAdmin={this.handleCreateClusterAdmin} />;
}
if (step === '3') {
return <CreateWebAdmin onCreateWebAdmin={this.handleCreateWebAdmin} />;
}
},
});
export default withRouter(SignUpApp);