You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@couchdb.apache.org by GitBox <gi...@apache.org> on 2018/03/12 14:11:27 UTC

[GitHub] popojargo closed pull request #1066: Bring back compaction addon

popojargo closed pull request #1066: Bring back compaction addon
URL: https://github.com/apache/couchdb-fauxton/pull/1066
 
 
   

This is a PR merged from a forked repository.
As GitHub hides the original diff on merge, it is displayed below for
the sake of provenance:

As this is a foreign pull request (from a fork), the diff is supplied
below (as it won't show otherwise due to GitHub magic):

diff --git a/app/addons/compaction/actions.js b/app/addons/compaction/actions.js
new file mode 100644
index 000000000..3ef4469d3
--- /dev/null
+++ b/app/addons/compaction/actions.js
@@ -0,0 +1,130 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+import {
+  COMPACTION_CLEANUP_FINISHED,
+  COMPACTION_CLEANUP_STARTED, COMPACTION_COMPACTION_FINISHED,
+  COMPACTION_COMPACTION_STARTING, COMPACTION_VIEW_FINISHED, COMPACTION_VIEW_STARTED
+} from "./actiontypes";
+
+import {post} from '../../core/ajax';
+import app from '../../app';
+import FauxtonAPI from "../../core/api";
+
+export const compactionStarted = () => async dispatch => {
+  dispatch({
+    type: COMPACTION_COMPACTION_STARTING
+  });
+};
+
+export const compactionFinished = () => async dispatch => {
+  dispatch({
+    type: COMPACTION_COMPACTION_FINISHED
+  });
+};
+
+export const cleaningViewsStarted = () => async dispatch => {
+  dispatch({
+    type: COMPACTION_CLEANUP_STARTED
+  });
+};
+
+export const cleaningViewsFinished = () => async dispatch => {
+  dispatch({
+    type: COMPACTION_CLEANUP_FINISHED
+  });
+};
+
+export const compactViewStarted = () => async dispatch => {
+  dispatch({
+    type: COMPACTION_VIEW_STARTED
+  });
+};
+
+export const compactViewFinished = () => async dispatch => {
+  dispatch({
+    type: COMPACTION_VIEW_FINISHED
+  });
+};
+
+export const compactDatabase = databaseName => async dispatch => {
+  dispatch(compactionStarted());
+  const url = app.host + "/" + databaseName + "/_compact";
+  try {
+    const json = await post(url);
+    if (json.ok) {
+      FauxtonAPI.addNotification({
+        type: 'success',
+        msg: 'Database compaction has started. Visit <a href="#activetasks">Active Tasks</a> to view the compaction progress.',
+        escape: false // beware of possible XSS when the message changes
+      });
+    } else {
+      throw new Error(json.reason);
+    }
+  } catch (error) {
+    FauxtonAPI.addNotification({
+      msg: 'Database compaction failed - reason: ' + error,
+      type: 'error'
+    });
+  } finally {
+    dispatch(compactionFinished());
+  }
+};
+
+export const cleanupViews = databaseName => async dispatch => {
+  dispatch(cleaningViewsStarted());
+  const url = app.host + "/" + databaseName + "/_view_cleanup";
+  try {
+    const json = await post(url);
+    if (json.ok) {
+      FauxtonAPI.addNotification({
+        type: 'success',
+        msg: 'View cleanup has started. Visit <a href="#activetasks">Active Tasks</a> to view progress.',
+        escape: false // beware of possible XSS when the message changes
+      });
+    } else {
+      throw new Error(json.reason);
+    }
+  } catch (error) {
+    FauxtonAPI.addNotification({
+      msg: 'View cleanup failed - reason: ' + error,
+      type: 'error'
+    });
+  } finally {
+    dispatch(cleaningViewsFinished());
+  }
+};
+
+export const compactView = (databaseName, designDocName) => async dispatch => {
+  dispatch(compactViewStarted());
+
+  const url = app.host + "/" + databaseName + "/_compact/" + designDocName.replace('_design/', '');
+  try {
+    const json = await post(url);
+    if (json.ok) {
+      FauxtonAPI.addNotification({
+        type: 'success',
+        msg: 'View compaction has started. Visit <a href="#activetasks">Active Tasks</a> to view progress.',
+        escape: false // beware of possible XSS when the message changes
+      });
+    } else {
+      throw new Error(json.reason);
+    }
+  } catch (error) {
+    FauxtonAPI.addNotification({
+      msg: 'View compaction failed - reason: ' + error,
+      type: 'error'
+    });
+  } finally {
+    dispatch(compactViewFinished());
+  }
+};
diff --git a/app/addons/compaction/actiontypes.js b/app/addons/compaction/actiontypes.js
new file mode 100644
index 000000000..36a8e4d3c
--- /dev/null
+++ b/app/addons/compaction/actiontypes.js
@@ -0,0 +1,18 @@
+// Licensed under the Apache License; Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http=//www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing; software
+// distributed under the License is distributed on an "AS IS" BASIS; WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND; either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+export const COMPACTION_COMPACTION_STARTING = 'COMPACTION_COMPACTION_STARTING';
+export const COMPACTION_COMPACTION_FINISHED = 'COMPACTION_COMPACTION_FINISHED';
+export const COMPACTION_CLEANUP_STARTED = 'COMPACTION_CLEANUP_STARTED';
+export const COMPACTION_CLEANUP_FINISHED = 'COMPACTION_CLEANUP_FINISHED';
+export const COMPACTION_VIEW_STARTED = 'COMPACTION_VIEW_STARTED';
+export const COMPACTION_VIEW_FINISHED = 'COMPACTION_VIEW_FINISHED';
diff --git a/app/addons/compaction/assets/less/compaction.less b/app/addons/compaction/assets/less/compaction.less
new file mode 100644
index 000000000..4083c6a9f
--- /dev/null
+++ b/app/addons/compaction/assets/less/compaction.less
@@ -0,0 +1,24 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+@import "../../../../../assets/less/variables.less";
+
+.compaction-option {
+  background-color: #F7F7F7;
+  border: 1px solid #DDD;
+  margin-bottom: 30px;
+  padding: 10px;
+}
+
+#dashboard-content.flex-layout .compaction-page {
+  padding: @panelPadding;
+}
diff --git a/app/addons/compaction/base.js b/app/addons/compaction/base.js
new file mode 100644
index 000000000..288d57239
--- /dev/null
+++ b/app/addons/compaction/base.js
@@ -0,0 +1,33 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+import FauxtonAPI from '../../core/api';
+import Compaction from './routes';
+import './assets/less/compaction.less';
+import ViewCompactionButtonContainer from './containers/ViewCompactionButtonContainer';
+import reducer from './reducer';
+
+Compaction.initialize = function () {
+  FauxtonAPI.registerExtension('docLinks', {
+    title: "Compact & Clean",
+    url: "compact",
+    icon: "icon-cogs"
+  });
+
+  FauxtonAPI.registerExtension('view-editor:compaction-button', ViewCompactionButtonContainer);
+};
+
+FauxtonAPI.addReducers({
+  compaction: reducer
+});
+
+export default Compaction;
diff --git a/app/addons/compaction/components/CleanView.js b/app/addons/compaction/components/CleanView.js
new file mode 100644
index 000000000..c29037629
--- /dev/null
+++ b/app/addons/compaction/components/CleanView.js
@@ -0,0 +1,31 @@
+import React from 'react';
+
+export class CleanView extends React.Component {
+  constructor() {
+    super();
+    this.run = this.run.bind(this);
+  }
+  run() {
+    this.props.cleanupViews(this.props.database);
+  }
+
+  render() {
+    const {isCleaningViews} = this.props;
+    let btnText = 'Run';
+
+    if (isCleaningViews) {
+      btnText = 'Cleaning Views...';
+    }
+    return (
+      <div className="row-fluid">
+        <div className="span12 compaction-option">
+          <h3>Cleanup Views</h3>
+          <p>Cleaning up views in a database removes old view files still stored on the filesystem. It is an
+                        irreversible operation.</p>
+          <button id="cleanup-views" disabled={isCleaningViews} onClick={this.run}
+            className="btn btn-large btn-primary">{btnText}</button>
+        </div>
+      </div>
+    );
+  }
+}
diff --git a/app/addons/compaction/components/CompactDatabase.js b/app/addons/compaction/components/CompactDatabase.js
new file mode 100644
index 000000000..c5e62135b
--- /dev/null
+++ b/app/addons/compaction/components/CompactDatabase.js
@@ -0,0 +1,36 @@
+
+import React from 'react';
+
+
+export class CompactDatabase extends React.Component {
+
+  constructor() {
+    super();
+    this.run = this.run.bind(this);
+  }
+
+  run() {
+    this.props.compactDatabase(this.props.database);
+  }
+
+  render() {
+    const {isCompacting} = this.props;
+    let btnText = 'Run';
+
+    if (isCompacting) {
+      btnText = 'Compacting...';
+    }
+
+    return (
+      <div className="row-fluid">
+        <div className="span12 compaction-option">
+          <h3>Compact Database</h3>
+          <p>Compacting a database removes deleted documents and previous revisions. It is an irreversible
+                        operation and may take a while to complete for large databases.</p>
+          <button id="compact-db" disabled={isCompacting} onClick={this.run}
+            className="btn btn-large btn-primary">{btnText}</button>
+        </div>
+      </div>
+    );
+  }
+}
diff --git a/app/addons/compaction/components/CompactionController.js b/app/addons/compaction/components/CompactionController.js
new file mode 100644
index 000000000..a5128fdf9
--- /dev/null
+++ b/app/addons/compaction/components/CompactionController.js
@@ -0,0 +1,14 @@
+import React from 'react';
+import {CompactDatabase} from "./CompactDatabase";
+import {CleanView} from "./CleanView";
+export default class CompactionController extends React.Component {
+  render () {
+    const {isCompacting, isCleaningViews, cleanupViews, compactDatabase, database} = this.props;
+    return (
+      <div className="compaction-page flex-body">
+        <CompactDatabase isCompacting={isCompacting} database={database} compactDatabase={compactDatabase}/>
+        <CleanView isCleaningViews={isCleaningViews} database={database} cleanupViews={cleanupViews}/>
+      </div>
+    );
+  }
+}
diff --git a/app/addons/compaction/components/ViewCompactionButton.js b/app/addons/compaction/components/ViewCompactionButton.js
new file mode 100644
index 000000000..e46f9a016
--- /dev/null
+++ b/app/addons/compaction/components/ViewCompactionButton.js
@@ -0,0 +1,40 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+import React from 'react';
+
+export class ViewCompactionButton extends React.Component {
+  constructor() {
+    super();
+    this.onClick = this.onClick.bind(this);
+  }
+  onClick(e) {
+    e.preventDefault();
+    this.props.compactView(this.props.database, this.props.designDoc);
+  }
+
+  render() {
+    const {isCompactingView} = this.props;
+    let btnMsg = 'Compact View';
+
+    if (isCompactingView) {
+      btnMsg = 'Compacting View';
+    }
+
+    return (
+      <button disabled={isCompactingView}
+        className="btn btn-info pull-right"
+        onClick={this.onClick}>{btnMsg}</button>
+    );
+  }
+
+}
diff --git a/app/addons/compaction/containers/CompactionContainer.js b/app/addons/compaction/containers/CompactionContainer.js
new file mode 100644
index 000000000..81d598378
--- /dev/null
+++ b/app/addons/compaction/containers/CompactionContainer.js
@@ -0,0 +1,42 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+import {connect} from 'react-redux';
+
+import CompactionController from '../components/CompactionController';
+
+import {isCleaningViews, isCompacting} from "../reducer";
+import {cleanupViews, compactDatabase} from "../actions";
+
+const mapStateToProps = ({compaction}, ownProps) => {
+  return {
+    isCompacting: isCompacting(compaction),
+    isCleaningViews: isCleaningViews(compaction),
+    database: ownProps.database
+  };
+};
+
+const mapDispatchToProps = dispatch => {
+  return {
+    cleanupViews(databaseName) {
+      dispatch(cleanupViews(databaseName));
+    },
+    compactDatabase(databaseName) {
+      dispatch(compactDatabase(databaseName));
+    }
+  };
+};
+
+export default connect(
+  mapStateToProps,
+  mapDispatchToProps
+)(CompactionController);
diff --git a/app/addons/compaction/containers/ViewCompactionButtonContainer.js b/app/addons/compaction/containers/ViewCompactionButtonContainer.js
new file mode 100644
index 000000000..032fdd007
--- /dev/null
+++ b/app/addons/compaction/containers/ViewCompactionButtonContainer.js
@@ -0,0 +1,39 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+import {connect} from 'react-redux';
+
+import {ViewCompactionButton} from '../components/ViewCompactionButton';
+
+import {isCompactingView} from "../reducer";
+import {compactView} from "../actions";
+
+const mapStateToProps = ({compaction}, ownProps) => {
+  return {
+    isCompactingView: isCompactingView(compaction),
+    database: ownProps.database,
+    designDoc: ownProps.designDoc
+  };
+};
+
+const mapDispatchToProps = dispatch => {
+  return {
+    compactView(databaseName, designDoc) {
+      dispatch(compactView(databaseName, designDoc));
+    }
+  };
+};
+
+export default connect(
+  mapStateToProps,
+  mapDispatchToProps
+)(ViewCompactionButton);
diff --git a/app/addons/compaction/layout.js b/app/addons/compaction/layout.js
new file mode 100644
index 000000000..c9ea39b3b
--- /dev/null
+++ b/app/addons/compaction/layout.js
@@ -0,0 +1,42 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+import React from 'react';
+import {TabsSidebarHeader} from '../documents/layouts';
+import CompactionContainer from './containers/CompactionContainer';
+import SidebarComponents from "../documents/sidebar/sidebar";
+
+export const Layout = ({docURL, database, endpoint, dbName, dropDownLinks}) => {
+  return (
+    <div id="dashboard" className="with-sidebar">
+      <TabsSidebarHeader
+        hideQueryOptions={true}
+        hideJumpToDoc={true}
+        docURL={docURL}
+        endpoint={endpoint}
+        dbName={dbName}
+        dropDownLinks={dropDownLinks}
+        database={database}
+      />
+      <div className="with-sidebar tabs-with-sidebar content-area">
+        <aside id="sidebar-content" className="scrollable">
+          <SidebarComponents.SidebarController />
+        </aside>
+        <section id="dashboard-content" className="flex-layout flex-col">
+          <CompactionContainer database={database.id} />
+        </section>
+      </div>
+    </div>
+  );
+};
+
+export default Layout;
diff --git a/app/addons/compaction/reducer.js b/app/addons/compaction/reducer.js
new file mode 100644
index 000000000..d135f950d
--- /dev/null
+++ b/app/addons/compaction/reducer.js
@@ -0,0 +1,68 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+
+import {
+  COMPACTION_CLEANUP_FINISHED, COMPACTION_CLEANUP_STARTED, COMPACTION_COMPACTION_FINISHED,
+  COMPACTION_COMPACTION_STARTING,
+  COMPACTION_VIEW_FINISHED,
+  COMPACTION_VIEW_STARTED
+} from "./actiontypes";
+import _ from 'lodash';
+
+const cloneAssign = (state, toAssign = {}) => {
+  let newState = _.cloneDeep(state);
+  _.assign(newState, toAssign);
+  return newState;
+};
+
+const initialState = {
+  isCompacting: false,
+  isCleaningView: false,
+  isCompactingView: false
+};
+
+export default function compaction(state = initialState, action) {
+  const {type} = action;
+  switch (type) {
+    case COMPACTION_COMPACTION_STARTING:
+      return cloneAssign(state, {
+        isCompacting: true
+      });
+    case COMPACTION_COMPACTION_FINISHED:
+      return cloneAssign(state, {
+        isCompacting: false
+      });
+    case COMPACTION_CLEANUP_STARTED:
+      return cloneAssign(state, {
+        isCleaningViews: true
+      });
+    case COMPACTION_CLEANUP_FINISHED:
+      return cloneAssign(state, {
+        isCleaningViews: false
+      });
+    case COMPACTION_VIEW_STARTED:
+      return cloneAssign(state, {
+        isCompactingView: true
+      });
+    case COMPACTION_VIEW_FINISHED:
+      return cloneAssign(state, {
+        isCompactingView: false
+      });
+    default:
+      return state;
+  }
+}
+
+export const isCompacting = state => state.isCompacting;
+export const isCleaningViews = state => state.isCleaningViews;
+export const isCompactingView = state => state.isCompactingView;
diff --git a/app/addons/compaction/routes.js b/app/addons/compaction/routes.js
new file mode 100644
index 000000000..c35677d52
--- /dev/null
+++ b/app/addons/compaction/routes.js
@@ -0,0 +1,68 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+import React from 'react';
+import FauxtonAPI from "../../core/api";
+import Databases from "../databases/base";
+
+import BaseRoute from "../documents/shared-routes";
+import Layout from './layout';
+
+const CompactionRouteObject = BaseRoute.extend({
+
+  roles: ['fx_loggedIn'],
+  routes: {
+    "database/:database/compact": "compaction"
+  },
+
+  initialize: function (route, options) {
+    this.initViews(options[0]);
+  },
+
+  initViews: function (databaseName) {
+    this.database = new Databases.Model({id: databaseName});
+
+    this.createDesignDocsCollection();
+    // this.addLeftHeader();
+    this.addSidebar('compact');
+  },
+
+  compaction: function (databaseId) {
+    // XXX magic inheritance props we need to maintain for BaseRoute
+    this.database = new Databases.Model({ id: databaseId });
+
+    // XXX magic methods we have to call - originating from BaseRoute.extend
+    this.createDesignDocsCollection();
+    this.addSidebar('compaction');
+
+    const crumbs = [
+      { name: databaseId, link: Databases.databaseUrl(encodedDatabaseId)},
+      { name: 'Permissions' }
+    ];
+
+    const encodedDatabaseId = encodeURIComponent(databaseId);
+    const url = FauxtonAPI.urls('permissions', 'server', encodedDatabaseId);
+
+    return <Layout
+      docURL={FauxtonAPI.constants.DOC_URLS.COMPACTION}
+      endpoint={url}
+      dbName={this.database.id}
+      dropDownLinks={crumbs}
+      database={this.database} />;
+  }
+});
+
+const Compaction = {
+  RouteObjects: [CompactionRouteObject]
+};
+
+export default Compaction;
diff --git a/app/addons/compaction/tests/components.test.js b/app/addons/compaction/tests/components.test.js
new file mode 100644
index 000000000..29d18dea1
--- /dev/null
+++ b/app/addons/compaction/tests/components.test.js
@@ -0,0 +1,89 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+import React from "react";
+import TestUtils from 'react-dom/test-utils';
+import utils  from "../../../../test/mocha/testUtils";
+import sinon from "sinon";
+import configureMockStore from 'redux-mock-store';
+import {Provider} from "react-redux";
+import {mount} from "enzyme";
+import CompactionContainer from '../containers/CompactionContainer';
+import CompactionController from '../components/CompactionController';
+import {CleanView} from "../components/CleanView";
+import {CompactDatabase} from "../components/CompactDatabase";
+
+const store = configureMockStore()({
+  compaction: {
+    isCompacting: false,
+    isCleaningView: false,
+    isCompactingView: false
+  }
+});
+
+const {assert} = utils;
+
+describe('Compaction Controller', function () {
+  let instance, container;
+
+  beforeEach(() => {
+  // eslint-disable-next-line no-undef
+    spyOn(store, 'dispatch');
+    instance = TestUtils.renderIntoDocument(
+      <Provider store={store}>
+        <CompactionContainer database="my-database"/>
+      </Provider>
+    );
+  });
+
+  it('triggers compact database action', function () {
+    container = TestUtils.findRenderedComponentWithType(instance, CompactionController);
+    container.props.compactDatabase('test');
+    expect(store.dispatch).toHaveBeenCalled();
+  });
+
+  it('triggers clean up view action', function () {
+    container = TestUtils.findRenderedComponentWithType(instance, CompactionController);
+    container.props.cleanupViews('test');
+    expect(store.dispatch).toHaveBeenCalled();
+  });
+});
+
+describe('CleanView', function () {
+  let spy, cleanupViewEl;
+
+  beforeEach(function () {
+    spy = sinon.spy();
+    cleanupViewEl = mount(<CleanView cleanupViews={spy}/>);
+  });
+
+  it('calls cleanupView on button click', function () {
+    cleanupViewEl.find('button').first().simulate('click');
+    assert.ok(spy.calledOnce);
+
+  });
+});
+
+describe('CompactDatabase', function () {
+  let spy, compactViewEl;
+
+  beforeEach(function () {
+    spy = sinon.spy();
+    compactViewEl = mount(<CompactDatabase compactDatabase={spy}/>);
+  });
+
+  it('calls compact database on button click', function () {
+    compactViewEl.find('button').first().simulate('click');
+    assert.ok(spy.calledOnce);
+  });
+});
+
diff --git a/app/addons/compaction/tests/nightwatch/compactAndCleanIsPresent.js b/app/addons/compaction/tests/nightwatch/compactAndCleanIsPresent.js
new file mode 100644
index 000000000..d9b9bdc8b
--- /dev/null
+++ b/app/addons/compaction/tests/nightwatch/compactAndCleanIsPresent.js
@@ -0,0 +1,30 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+module.exports = {
+  'Compact and Clean Menu shows up' : function (client) {
+    const waitTime = client.globals.maxWaitTime,
+          newDatabaseName = client.globals.testDatabaseName,
+          baseUrl = client.globals.test_settings.launch_url;
+
+    client
+      .loginToGUI()
+      .url(baseUrl)
+
+      //navigate to 'Compact & Clean' view
+      .clickWhenVisible('#dashboard-content a[href="#/database/' + newDatabaseName + '/_all_docs"]')
+      .clickWhenVisible('#compact')
+      .waitForElementVisible('#compact-db', waitTime, false)
+      .waitForElementVisible('#cleanup-views', waitTime, false)
+      .end();
+  }
+};
diff --git a/app/addons/documents/index-editor/components/IndexEditor.js b/app/addons/documents/index-editor/components/IndexEditor.js
index 0e0eeceee..a928b5e2a 100644
--- a/app/addons/documents/index-editor/components/IndexEditor.js
+++ b/app/addons/documents/index-editor/components/IndexEditor.js
@@ -19,6 +19,7 @@ import Stores from "../stores";
 import Actions from "../actions";
 import DesignDocSelector from './DesignDocSelector';
 import ReduceEditor from './ReduceEditor';
