Merge pull request #1117 from influxdata/feature/remove-archive
Remove archived enterprise web contentpull/10616/head
commit
60d4825233
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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);
|
|
@ -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;
|
|
@ -1,3 +0,0 @@
|
|||
import RolesPageContainer from './containers/RolesPageContainer';
|
||||
import RolePageContainer from './containers/RolePageContainer';
|
||||
export {RolesPageContainer, RolePageContainer};
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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} <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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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);
|
|
@ -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;
|
|
@ -1,4 +0,0 @@
|
|||
import ClusterAccountsPage from './containers/ClusterAccountsPageContainer';
|
||||
import ClusterAccountPage from './containers/ClusterAccountContainer';
|
||||
export {ClusterAccountsPage, ClusterAccountPage};
|
||||
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -1,2 +0,0 @@
|
|||
import DatabaseManagerApp from './containers/DatabaseManagerApp';
|
||||
export default DatabaseManagerApp;
|
|
@ -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;
|
|
@ -1,2 +0,0 @@
|
|||
import QueriesPage from './containers/QueriesPage';
|
||||
export default QueriesPage;
|
|
@ -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,
|
||||
});
|
||||
},
|
||||
});
|
|
@ -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;
|
|
@ -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>
|
||||
);
|
||||
},
|
||||
});
|
|
@ -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>
|
||||
);
|
||||
},
|
||||
});
|
|
@ -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 */
|
|
@ -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;
|
|
@ -1,2 +0,0 @@
|
|||
import RetentionPoliciesApp from './containers/RetentionPoliciesApp';
|
||||
export default RetentionPoliciesApp;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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);
|
Loading…
Reference in New Issue