Takin' out the trash

pull/10616/head
Will Piers 2016-11-14 17:09:58 -08:00
parent 729ac65367
commit 3b5c59ed15
9 changed files with 0 additions and 865 deletions

View File

@ -1,15 +0,0 @@
FROM node
RUN apt-get update -qq && apt-get install -y build-essential git-core
ENV GOPATH=/go
ENV APP_HOME=$GOPATH/src/github.com/influxdata/enterprise
ENV UI_HOME=$APP_HOME/ui
ARG GITHUB_TOKEN
RUN git config --global url."https://${GITHUB_TOKEN}:x-oauth-basic@github.com/".insteadOf "https://github.com/"
ADD . $APP_HOME
WORKDIR $UI_HOME
RUN npm install

View File

@ -1,62 +0,0 @@
# Enterprise UI
Here lies a collection of React components used in the Enterprise web app.
Currently they're organized per "page", with each separate directly having it's own components/containers folders, index.js, etc. For example:
/ui
/overview
/components
/containers
index.js
/chronograf
/components
/containers
index.js
## Getting started
This project uses Node v5.
It depends on at least one private Node module.
In order to successfully `npm install`, you'll need an [npm authentication token](http://blog.npmjs.org/post/118393368555/deploying-with-npm-private-modules).
There are two ways to get an npm token.
1. You can copy someone else's token (have them give you a copy of their `~/.npmrc`).
This is fine for most cases, and it's probably necessary for CI machines.
2. You can create an npm account and coordinate with Mark R to join the `influxdata` npm org.
You probably don't need this if you aren't ever going to create/publish a private Node module.
### Development
First, in the `ui/` folder run `npm install`.
run `npm run build:dev` to start the webpack process which bundles the JS into `assets/javascripts/generated/`.
### Tests
We use mocha, sinon, chai, and React's TestUtils library.
Run tests against jsdom from the command line:
```
npm run test # single run
npm run test:watch # re-run tests on file changes
```
Run tests in the browser:
```
# This starts a webpack process that you'll need to leave running.
# It rebuilds your tests on each file change.
npm run test:browser
# open http://localhost:7357/spec/test.html
```
### Production
As before, `npm install` first.
Then `npm run build` to generate a production build into the `assets/javascripts/generated/` folder.
If you want to run tests against the production build, `npm run test` will build the test files and run the tests in a headless Phantom browser.

View File

@ -1,108 +0,0 @@
import TasksPage from 'src/tasks/containers/TasksPage';
import RebalanceModal from 'src/tasks/components/RebalanceModal';
import React from 'react';
import {mount, shallow} from 'enzyme';
const clusterID = '1000';
const JOBS = [
{
"source": "localhost:8088",
"dest": "localhost:8188",
"id": 20,
"status": "Planned"
},
{
"source": "localhost:8088",
"dest": "localhost:8188",
"id": 10,
"status": "Planned"
},
];
function setup(customProps = {}) {
const props = Object.assign({}, {
params: {clusterID},
}, customProps);
return mount(<TasksPage {...props} />);
}
function setupShallow(customProps = {}) {
const props = Object.assign({}, {
params: {clusterID},
}, customProps);
return shallow(<TasksPage {...props} />);
}
describe('Tasks.Containers.TasksPage', function() {
before(function() { this.server = sinon.fakeServer.create(); });
after(function() { this.server.restore() });
beforeEach(function() {
this.server.respondWith("GET", /\/api\/int\/v1\/clusters\/1000\/authorized/, [
200, { "Content-Type": "application/json" }, '']);
this.server.respondWith("GET", '/api/v1/jobs', [
200, { "Content-Type": "application/json" }, '']);
});
describe('in intial state with no tasks', function() {
it('renders a an empty state for the task list', function() {
const wrapper = setupShallow();
const table = wrapper.find('.tasks-empty-state');
expect(table.text()).to.match(/No tasks/);
});
it('renders a legend', function() {
const wrapper = setupShallow();
const legend = wrapper.find('.dot-legend');
expect(legend.text()).to.match(/Running/);
expect(legend.text()).to.match(/Planned/);
});
it('renders a rebalance modal', function() {
const wrapper = setupShallow();
expect(wrapper.find(RebalanceModal).length).to.equal(1);
});
it('fetches a list of active tasks', function(done) {
const wrapper = setup();
this.server.respond();
setTimeout(() => {
const request = this.server.requests.find(r => r.url.match(/\/api\/v1\/jobs/));
expect(request).to.be.ok;
expect(request.method).to.equal('GET');
done();
});
});
});
describe('when there are active tasks', function() {
it('renders a table row for each task');
});
describe('with valid permissions', function() {
it('renders the rebalance button', function() {
const wrapper = setupShallow();
// TODO: get this working with FakeServer. For some reason I can't
// get it to respond and trigger the `then` block in componentDidMount.
// Lots of async issues with testing containers like this :(.
wrapper.setState({canRebalance: true});
wrapper.update();
const button = wrapper.find('button.rebalance');
expect(button.length).to.equal(1);
});
it('sends a request to rebalance after the rebalance button is clicked');
});
describe('with invalid permissions', function() {
it('doesn\'t render the rebalance button');
});
});