+import _ from 'lodash';
 
 const getDocUrl = app.helpers.getDocUrl;
 const store = Stores.indexEditorStore;
@@ -99,6 +100,16 @@ export default class IndexEditor extends Component {
     Actions.updateMapCode(code);
   }
 
+  getCompactButton () {
+    const extension = FauxtonAPI.getExtensions('view-editor:compaction-button');
+
+    if (_.isEmpty(extension)) {
+      return null;
+    }
+    const CompactButton = extension[0];
+    return <CompactButton database={this.state.database.id} designDoc={this.state.designDocId}/>;
+  }
+
   render() {
     if (this.state.isLoading) {
       return (
@@ -157,6 +168,7 @@ export default class IndexEditor extends Component {
           <div className="padded-box">
             <div className="control-group">
               <ConfirmButton id="save-view" text={btnLabel} />
+              {!this.state.isNewView && this.getCompactButton()}
               <a href={cancelLink} className="index-cancel-link">Cancel</a>
             </div>
           </div>
diff --git a/app/constants.js b/app/constants.js
index ecb64e38b..69c1a3280 100644
--- a/app/constants.js
+++ b/app/constants.js
@@ -46,6 +46,7 @@ export default {
     VIEWS: '/_utils/docs/intro/overview.html#views',
     MANGO_INDEX: '/_utils/docs/intro/api.html#documents',
     MANGO_SEARCH: '/_utils/docs/intro/api.html#documents',
-    CHANGES: '/_utils/docs/api/database/changes.html?highlight=changes#post--db-_changes'
+    CHANGES: '/_utils/docs/api/database/changes.html?highlight=changes#post--db-_changes',
+    COMPACTION: '/_utils/docs/api/maintenance/compaction.html'
   }
 };
diff --git a/jest-setup.js b/jest-setup.js
index 870b7f8e5..b1b51d615 100644
--- a/jest-setup.js
+++ b/jest-setup.js
@@ -13,7 +13,7 @@
 require('jest');
 require('whatwg-fetch');
 require('mock-local-storage');
-
+require('regenerator-runtime/runtime');
 window.localStorage = global.localStorage;
 window.$ = window.jQuery = require('jquery');
 window._ = require('lodash');
diff --git a/package-lock.json b/package-lock.json
index 97f1c4713..f105385eb 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -357,7 +357,6 @@
           "requires": {
             "anymatch": "1.3.2",
             "async-each": "1.0.1",
-            "fsevents": "1.1.3",
             "glob-parent": "2.0.0",
             "inherits": "2.0.3",
             "is-binary-path": "1.0.1",
@@ -462,16 +461,10 @@
           "version": "1.1.3",
           "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.1.3.tgz",
           "integrity": "sha512-WIr7iDkdmdbxu/Gh6eKEZJL6KPE74/5MEsf2whTOFNxbIoIixogroLdKYqB6FDav4Wavh/lZdzzd3b2KxIXC5Q==",
-          "optional": true,
-          "requires": {
-            "nan": "2.8.0",
-            "node-pre-gyp": "0.6.39"
-          },
           "dependencies": {
             "abbrev": {
               "version": "1.1.0",
-              "bundled": true,
-              "optional": true
+              "bundled": true
             },
             "ajv": {
               "version": "4.11.8",
@@ -488,8 +481,7 @@
             },
             "aproba": {
               "version": "1.1.1",
-              "bundled": true,
-              "optional": true
+              "bundled": true
             },
             "are-we-there-yet": {
               "version": "1.1.4",
@@ -502,28 +494,23 @@
             },
             "asn1": {
               "version": "0.2.3",
-              "bundled": true,
-              "optional": true
+              "bundled": true
             },
             "assert-plus": {
               "version": "0.2.0",
-              "bundled": true,
-              "optional": true
+              "bundled": true
             },
             "asynckit": {
               "version": "0.4.0",
-              "bundled": true,
-              "optional": true
+              "bundled": true
             },
             "aws-sign2": {
               "version": "0.6.0",
-              "bundled": true,
-              "optional": true
+              "bundled": true
             },
             "aws4": {
               "version": "1.6.0",
-              "bundled": true,
-              "optional": true
+              "bundled": true
             },
             "balanced-match": {
               "version": "0.4.2",
@@ -553,11 +540,7 @@
             },
             "brace-expansion": {
               "version": "1.1.7",
-              "bundled": true,
-              "requires": {
-                "balanced-match": "0.4.2",
-                "concat-map": "0.0.1"
-              }
+              "bundled": true
             },
             "buffer-shims": {
               "version": "1.0.0",
@@ -565,13 +548,11 @@
             },
             "caseless": {
               "version": "0.12.0",
-              "bundled": true,
-              "optional": true
+              "bundled": true
             },
             "co": {
               "version": "4.6.0",
-              "bundled": true,
-              "optional": true
+              "bundled": true
             },
             "code-point-at": {
               "version": "1.1.0",
@@ -683,23 +664,11 @@
             },
             "fstream": {
               "version": "1.0.11",
-              "bundled": true,
-              "requires": {
-                "graceful-fs": "4.1.11",
-                "inherits": "2.0.3",
-                "mkdirp": "0.5.1",
-                "rimraf": "2.6.1"
-              }
+              "bundled": true
             },
             "fstream-ignore": {
               "version": "1.0.5",
-              "bundled": true,
-              "optional": true,
-              "requires": {
-                "fstream": "1.0.11",
-                "inherits": "2.0.3",
-                "minimatch": "3.0.4"
-              }
+              "bundled": true
             },
             "gauge": {
               "version": "2.7.4",
@@ -918,21 +887,7 @@
             },
             "node-pre-gyp": {
               "version": "0.6.39",
-              "bundled": true,
-              "optional": true,
-              "requires": {
-                "detect-libc": "1.0.2",
-                "hawk": "3.1.3",
-                "mkdirp": "0.5.1",
-                "nopt": "4.0.1",
-                "npmlog": "4.1.0",
-                "rc": "1.2.1",
-                "request": "2.81.0",
-                "rimraf": "2.6.1",
-                "semver": "5.3.0",
-                "tar": "2.2.1",
-                "tar-pack": "3.4.0"
-              }
+              "bundled": true
             },
             "nopt": {
               "version": "4.0.1",
@@ -1133,6 +1088,13 @@
                 }
               }
             },
+            "string_decoder": {
+              "version": "1.0.1",
+              "bundled": true,
+              "requires": {
+                "safe-buffer": "5.0.1"
+              }
+            },
             "string-width": {
               "version": "1.0.2",
               "bundled": true,
@@ -1142,13 +1104,6 @@
                 "strip-ansi": "3.0.1"
               }
             },
-            "string_decoder": {
-              "version": "1.0.1",
-              "bundled": true,
-              "requires": {
-                "safe-buffer": "5.0.1"
-              }
-            },
             "stringstream": {
               "version": "0.0.5",
               "bundled": true,
@@ -1168,27 +1123,11 @@
             },
             "tar": {
               "version": "2.2.1",
-              "bundled": true,
-              "requires": {
-                "block-stream": "0.0.9",
-                "fstream": "1.0.11",
-                "inherits": "2.0.3"
-              }
+              "bundled": true
             },
             "tar-pack": {
               "version": "3.4.0",
-              "bundled": true,
-              "optional": true,
-              "requires": {
-                "debug": "2.6.8",
-                "fstream": "1.0.11",
-                "fstream-ignore": "1.0.5",
-                "once": "1.4.0",
-                "readable-stream": "2.2.9",
-                "rimraf": "2.6.1",
-                "tar": "2.2.1",
-                "uid-number": "0.0.6"
-              }
+              "bundled": true
             },
             "tough-cookie": {
               "version": "2.3.2",
@@ -1425,8 +1364,7 @@
         "nan": {
           "version": "2.8.0",
           "resolved": "https://registry.npmjs.org/nan/-/nan-2.8.0.tgz",
-          "integrity": "sha1-7XFfP+neArV6XmJS2QqWZ14fCFo=",
-          "optional": true
+          "integrity": "sha1-7XFfP+neArV6XmJS2QqWZ14fCFo="
         },
         "normalize-path": {
           "version": "2.1.1",
@@ -14893,6 +14831,12 @@
       "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=",
       "dev": true
     },
+    "lodash.isplainobject": {
+      "version": "4.0.6",
+      "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
+      "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=",
+      "dev": true
+    },
     "lolex": {
       "version": "2.3.2",
       "resolved": "https://registry.npmjs.org/lolex/-/lolex-2.3.2.tgz",
@@ -18377,20 +18321,12 @@
       }
     },
     "redux-mock-store": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/redux-mock-store/-/redux-mock-store-1.4.0.tgz",
-      "integrity": "sha512-y+SGh/SONWwqs4DiyHjd0H6NMgz368wXDiUjSHuOnMEr4dN9PmjV6N3bNvxoILaIQ7zeVKclLyxsCQ2TwGZfEw==",
+      "version": "1.5.1",
+      "resolved": "https://registry.npmjs.org/redux-mock-store/-/redux-mock-store-1.5.1.tgz",
+      "integrity": "sha512-B+iZ98ESHw4EAWVLKUknQlop1OdLKOayGRmd6KavNtC0zoSsycD8hTt0hEr1eUTw2gmYJOdfBY5QAgZweTUcLQ==",
       "dev": true,
       "requires": {
         "lodash.isplainobject": "4.0.6"
-      },
-      "dependencies": {
-        "lodash.isplainobject": {
-          "version": "4.0.6",
-          "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
-          "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=",
-          "dev": true
-        }
       }
     },
     "redux-thunk": {
@@ -19047,6 +18983,14 @@
       "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz",
       "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4="
     },
+    "string_decoder": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz",
+      "integrity": "sha1-D8Z9fBQYJd6UKC3VNr7GubzoYKs=",
+      "requires": {
+        "safe-buffer": "5.1.1"
+      }
+    },
     "string-width": {
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
@@ -19056,14 +19000,6 @@
         "strip-ansi": "4.0.0"
       }
     },
-    "string_decoder": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz",
-      "integrity": "sha1-D8Z9fBQYJd6UKC3VNr7GubzoYKs=",
-      "requires": {
-        "safe-buffer": "5.1.1"
-      }
-    },
     "strip-ansi": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
@@ -19271,7 +19207,7 @@
       }
     },
     "url-polyfill": {
-      "version": "git+https://github.com/webcomponents/URL.git#812ca9b2baae653290aceb36362d219cb96c0f35"
+      "version": "git+https://github.com/webcomponents/URL.git#54e1965b8e0634c18af32131e744eff005658f47"
     },
     "urls": {
       "version": "0.0.4",
@@ -19582,7 +19518,6 @@
           "requires": {
             "anymatch": "1.3.2",
             "async-each": "1.0.1",
-            "fsevents": "1.1.3",
             "glob-parent": "2.0.0",
             "inherits": "2.0.3",
             "is-binary-path": "1.0.1",
@@ -19855,11 +19790,6 @@
           "version": "1.1.3",
           "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.1.3.tgz",
           "integrity": "sha512-WIr7iDkdmdbxu/Gh6eKEZJL6KPE74/5MEsf2whTOFNxbIoIixogroLdKYqB6FDav4Wavh/lZdzzd3b2KxIXC5Q==",
-          "optional": true,
-          "requires": {
-            "nan": "2.8.0",
-            "node-pre-gyp": "0.6.39"
-          },
           "dependencies": {
             "abbrev": {
               "version": "1.1.0",
@@ -20526,6 +20456,13 @@
                 }
               }
             },
+            "string_decoder": {
+              "version": "1.0.1",
+              "bundled": true,
+              "requires": {
+                "safe-buffer": "5.0.1"
+              }
+            },
             "string-width": {
               "version": "1.0.2",
               "bundled": true,
@@ -20535,13 +20472,6 @@
                 "strip-ansi": "3.0.1"
               }
             },