View File

@ -8,7 +8,6 @@ import AlertsApp from 'src/alerts';
import CheckSources from 'src/CheckSources';
import {HostsPage, HostPage} from 'src/hosts';
import {KapacitorPage, KapacitorRulePage, KapacitorRulesPage, KapacitorTasksPage} from 'src/kapacitor';
import TasksPage from 'src/tasks';
import DataExplorer from 'src/chronograf';
import {CreateSource, SourceForm, ManageSources} from 'src/sources';
import NotFound from 'src/shared/components/NotFound';
@ -107,7 +106,6 @@ const Root = React.createClass({
<Route path="alert-rules/:ruleID" component={KapacitorRulePage} />
<Route path="alert-rules/new" component={KapacitorRulePage} />
</Route>
<Route path="tasks" component={TasksPage} />
<Route path="*" component={NotFound} />
</Route>
</Router>

View File

@ -35,435 +35,6 @@ export function deleteSource(source) {
});
}
export function updateCluster(clusterID, displayName) {
return AJAX({
url: `/api/int/v1/clusters/${clusterID}`,
method: 'PUT',
data: {
display_name: displayName,
},
});
}
export function getDatabaseManager(clusterID, dbName) {
return AJAX({
url: `/api/int/v1/${clusterID}/databases/${dbName}`,
});
}
export function createDatabase({database, rpName, duration, replicaN}) {
const params = new window.URLSearchParams();
params.append('name', database);
params.append('retention-policy', rpName);
params.append('duration', duration);
params.append('replication-factor', replicaN);
return AJAX({
url: `/api/int/v1/databases`,
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
data: params,
});
}
export function getClusters() {
return AJAX({
url: ``,
});
}
export function meShow() {
return AJAX({
url: `/api/int/v1/me`,
});
}
export function meUpdate({firstName, lastName, email, password, confirmation, oldPassword}) {
return AJAX({
url: `/api/int/v1/me`,
method: 'PUT',
data: {
first_name: firstName,
last_name: lastName,
email,
password,
confirmation,
old_password: oldPassword,
},
});
}
export function getWebUsers() {
return AJAX({
url: `/api/int/v1/users`,
});
}
export function createWebUser({firstName, lastName, email, password}) {
return AJAX({
url: `/api/int/v1/users`,
method: 'POST',
data: {
first_name: firstName,
last_name: lastName,
email,
password,
},
});
}
export function deleteWebUsers(userID) {
return AJAX({
url: `/api/int/v1/users/${userID}`,
method: 'DELETE',
});
}
export function showUser(userID) {
return AJAX({
url: `/api/int/v1/users/${userID}`,
});
}
export function updateUser(userID, {firstName, lastName, email, password, confirmation, admin}) {
return AJAX({
url: `/api/int/v1/users/${userID}`,
method: 'PUT',
data: {
first_name: firstName,
last_name: lastName,
email,
password,
confirmation,
admin,
},
});
}
export function getClusterAccounts(clusterID) {
return AJAX({
url: metaProxy(clusterID, '/user'),
});
}
// can only be used for initial app setup. will create first cluster user
// with global admin permissions.
export function createClusterUserAtSetup(clusterID, username, password) {
return AJAX({
url: `/api/v1/setup/cluster_user`,
method: 'POST',
data: {
cluster_id: clusterID,
username,
password,
},
});
}
// can only be used for initial app setup
export function createWebAdmin({firstName, lastName, email, password, confirmation, clusterLinks}) {
return AJAX({
url: `/api/v1/setup/admin`,
method: 'POST',
data: {
first_name: firstName,
last_name: lastName,
email,
password,
confirmation,
cluster_links: clusterLinks,
},
});
}
// can only be used for initial app setup
export function updateClusterAtSetup(clusterID, displayName) {
return AJAX({
url: `/api/v1/setup/clusters/${clusterID}`,
method: 'POST',
data: {
display_name: displayName,
},
});
}
export function createClusterUser(clusterID, name, password) {
return AJAX({
url: metaProxy(clusterID, '/user'),
method: 'POST',
data: {
action: 'create',
user: {
name,
password,
},
},
});
}
export function addUsersToRole(clusterID, name, users) {
return AJAX({
url: metaProxy(clusterID, '/role'),
method: 'POST',
data: {
action: 'add-users',
role: {
name,
users,
},
},
});
}
export function getClusterAccount(clusterID, accountID) {
return AJAX({
url: metaProxy(clusterID, `/user?name=${encodeURIComponent(accountID)}`),
});
}
export function updateClusterAccountPassword(clusterID, name, password) {
return AJAX({
url: metaProxy(clusterID, '/user'),
method: 'POST',
data: {
action: 'change-password',
user: {
name,
password,
},
},
});
}
export function getRoles(clusterID) {
return AJAX({
url: metaProxy(clusterID, '/role'),
});
}
export function createRole(clusterID, roleName) {
return AJAX({
url: metaProxy(clusterID, '/role'),
method: 'POST',
data: {
action: 'create',
role: {
name: roleName,
},
},
});
}
// TODO: update usage on index page
export function deleteClusterAccount(clusterID, accountName) {
return Promise.all([
// Remove the cluster account from plutonium.
AJAX({
url: metaProxy(clusterID, '/user'),
method: `POST`,
data: {
action: 'delete',
user: {
name: accountName,
},
},
}),
// Remove any cluster user links that are tied to this cluster account.
AJAX({
url: `/api/int/v1/user_links/batch/${accountName}`,
method: 'DELETE',
}),
]);
}
export function createClusterAccount(clusterID, name, password) {
return AJAX({
url: metaProxy(clusterID, '/user'),
method: `POST`,
data: {
action: 'create',
user: {
name,
password,
},
},
});
}
export function addAccountsToRole(clusterID, roleName, usernames) {
return AJAX({
url: metaProxy(clusterID, '/role'),
method: 'POST',
data: {
action: 'add-users',
role: {
name: roleName,
users: usernames,
},
},
});
}
export function removeAccountsFromRole(clusterID, roleName, usernames) {
return AJAX({
url: metaProxy(clusterID, '/role'),
method: 'POST',
data: {
action: 'remove-users',
role: {
name: roleName,
users: usernames,
},
},
});
}
export function addPermissionToRole(clusterID, roleName, permission) {
const permissions = buildPermissionForPlutonium(permission);
return AJAX({
url: metaProxy(clusterID, '/role'),
method: 'POST',
data: {
action: 'add-permissions',
role: {
name: roleName,
permissions,
},
},
});
}
export function removePermissionFromRole(clusterID, roleName, permission) {
const permissions = buildPermissionForPlutonium(permission);
return AJAX({
url: metaProxy(clusterID, '/role'),
method: 'POST',
data: {
action: 'remove-permissions',
role: {
name: roleName,
permissions,
},
},
});
}
export function removePermissionFromAccount(clusterID, username, permission) {
const permissions = buildPermissionForPlutonium(permission);
return AJAX({
url: metaProxy(clusterID, '/user'),
method: 'POST',
data: {
action: 'remove-permissions',
user: {
name: username,
permissions,
},
},
});
}
// The structure that plutonium expects for adding permissions is a little unorthodox,
// where the permission(s) being added have to be under a resource key, e.g.
// {
// "db1": ["ViewAdmin"],
// "": ["CreateRole"]
// }
// This transforms a more web client-friendly permissions object into something plutonium understands.
function buildPermissionForPlutonium({name, resources}) {
return resources.reduce((obj, resource) => {
obj[resource] = [name];
return obj;
}, {});
}
export function addPermissionToAccount(clusterID, name, permission, resources) {
const permissions = resources.reduce((obj, resource) => {
obj[resource] = [permission];
return obj;
}, {});
return AJAX({
url: metaProxy(clusterID, '/user'),
method: 'POST',
data: {
action: 'add-permissions',
user: {
name,
permissions,
},
},
});
}
export function deleteRole(clusterID, roleName) {
return AJAX({
url: metaProxy(clusterID, '/role'),
method: 'POST',
data: {
action: 'delete',
role: {
name: roleName,
},
},
});
}
export function deleteUserClusterLink(clusterID, userClusterLinkID) {
return AJAX({
url: `/api/int/v1/user_links/${userClusterLinkID}`,
method: `DELETE`,
});
}
export function getUserClusterLinks() {
return AJAX({
url: `/api/int/v1/user_links`,
});
}
export function createUserClusterLink({userID, clusterID, clusterUser}) {
return AJAX({
url: `/api/int/v1/user_links`,
method: 'POST',
data: {
user_id: userID,
cluster_user: clusterUser,
cluster_id: clusterID,
},
});
}
export function getWebUsersByClusterAccount(clusterID, clusterAccount) {
return AJAX({
url: `/api/int/v1/user_links/batch/${encodeURIComponent(clusterAccount)}`,
});
}
export function batchCreateUserClusterLink(userID, clusterLinks) {
return AJAX({
url: `/api/int/v1/users/${userID}/cluster_links/batch`,
method: 'POST',
data: clusterLinks,
});
}
export function addWebUsersToClusterAccount(clusterID, clusterAccount, userIDs) {
return AJAX({
url: `/api/int/v1/user_links/batch/${encodeURIComponent(clusterAccount)}`,
method: 'POST',
data: userIDs,
});
}
function metaProxy(clusterID, slug) {
return `/api/int/v1/meta${slug}`;
}
// Kapacitor functions
// TODO: update kapacitor functions to assume only one kapacitor. waiting for @goller
export function getKapacitor(source) {
return AJAX({
url: source.links.kapacitors,
@ -503,7 +74,6 @@ export function getKapacitorConfig(kapacitor) {
return kapacitorProxy(kapacitor, 'GET', '/kapacitor/v1/config', '');
}
// updateKapacitorConfigSection will update one section in the Kapacitor config.
export function updateKapacitorConfigSection(kapacitor, section, properties) {
return AJAX({
method: 'POST',

View File

@ -1,31 +0,0 @@
import React, {PropTypes} from 'react';
const {func} = PropTypes;
const RebalanceModal = React.createClass({
propTypes: {
onConfirmRebalance: func.isRequired,
},
render() {
return (
<div className="modal fade" id="rebalanceModal" tabIndex="-1" role="dialog">
<div className="modal-dialog" role="document">
<div className="modal-content">
<div className="modal-header">
<button type="button" className="close" data-dismiss="modal">
<span aria-hidden="true">×</span>
</button>
<h4 className="modal-title" id="myModalLabel">This is a potentially heavy operation. <br/> Are you sure you want to rebalance?</h4>
</div>
<div className="modal-footer">
<button type="button" className="btn btn-default" data-dismiss="modal">No</button>
<button type="button" className="btn btn-success" data-dismiss="modal" onClick={this.props.onConfirmRebalance}>Yes, run it!</button>
</div>
</div>
</div>
</div>
);
},
});
export default RebalanceModal;

View File

@ -1,30 +0,0 @@
import React, {PropTypes} from 'react';
const {shape, string, number} = PropTypes;
const Task = React.createClass({
propTypes: {
task: shape({
source: string,
dest: string,
id: number,
status: string,
}).isRequired,
},
render() {
const {id, source, dest, status} = this.props.task;
return (
<tr className="task" id={`task-${id}`}>
<td><div className={`status-dot status-dot__${status.toLowerCase()}`}></div></td>
<td>Copy Shard</td>{/* TODO: Copy Shard is hardcoded, change when he have more types of tasks */}
<td>{source}</td>
<td>{dest}</td>
{/* TODO: add killing a task into app when it exists in backend
<td className="text-right"><a href="#" data-toggle="modal" data-task-id={id} data-target="#killModal">Kill</a></td>
*/}
</tr>
);
},
});
export default Task;

View File

@ -1,185 +0,0 @@
import React, {PropTypes} from 'react';
import RebalanceModal from '../components/RebalanceModal';
import Task from '../components/Task';
import AJAX from 'utils/ajax';
import {meShow} from 'shared/apis';
const REFRESH_INTERVAL = 2000;
const REBALANCE_PERMISSION = 'Rebalance';
const Tasks = React.createClass({
propTypes: {
params: PropTypes.shape({
clusterID: PropTypes.string.isRequired,
}).isRequired,
},
getInitialState() {
return {
isRebalancing: false,
canRebalance: false,
tasks: [],
};
},
componentDidMount() {
this.fetchTasks();
meShow().then(({data}) => {
const clusterAccount = data.cluster_links.find((cl) => cl.cluster_id === this.props.params.clusterID);
if (!clusterAccount) {
return this.setState({canRebalance: false});
}
AJAX({
url: `/api/int/v1/clusters/${this.props.params.clusterID}/meta/authorized?password=""&resource=""`,
params: {
name: clusterAccount.cluster_user,
permission: REBALANCE_PERMISSION,
},
}).then(() => {
this.setState({canRebalance: true});
}).catch(() => {
this.setState({canRebalance: false});
});
});
},
fetchTasks() {
AJAX({
url: '/api/v1/jobs',
}).then((resp) => {
const tasks = resp.data;
this.setState({tasks});
if (tasks.length) {
this.setState({isRebalancing: true});
if (!this.intervalID) {
this.intervalID = setInterval(() => this.fetchTasks(), REFRESH_INTERVAL);
}
} else {
this.setState({isRebalancing: false});
clearInterval(this.intervalID);
}
});
},
handleRebalance() {
AJAX({
url: `/clusters/${this.props.params.clusterID}/rebalance`,
method: 'POST',
}).then(() => {
this.fetchTasks();
}).catch(() => {
// TODO: render flash message
});
},
renderRebalanceButton() {
if (!this.state.canRebalance) {
return null;
}
if (this.state.isRebalancing) {
return (
<div disabled={true} className="btn btn-sm btn-success rebalance">
<div>Rebalancing</div>
<div id="rebalance" className="icon sync"/>
</div>
);
}
return (
<button className="btn btn-sm btn-primary rebalance" data-toggle="modal" data-target="#rebalanceModal">
Rebalance
</button>
);
},
render() {
const {tasks} = this.state;
return (
<div>
<div className="enterprise-header">
<div className="enterprise-header__container">
<div className="enterprise-header__left">
<h1>
Tasks
</h1>
</div>
<div className="enterprise-header__right">
{this.renderRebalanceButton()}
</div>
</div>
</div>
<div className="container-fluid">
<div className="row">
<div className="col-md-12">
<div className="panel panel-minimal">
<div className="panel-heading u-flex u-jc-space-between u-ai-center">
<h2 className="panel-title">Running Tasks</h2>
<div className="dot-container">
<ul className="dot-legend">
{/* TODO: add back in when task histrory is introduced
<li>
<div className="status-dot status-dot__finished">
</div>
Finished
</li>
*/}
<li>
<div className="status-dot status-dot__running">
</div>
Running
</li>
<li>
<div className="status-dot status-dot__planned">
</div>
Planned
</li>
{/* TODO: add back in when task histrory is introduced
<li>
<div className="status-dot status-dot__failed">
</div>
Failed
</li>
*/}
</ul>
</div>
</div>
{tasks.length ?
<div className="panel-body">
<table className="table task-table">
<thead>
<tr>
<th>Status</th>
<th>Type</th>
<th>Source</th>
<th>Destination</th>
<th></th>
</tr>
</thead>
<tbody>
{tasks.map((task, index) => <Task key={index} task={task} />)}
</tbody>
</table>
</div> :
<div className="panel-body">
<div className="generic-empty-state tasks-empty-state">
<span className="icon cubo-node"></span>
<h4>No tasks running</h4>
</div>
</div>
}
</div>
</div>
</div>
</div>
<RebalanceModal onConfirmRebalance={this.handleRebalance} />
</div>
);
},
});
export default Tasks;

View File

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