-            "string_decoder": {
-              "version": "1.0.1",
-              "bundled": true,
-              "requires": {
-                "safe-buffer": "5.0.1"
-              }
-            },
             "stringstream": {
               "version": "0.0.5",
               "bundled": true,
@@ -20996,8 +20926,7 @@
         "nan": {
           "version": "2.8.0",
           "resolved": "https://registry.npmjs.org/nan/-/nan-2.8.0.tgz",
-          "integrity": "sha1-7XFfP+neArV6XmJS2QqWZ14fCFo=",
-          "optional": true
+          "integrity": "sha1-7XFfP+neArV6XmJS2QqWZ14fCFo="
         },
         "node-libs-browser": {
           "version": "2.1.0",
@@ -21438,6 +21367,14 @@
             "xtend": "4.0.1"
           }
         },
+        "string_decoder": {
+          "version": "1.0.3",
+          "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz",
+          "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==",
+          "requires": {
+            "safe-buffer": "5.1.1"
+          }
+        },
         "string-width": {
           "version": "1.0.2",
           "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
@@ -21448,14 +21385,6 @@
             "strip-ansi": "3.0.1"
           }
         },
-        "string_decoder": {
-          "version": "1.0.3",
-          "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz",
-          "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==",
-          "requires": {
-            "safe-buffer": "5.1.1"
-          }
-        },
         "strip-ansi": {
           "version": "3.0.1",
           "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
@@ -21828,7 +21757,6 @@
           "requires": {
             "anymatch": "1.3.2",
             "async-each": "1.0.1",
-            "fsevents": "1.1.3",
             "glob-parent": "2.0.0",
             "inherits": "2.0.3",
             "is-binary-path": "1.0.1",
@@ -22016,11 +21944,6 @@
           "version": "1.1.3",
           "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.1.3.tgz",
           "integrity": "sha512-WIr7iDkdmdbxu/Gh6eKEZJL6KPE74/5MEsf2whTOFNxbIoIixogroLdKYqB6FDav4Wavh/lZdzzd3b2KxIXC5Q==",
-          "optional": true,
-          "requires": {
-            "nan": "2.8.0",
-            "node-pre-gyp": "0.6.39"
-          },
           "dependencies": {
             "abbrev": {
               "version": "1.1.0",
@@ -22687,6 +22610,13 @@
                 }
               }
             },
+            "string_decoder": {
+              "version": "1.0.1",
+              "bundled": true,
+              "requires": {
+                "safe-buffer": "5.0.1"
+              }
+            },
             "string-width": {
               "version": "1.0.2",
               "bundled": true,
@@ -22696,13 +22626,6 @@
                 "strip-ansi": "3.0.1"
               }
             },
-            "string_decoder": {
-              "version": "1.0.1",
-              "bundled": true,
-              "requires": {
-                "safe-buffer": "5.0.1"
-              }
-            },
             "stringstream": {
               "version": "0.0.5",
               "bundled": true,
@@ -23123,8 +23046,7 @@
         "nan": {
           "version": "2.8.0",
           "resolved": "https://registry.npmjs.org/nan/-/nan-2.8.0.tgz",
-          "integrity": "sha1-7XFfP+neArV6XmJS2QqWZ14fCFo=",
-          "optional": true
+          "integrity": "sha1-7XFfP+neArV6XmJS2QqWZ14fCFo="
         },
         "negotiator": {
           "version": "0.6.1",
@@ -23555,6 +23477,14 @@
           "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz",
           "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew=="
         },
+        "string_decoder": {
+          "version": "1.0.3",
+          "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz",
+          "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==",
+          "requires": {
+            "safe-buffer": "5.1.1"
+          }
+        },
         "string-width": {
           "version": "1.0.2",
           "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
@@ -23565,14 +23495,6 @@
             "strip-ansi": "3.0.1"
           }
         },
-        "string_decoder": {
-          "version": "1.0.3",
-          "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz",
-          "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==",
-          "requires": {
-            "safe-buffer": "5.1.1"
-          }
-        },
         "strip-ansi": {
           "version": "3.0.1",
           "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
diff --git a/package.json b/package.json
index 9e47662d2..8fedf5f43 100644
--- a/package.json
+++ b/package.json
@@ -29,7 +29,7 @@
     "mock-local-storage": "^1.0.4",
     "nightwatch": "~0.9.0",
     "redux-devtools": "^3.3.1",
-    "redux-mock-store": "^1.2.1",
+    "redux-mock-store": "^1.5.1",
     "sinon": "^4.4.2"
   },
   "dependencies": {
diff --git a/settings.json.default.json b/settings.json.default.json
index 9a5e8adc0..46a4760cc 100644
--- a/settings.json.default.json
+++ b/settings.json.default.json
@@ -11,6 +11,7 @@
   { "name": "replication" },
   { "name": "cors" },
   { "name": "permissions" },
+  { "name": "compaction" },
   { "name": "auth" },
   { "name": "verifyinstall" },
   { "name": "documentation" }


 

----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on GitHub and use the
URL above to go to the specific comment.
 
For queries about this service, please contact Infrastructure at:
users@infra.apache.org


With regards,
Apache Git Services