Customize style of the results list (#1103)

* Use dropdown to show options

* Customize toggle for menu dropdown

* Update tests

* Address PR comments
diff --git a/app/addons/components/assets/less/components.less b/app/addons/components/assets/less/components.less
index 9a0df82..480681e 100644
--- a/app/addons/components/assets/less/components.less
+++ b/app/addons/components/assets/less/components.less
@@ -25,3 +25,4 @@
 @import "layouts.less";
 @import "polling.less";
 @import "jsonlink.less";
+@import "menudropdown.less";
diff --git a/app/addons/components/assets/less/docs.less b/app/addons/components/assets/less/docs.less
index 1e12e98..44f60f2 100644
--- a/app/addons/components/assets/less/docs.less
+++ b/app/addons/components/assets/less/docs.less
@@ -30,6 +30,12 @@
         border-left: 1px solid @docHeaderOtherBorders;
         border-right: 1px solid @docHeaderOtherBorders;
         font-family: monospace;
+        .prettyprint--small {
+          font-size: 11px;
+        }
+        .prettyprint--large {
+          font-size: 15px;
+        }
       }
       header {
         font-weight: bold;
diff --git a/app/addons/components/assets/less/menudropdown.less b/app/addons/components/assets/less/menudropdown.less
new file mode 100644
index 0000000..2638a29
--- /dev/null
+++ b/app/addons/components/assets/less/menudropdown.less
@@ -0,0 +1,17 @@
+// 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.
+
+
+.dropdown-menu > li > a.fonticon-placeholder:before {
+  padding-right: 17px;
+  content: "";
+}
diff --git a/app/addons/components/components/document.js b/app/addons/components/components/document.js
index 1f4ba9e..4dd3312 100644
--- a/app/addons/components/components/document.js
+++ b/app/addons/components/components/document.js
@@ -9,24 +9,29 @@
 // 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 PropTypes from 'prop-types';
 
-import React from "react";
-import ReactDOM from "react-dom";
-import FauxtonAPI from "../../../core/api";
-import Helpers from "../../documents/helpers";
+import classnames from 'classnames';
+import PropTypes from 'prop-types';
+import React from 'react';
+import FauxtonAPI from '../../../core/api';
+import Constants from '../../documents/constants';
+import Helpers from '../../documents/helpers';
 
 export class Document extends React.Component {
   static propTypes = {
     docIdentifier: PropTypes.string.isRequired,
     docChecked: PropTypes.func.isRequired,
     truncate: PropTypes.bool,
-    maxRows: PropTypes.number
+    maxRows: PropTypes.number,
+    resultsStyle: PropTypes.object
   };
 
   static defaultProps = {
     truncate: true,
-    maxRows: 500
+    maxRows: 500,
+    resultsStyle: {
+      fontSize: Constants.INDEX_RESULTS_STYLE.FONT_SIZE_MEDIUM
+    }
   };
 
   onChange = (e) => {
@@ -91,10 +96,14 @@
       isTruncated = result.isTruncated;
       content = result.content;
     }
-
+    const { fontSize } = this.props.resultsStyle;
+    const classNames = classnames('prettyprint', {
+      'prettyprint--small': fontSize === Constants.INDEX_RESULTS_STYLE.FONT_SIZE_SMALL,
+      'prettyprint--large': fontSize === Constants.INDEX_RESULTS_STYLE.FONT_SIZE_LARGE
+    });
     return (
       <div className="doc-data">
-        <pre className="prettyprint">{content}</pre>
+        <pre className={classNames}>{content}</pre>
         {isTruncated ? <div className="doc-content-truncated">(truncated)</div> : null}
       </div>
     );
diff --git a/app/addons/components/components/menudropdown.js b/app/addons/components/components/menudropdown.js
index a67e240..8df4c23 100644
--- a/app/addons/components/components/menudropdown.js
+++ b/app/addons/components/components/menudropdown.js
@@ -9,14 +9,25 @@
 // 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 classnames from 'classnames';
+import PropTypes from 'prop-types';
 import React from "react";
-import ReactDOM from "react-dom";
-import { Dropdown } from "react-bootstrap";
+import { Button, Dropdown } from "react-bootstrap";
 import RootCloseWrapper from 'react-overlays/lib/RootCloseWrapper';
 
 export class MenuDropDown extends React.Component {
   static defaultProps = {
-    icon: 'fonticon-plus-circled'
+    icon: 'fonticon-plus-circled',
+    hideArrow: false,
+    toggleType: 'link'
+  };
+
+  static propTypes = {
+    icon: PropTypes.string,
+    hideArrow: PropTypes.bool,
+    links: PropTypes.array.isRequired,
+    toggleType: PropTypes.string.isRequired
   };
 
   createSectionLinks = (links) => {
@@ -30,7 +41,7 @@
   createEntry = (link, key) => {
     return (
       <li key={key}>
-        <a className={link.icon ? 'icon ' + link.icon : ''}
+        <a className={classnames('icon', link.icon, { 'fonticon-placeholder': !link.icon})}
           data-bypass={link.external ? 'true' : ''}
           href={link.url}
           onClick={link.onClick}
@@ -65,11 +76,13 @@
 
   render() {
     const menuItems = this.createSection();
+    const arrowClass = this.props.hideArrow ? '' : 'arrow';
+    const CustomMenuToggle = this.props.toggleType === 'button' ? CustomMenuButtonToggle : CustomMenuLinkToggle;
     return (
       <Dropdown id="dropdown-menu">
         <CustomMenuToggle bsRole="toggle" icon={this.props.icon}>
         </CustomMenuToggle>
-        <CustomMenu bsRole="menu" className="arrow">
+        <CustomMenu bsRole="menu" className={arrowClass}>
           {menuItems}
         </CustomMenu>
       </Dropdown>
@@ -77,7 +90,31 @@
   }
 }
 
-class CustomMenuToggle extends React.Component {
+class CustomMenuButtonToggle extends React.Component {
+  constructor(props, context) {
+    super(props, context);
+    this.handleClick = this.handleClick.bind(this);
+  }
+
+  handleClick(e) {
+    e.preventDefault();
+    this.props.onClick(e);
+  }
+
+  render() {
+    return (
+      <Button
+        onClick={this.handleClick}>
+        <i className={"dropdown-toggle " + this.props.icon}
+          style={{ fontSize: '1rem', boxShadow: '0px 0px 0px' }}>
+        </i>
+        {this.props.children}
+      </Button>
+    );
+  }
+}
+
+class CustomMenuLinkToggle extends React.Component {
   constructor(props, context) {
     super(props, context);
     this.handleClick = this.handleClick.bind(this);
@@ -105,11 +142,10 @@
   }
 
   render() {
-    const { children, open, onClose } = this.props;
-
+    const { children, open, onClose, className } = this.props;
     return (
       <RootCloseWrapper disabled={!open} onRootClose={onClose}>
-        <ul className="dropdown-menu arrow" role="menu" aria-labelledby="dLabel">
+        <ul className={classnames('dropdown-menu', className)} role="menu" aria-labelledby="dLabel">
           {children}
         </ul>
       </RootCloseWrapper>
diff --git a/app/addons/documents/__tests__/results-options.test.js b/app/addons/documents/__tests__/results-options.test.js
new file mode 100644
index 0000000..62be559
--- /dev/null
+++ b/app/addons/documents/__tests__/results-options.test.js
@@ -0,0 +1,63 @@
+// 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 { mount } from 'enzyme';
+import React from 'react';
+import sinon from 'sinon';
+import ResultsOptions from '../components/results-options';
+import Constants from '../constants';
+
+describe('Results Options', () => {
+
+  const defaultProps = {
+    resultsStyle: {
+      textOverflow: Constants.INDEX_RESULTS_STYLE.TEXT_OVERFLOW_TRUNCATED,
+      fontSize: Constants.INDEX_RESULTS_STYLE.FONT_SIZE_MEDIUM
+    },
+    updateStyle: () => {}
+  };
+
+  it('calls updateStyle when one of the options is clicked', () => {
+    const mockUpdateStyle = sinon.spy();
+    const wrapper = mount(<ResultsOptions
+      {...defaultProps}
+      updateStyle={mockUpdateStyle}/>
+    );
+    wrapper.find('a.icon').at(0).simulate('click');
+    sinon.assert.called(mockUpdateStyle);
+  });
+
+  it('shows the two sections by default', () => {
+    const wrapper = mount(<ResultsOptions
+      {...defaultProps} />
+    );
+    expect(wrapper.find('li.header-label').length).toBe(2);
+  });
+
+  it('hides Display Density when prop is set to false', () => {
+    const wrapper = mount(<ResultsOptions
+      {...defaultProps}
+      showDensity={false} />
+    );
+    expect(wrapper.find('li.header-label').length).toBe(1);
+    expect(wrapper.find('li.header-label').text()).toBe('Font size');
+  });
+
+  it('hides Font Size when prop is set to false', () => {
+    const wrapper = mount(<ResultsOptions
+      {...defaultProps}
+      showFontSize={false} />
+    );
+    expect(wrapper.find('li.header-label').length).toBe(1);
+    expect(wrapper.find('li.header-label').text()).toBe('Display density');
+  });
+});
diff --git a/app/addons/documents/__tests__/results-toolbar.test.js b/app/addons/documents/__tests__/results-toolbar.test.js
index 09b4c9c..8c6b4d8 100644
--- a/app/addons/documents/__tests__/results-toolbar.test.js
+++ b/app/addons/documents/__tests__/results-toolbar.test.js
@@ -10,23 +10,29 @@
 // License for the specific language governing permissions and limitations under
 // the License.
 
-import sinon from "sinon";
-import utils from "../../../../test/mocha/testUtils";
-import FauxtonAPI from "../../../core/api";
-import {ResultsToolBar} from "../components/results-toolbar";
-import React from 'react';
-import ReactDOM from 'react-dom';
 import { mount } from 'enzyme';
+import React from 'react';
+import sinon from 'sinon';
+import utils from '../../../../test/mocha/testUtils';
+import FauxtonAPI from '../../../core/api';
+import {ResultsToolBar} from '../components/results-toolbar';
+import Constants from '../constants';
 
 describe('Results Toolbar', () => {
-  const restProps = {
+
+  const defaultProps = {
     removeItem: () => {},
     allDocumentsSelected: false,
     hasSelectedItem: false,
     toggleSelectAll: () => {},
     isLoading: false,
     queryOptionsParams: {},
-    databaseName: 'mydb'
+    databaseName: 'mydb',
+    resultsStyle: {
+      textOverflow: Constants.INDEX_RESULTS_STYLE.TEXT_OVERFLOW_TRUNCATED,
+      fontSize: Constants.INDEX_RESULTS_STYLE.FONT_SIZE_MEDIUM
+    },
+    updateResultsStyle: () => {}
   };
 
   beforeEach(() => {
@@ -38,21 +44,93 @@
   });
 
   it('renders all content when there are results and they are deletable', () => {
-    const wrapper = mount(<ResultsToolBar hasResults={true} isListDeletable={true} {...restProps}/>);
+    const wrapper = mount(<ResultsToolBar
+      {...defaultProps}
+      hasResults={true}
+      isListDeletable={true} />
+    );
     expect(wrapper.find('.bulk-action-component').length).toBe(1);
     expect(wrapper.find('div.two-sides-toggle-button').length).toBe(1);
     expect(wrapper.find('.document-result-screen__toolbar-create-btn').length).toBe(1);
   });
 
   it('does not render bulk action component when list is not deletable', () => {
-    const wrapper = mount(<ResultsToolBar hasResults={true} isListDeletable={false} {...restProps}/>);
+    const wrapper = mount(<ResultsToolBar
+      {...defaultProps}
+      hasResults={true}
+      isListDeletable={false} />
+    );
     expect(wrapper.find('.bulk-action-component').length).toBe(0);
     expect(wrapper.find('div.two-sides-toggle-button').length).toBe(1);
     expect(wrapper.find('.document-result-screen__toolbar-create-btn').length).toBe(1);
   });
 
   it('includes default partition key when one is selected', () => {
-    const wrapper = mount(<ResultsToolBar hasResults={true} isListDeletable={false} {...restProps} partitionKey={'partKey1'}/>);
-    expect(wrapper.find('a').prop('href')).toMatch(/\?partitionKey=partKey1$/);
+    const wrapper = mount(<ResultsToolBar
+      {...defaultProps}
+      hasResults={true}
+      isListDeletable={false}
+      partitionKey={'partKey1'} />);
+    expect(wrapper.find('a.document-result-screen__toolbar-create-btn').prop('href')).toMatch(/\?partitionKey=partKey1$/);
+  });
+
+  it('toggles display density', () => {
+    const mockUpdateStyle = sinon.spy();
+    const wrapper = mount(<ResultsToolBar
+      {...defaultProps}
+      hasResults={true}
+      isListDeletable={false}
+      updateResultsStyle={mockUpdateStyle}
+      selectedLayout={Constants.LAYOUT_ORIENTATION.METADATA}/>
+    );
+    wrapper.find('a.icon').first().simulate('click');
+    sinon.assert.calledWith(mockUpdateStyle, { textOverflow: Constants.INDEX_RESULTS_STYLE.TEXT_OVERFLOW_FULL});
+  });
+
+  it('switches font size', () => {
+    const mockUpdateStyle = sinon.spy();
+    const wrapper = mount(<ResultsToolBar
+      {...defaultProps}
+      hasResults={true}
+      isListDeletable={false}
+      updateResultsStyle={mockUpdateStyle}
+      selectedLayout={Constants.LAYOUT_ORIENTATION.METADATA}/>
+    );
+    wrapper.find('a.icon').at(1).simulate('click');
+    sinon.assert.calledWith(mockUpdateStyle, { fontSize: Constants.INDEX_RESULTS_STYLE.FONT_SIZE_SMALL});
+
+    wrapper.find('a.icon').at(2).simulate('click');
+    sinon.assert.calledWith(mockUpdateStyle, { fontSize: Constants.INDEX_RESULTS_STYLE.FONT_SIZE_MEDIUM});
+
+    wrapper.find('a.icon').at(3).simulate('click');
+    sinon.assert.calledWith(mockUpdateStyle, { fontSize: Constants.INDEX_RESULTS_STYLE.FONT_SIZE_LARGE});
+  });
+
+  it.only('does not show Display Density option in JSON layout', () => {
+    const toolbarJson = mount(<ResultsToolBar
+      {...defaultProps}
+      hasResults={true}
+      isListDeletable={false}
+      selectedLayout={Constants.LAYOUT_ORIENTATION.JSON}/>
+    );
+    expect(toolbarJson.find('li.header-label').text()).toBe('Font size');
+
+    const toolbarMetadata = mount(<ResultsToolBar
+      {...defaultProps}
+      hasResults={true}
+      isListDeletable={false}
+      selectedLayout={Constants.LAYOUT_ORIENTATION.METADATA}/>
+    );
+    expect(toolbarMetadata.find('li.header-label').at(0).text()).toBe('Display density');
+    expect(toolbarMetadata.find('li.header-label').at(1).text()).toBe('Font size');
+
+    const toolbarTable = mount(<ResultsToolBar
+      {...defaultProps}
+      hasResults={true}
+      isListDeletable={false}
+      selectedLayout={Constants.LAYOUT_ORIENTATION.TABLE}/>
+    );
+    expect(toolbarTable.find('li.header-label').at(0).text()).toBe('Display density');
+    expect(toolbarTable.find('li.header-label').at(1).text()).toBe('Font size');
   });
 });
diff --git a/app/addons/documents/__tests__/table-row.test.js b/app/addons/documents/__tests__/table-row.test.js
index 5f727f3..266f985 100644
--- a/app/addons/documents/__tests__/table-row.test.js
+++ b/app/addons/documents/__tests__/table-row.test.js
@@ -9,35 +9,37 @@
 // 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 TableRow from "../index-results/components/results/TableRow";
-import utils from "../../../../test/mocha/testUtils";
-import React from "react";
-import ReactDOM from "react-dom";
-import sinon from "sinon";
+
+import React from 'react';
+import sinon from 'sinon';
 import { shallow } from 'enzyme';
+import FauxtonAPI from '../../../core/api';
+import Constants from '../constants';
+import TableRow from '../index-results/components/results/TableRow';
+import utils from '../../../../test/mocha/testUtils';
 
 FauxtonAPI.router = new FauxtonAPI.Router([]);
 const { assert } = utils;
 
 describe('Docs Table Row', () => {
 
-  it('all types of value are converted to the appropriate text for display', () => {
-    const elem = {
-      content: {
-        _id: "123",
-        vBool: true,
-        vString: 'abc',
-        vFloat: 123.1234,
-        vInt: 123,
-        vObject: { f1: 1, f2: 'b'},
-      },
-      id: "123"
-    };
+  const elem = {
+    content: {
+      _id: "123",
+      vBool: true,
+      vString: 'abc',
+      vFloat: 123.1234,
+      vInt: 123,
+      vObject: { f1: 1, f2: 'b'},
+    },
+    id: "123"
+  };
 
-    const data = {
-      selectedFields: ['vBool', 'vString', 'vFloat', 'vInt', 'vObject']
-    };
+  const data = {
+    selectedFields: ['vBool', 'vString', 'vFloat', 'vInt', 'vObject']
+  };
+
+  it('all types of value are converted to the appropriate text for display', () => {
     const wrapper = shallow(<TableRow
       onClick={sinon.stub()}
       docChecked={sinon.stub()}
@@ -54,4 +56,34 @@
     assert.equal(wrapper.find('td').at(5).text(), JSON.stringify(elem.content.vInt));
     assert.equal(wrapper.find('td').at(6).text().replace(/[\s]/g, ''), JSON.stringify(elem.content.vObject));
   });
+
+  it('shows full text values', () => {
+    const wrapper = shallow(<TableRow
+      onClick={sinon.stub()}
+      docChecked={sinon.stub()}
+      el={elem}
+      data={data}
+      index={0}
+      docIdentifier={elem.id}
+      isSelected={false}
+      textOverflow={Constants.INDEX_RESULTS_STYLE.TEXT_OVERFLOW_FULL}
+    />);
+
+    expect(wrapper.find('td.showall').exists()).toBe(true);
+  });
+
+  it('truncates text values', () => {
+    const wrapper = shallow(<TableRow
+      onClick={sinon.stub()}
+      docChecked={sinon.stub()}
+      el={elem}
+      data={data}
+      index={0}
+      docIdentifier={elem.id}
+      isSelected={false}
+      textOverflow={Constants.INDEX_RESULTS_STYLE.TEXT_OVERFLOW_TRUNCATED}
+    />);
+
+    expect(wrapper.find('td.showall').exists()).toBe(false);
+  });
 });
diff --git a/app/addons/documents/assets/less/index-results.less b/app/addons/documents/assets/less/index-results.less
index ead6b89..8d05cfc 100644
--- a/app/addons/documents/assets/less/index-results.less
+++ b/app/addons/documents/assets/less/index-results.less
@@ -34,6 +34,36 @@
     min-height: 26px;
     padding: 8px 0;
   }
+
+  .text-overflow-switch {
+    min-width: 145px;
+    min-height: 26px;
+    padding: 8px 0;
+  }
+
+  .toolbar-dropdown {    
+    
+    .btn {
+      color: #666;
+    }
+    
+    .dropdown-menu.arrow:before {
+      right: 80%;
+    }
+
+    a.dropdown-toggle {
+      padding-left: 16px;
+    }
+
+    .dropdown-menu li a {
+      padding: 10px 15px 10px 12px;
+      &:hover {
+        background-color: @hoverHighlight;
+        color: white;
+      }
+    }
+  }
+  
 }
 
 .document-result-screen__toolbar-flex-container {
@@ -117,14 +147,27 @@
   td, th, td a {
     vertical-align: middle;
     line-height: 20px;
-    font-size: 14px;
+    font-size: 0.875rem;
   }
+
   td, th {
     color: @defaultHTag;
     max-width: 160px;
     overflow: hidden;
     text-overflow: ellipsis;
     white-space: nowrap;
+    vertical-align: top;
+    &.showall {
+      overflow: visible;
+      white-space: normal;
+      word-wrap: break-word;
+    }
+    &.small-font {
+      font-size: 0.75rem;
+    }
+    &.large-font {
+      font-size: 1rem;
+    }
   }
   td.tableview-checkbox-cell, th.tableview-header-el-checkbox {
     width: 35px;
@@ -220,4 +263,4 @@
   .fonticon-attention-circled {
     margin-right: 4px;
   }
-}
\ No newline at end of file
+}
diff --git a/app/addons/documents/components/results-options.js b/app/addons/documents/components/results-options.js
new file mode 100644
index 0000000..c63f0ea
--- /dev/null
+++ b/app/addons/documents/components/results-options.js
@@ -0,0 +1,99 @@
+
+import React from 'react';
+import PropTypes from 'prop-types';
+import Components from '../../components/react-components';
+import Constants from '../constants';
+
+const { MenuDropDown } = Components;
+
+export default class ResultsOptions extends React.Component {
+  static defaultProps = {
+    showDensity: true,
+    showFontSize: true
+  };
+  static propTypes = {
+    resultsStyle: PropTypes.object.isRequired,
+    updateStyle: PropTypes.func.isRequired,
+    showDensity: PropTypes.bool,
+    showFontSize: PropTypes.bool
+  };
+
+  constructor(props) {
+    super(props);
+    this.toggleTextOverflow = this.toggleTextOverflow.bind(this);
+    this.setFontSize = this.setFontSize.bind(this);
+  }
+
+  toggleTextOverflow() {
+    if (this.props.resultsStyle.textOverflow === Constants.INDEX_RESULTS_STYLE.TEXT_OVERFLOW_FULL) {
+      this.props.updateStyle({
+        textOverflow: Constants.INDEX_RESULTS_STYLE.TEXT_OVERFLOW_TRUNCATED
+      });
+    } else {
+      this.props.updateStyle({
+        textOverflow: Constants.INDEX_RESULTS_STYLE.TEXT_OVERFLOW_FULL
+      });
+    }
+  }
+
+  setFontSize(size) {
+    this.props.updateStyle({
+      fontSize: size
+    });
+  }
+
+  getDensitySection() {
+    let menuOptionTitle = 'Show full values';
+    if (this.props.resultsStyle.textOverflow === Constants.INDEX_RESULTS_STYLE.TEXT_OVERFLOW_FULL) {
+      menuOptionTitle = 'Truncate values';
+    }
+
+    const densityItems = [{
+      title: menuOptionTitle,
+      onClick: this.toggleTextOverflow
+    }];
+
+    return {
+      title: 'Display density',
+      links: densityItems
+    };
+  }
+
+  getFontSizeSection() {
+    const fontSizeItems = [{
+      title: 'Small',
+      onClick: () => { this.setFontSize(Constants.INDEX_RESULTS_STYLE.FONT_SIZE_SMALL); },
+      icon: this.props.resultsStyle.fontSize === Constants.INDEX_RESULTS_STYLE.FONT_SIZE_SMALL ? 'fonticon-ok' : ''
+    },
+    {
+      title: 'Medium',
+      onClick: () => { this.setFontSize(Constants.INDEX_RESULTS_STYLE.FONT_SIZE_MEDIUM); },
+      icon: this.props.resultsStyle.fontSize === Constants.INDEX_RESULTS_STYLE.FONT_SIZE_MEDIUM ? 'fonticon-ok' : ''
+    },
+    {
+      title: 'Large',
+      onClick: () => { this.setFontSize(Constants.INDEX_RESULTS_STYLE.FONT_SIZE_LARGE); },
+      icon: this.props.resultsStyle.fontSize === Constants.INDEX_RESULTS_STYLE.FONT_SIZE_LARGE ? 'fonticon-ok' : ''
+    }];
+    return {
+      title: 'Font size',
+      links: fontSizeItems
+    };
+  }
+
+  render() {
+    const links = [];
+    if (this.props.showDensity) {
+      links.push(this.getDensitySection());
+    }
+    if (this.props.showFontSize) {
+      links.push(this.getFontSizeSection());
+    }
+
+    return (
+      <div className='toolbar-dropdown'>
+        <MenuDropDown id="result-style-menu" links={links} icon='fonticon-mixer' hideArrow={true} toggleType='button'/>
+      </div>
+    );
+  }
+}
diff --git a/app/addons/documents/components/results-toolbar.js b/app/addons/documents/components/results-toolbar.js
index c7dbccc..8c765c2 100644
--- a/app/addons/documents/components/results-toolbar.js
+++ b/app/addons/documents/components/results-toolbar.js
@@ -13,12 +13,15 @@
 
 import React from 'react';
 import BulkDocumentHeaderController from "../header/header";
+import ResultsOptions from './results-options';
 import Components from "../../components/react-components";
+import Constants from '../constants';
 import Helpers from '../helpers';
 
 const {BulkActionComponent} = Components;
 
 export class ResultsToolBar extends React.Component {
+
   shouldComponentUpdate (nextProps) {
     return nextProps.isListDeletable != undefined;
   }
@@ -48,10 +51,17 @@
         title="Select all docs that can be..." />;
     }
 
-    // Determine if we need to display the bulk doc header
+    // Determine if we need to display the bulk doc header and result options
     let bulkHeader = null;
+    const showDensityOptions = this.props.selectedLayout !== Constants.LAYOUT_ORIENTATION.JSON;
+    let resultOptions = null;
     if (hasResults || isLoading) {
       bulkHeader = <BulkDocumentHeaderController {...this.props} />;
+      resultOptions = <ResultsOptions
+        updateStyle={this.props.updateResultsStyle}
+        resultsStyle={this.props.resultsStyle}
+        showDensity={showDensityOptions}
+      />;
     }
 
     let createDocumentLink = null;
@@ -69,6 +79,7 @@
       <div className="document-result-screen__toolbar">
         {bulkAction}
         {bulkHeader}
+        {resultOptions}
         {createDocumentLink}
       </div>
     );
@@ -83,5 +94,7 @@
   isLoading: PropTypes.bool.isRequired,
   hasResults: PropTypes.bool.isRequired,
   isListDeletable: PropTypes.bool,
-  partitionKey: PropTypes.string
+  partitionKey: PropTypes.string,
+  resultsStyle: PropTypes.object.isRequired,
+  updateResultsStyle: PropTypes.func.isRequired
 };
diff --git a/app/addons/documents/constants.js b/app/addons/documents/constants.js
index 3d9f33b..49a3f96 100644
--- a/app/addons/documents/constants.js
+++ b/app/addons/documents/constants.js
@@ -21,5 +21,12 @@
     MANGO_INDEX: 'MangoIndex',
     MANGO_QUERY: 'MangoQueryResult',
     VIEW: 'view'
+  },
+  INDEX_RESULTS_STYLE: {
+    TEXT_OVERFLOW_TRUNCATED: 'truncated',
+    TEXT_OVERFLOW_FULL: 'full',
+    FONT_SIZE_SMALL: 'small-font',
+    FONT_SIZE_LARGE: 'large-font',
+    FONT_SIZE_MEDIUM: 'medium-font'
   }
 };
diff --git a/app/addons/documents/index-results/actions/base.js b/app/addons/documents/index-results/actions/base.js
index 6fac0ff..cd9cc88 100644
--- a/app/addons/documents/index-results/actions/base.js
+++ b/app/addons/documents/index-results/actions/base.js
@@ -105,3 +105,9 @@
   };
 };
 
+export const updateResultsStyle = (newStyle) => {
+  return {
+    type: ActionTypes.INDEX_RESULTS_SET_STYLE,
+    resultsStyle: newStyle
+  };
+};
diff --git a/app/addons/documents/index-results/actiontypes.js b/app/addons/documents/index-results/actiontypes.js
index ddd17e6..d69c15d 100644
--- a/app/addons/documents/index-results/actiontypes.js
+++ b/app/addons/documents/index-results/actiontypes.js
@@ -29,5 +29,6 @@
   INDEX_RESULTS_REDUX_NEW_SELECTED_DOCS: 'INDEX_RESULTS_REDUX_NEW_SELECTED_DOCS',
   INDEX_RESULTS_REDUX_CHANGE_TABLE_HEADER_ATTRIBUTE: 'INDEX_RESULTS_REDUX_CHANGE_TABLE_HEADER_ATTRIBUTE',
   INDEX_RESULTS_REDUX_RESET_STATE: 'INDEX_RESULTS_REDUX_RESET_STATE',
-  INDEX_RESULTS_REDUX_NEW_QUERY_OPTIONS: 'INDEX_RESULTS_REDUX_NEW_QUERY_OPTIONS'
+  INDEX_RESULTS_REDUX_NEW_QUERY_OPTIONS: 'INDEX_RESULTS_REDUX_NEW_QUERY_OPTIONS',
+  INDEX_RESULTS_SET_STYLE: 'INDEX_RESULTS_SET_STYLE'
 };
diff --git a/app/addons/documents/index-results/components/results/ResultsScreen.js b/app/addons/documents/index-results/components/results/ResultsScreen.js
index ac209dc..86a7bb4 100644
--- a/app/addons/documents/index-results/components/results/ResultsScreen.js
+++ b/app/addons/documents/index-results/components/results/ResultsScreen.js
@@ -65,7 +65,8 @@
           header={doc.header}
           docChecked={this.props.docChecked}
           isDeletable={doc.isDeletable}
-          docIdentifier={doc.id} >
+          docIdentifier={doc.id}
+          resultsStyle={this.props.resultsStyle} >
           {doc.url ? this.getUrlFragment('#' + doc.url) : doc.url}
         </Document>
       );
@@ -104,6 +105,7 @@
           hasSelectedItem={this.props.hasSelectedItem}
           toggleSelect={this.toggleSelectAll}
           changeField={this.props.changeTableHeaderAttribute}
+          resultsStyle={this.props.resultsStyle}
           title="Select all docs that can be..." />
       </div>
     );
diff --git a/app/addons/documents/index-results/components/results/TableRow.js b/app/addons/documents/index-results/components/results/TableRow.js
index 5fe2daf..32614fe 100644
--- a/app/addons/documents/index-results/components/results/TableRow.js
+++ b/app/addons/documents/index-results/components/results/TableRow.js
@@ -10,16 +10,21 @@
 // License for the specific language governing permissions and limitations under
 // the License.
 
+import classnames from 'classnames';
 import PropTypes from 'prop-types';
-
 import React from 'react';
 import FauxtonAPI from '../../../../../core/api';
 import Components from '../../../../components/react-components';
+import Constants from '../../../constants';
 import uuid from 'uuid';
 
 const { Copy } = Components;
 
 export default class TableRow extends React.Component {
+  static defaultProps = {
+    textOverflow: Constants.INDEX_RESULTS_STYLE.TEXT_OVERFLOW_TRUNCATED
+  };
+
   constructor (props) {
     super(props);
     this.state = {
@@ -40,8 +45,14 @@
       const stringified = ['object', 'boolean'].includes(typeof el[k]) ?
         JSON.stringify(el[k], null, '  ') : el[k];
 
+      const classNames = classnames({
+        'showall': this.props.textOverflow === Constants.INDEX_RESULTS_STYLE.TEXT_OVERFLOW_FULL,
+        'small-font': this.props.fontSize === Constants.INDEX_RESULTS_STYLE.FONT_SIZE_SMALL,
+        'large-font': this.props.fontSize === Constants.INDEX_RESULTS_STYLE.FONT_SIZE_LARGE
+      });
+
       return (
-        <td key={key} title={stringified} onClick={this.onClick.bind(this)}>
+        <td key={key} title={stringified} className={classNames} onClick={this.onClick.bind(this)}>
           {stringified}
         </td>
       );
@@ -147,5 +158,6 @@
   isSelected: PropTypes.bool.isRequired,
   index: PropTypes.number.isRequired,
   data: PropTypes.object.isRequired,
-  onClick: PropTypes.func.isRequired
+  onClick: PropTypes.func.isRequired,
+  textOverflow: PropTypes.string
 };
diff --git a/app/addons/documents/index-results/components/results/TableView.js b/app/addons/documents/index-results/components/results/TableView.js
index 550a088..3197839 100644
--- a/app/addons/documents/index-results/components/results/TableView.js
+++ b/app/addons/documents/index-results/components/results/TableView.js
@@ -21,7 +21,7 @@
 
   getContentRows () {
     const data = this.props.data.results;
-
+    const { textOverflow, fontSize } = this.props.resultsStyle;
     return data.map(function (el, i) {
       return (
         <TableRow
@@ -32,7 +32,9 @@
           docIdentifier={el.id || "tableview-row-component-" + i}
           docChecked={this.props.docChecked}
           isSelected={this.props.isSelected(el.id)}
-          data={this.props.data} />
+          data={this.props.data}
+          textOverflow={textOverflow}
+          fontSize={fontSize} />
       );
     }.bind(this));
   }
diff --git a/app/addons/documents/index-results/containers/IndexResultsContainer.js b/app/addons/documents/index-results/containers/IndexResultsContainer.js
index ca940ec..7914265 100644
--- a/app/addons/documents/index-results/containers/IndexResultsContainer.js
+++ b/app/addons/documents/index-results/containers/IndexResultsContainer.js
@@ -20,7 +20,8 @@
   changeLayout,
   bulkCheckOrUncheck,
   changeTableHeaderAttribute,
-  resetState
+  resetState,
+  updateResultsStyle
 } from '../actions/base';
 import {
   getDocs,
@@ -36,7 +37,8 @@
   getTextEmptyIndex,
   getDocType,
   getFetchParams,
-  getQueryOptionsParams
+  getQueryOptionsParams,
+  getResultsStyle
 } from '../reducers';
 
 
@@ -56,7 +58,8 @@
     docType: getDocType(indexResults),
     fetchParams: getFetchParams(indexResults),
     queryOptionsParams: getQueryOptionsParams(indexResults),
-    partitionKey: ownProps.partitionKey
+    partitionKey: ownProps.partitionKey,
+    resultsStyle: getResultsStyle(indexResults)
   };
 };
 
@@ -91,6 +94,9 @@
     },
     queryOptionsToggleIncludeDocs: (previousIncludeDocs) => {
       dispatch(queryOptionsToggleIncludeDocs(previousIncludeDocs));
+    },
+    updateResultsStyle: (newStyle) => {
+      dispatch(updateResultsStyle(newStyle));
     }
   };
 };
diff --git a/app/addons/documents/index-results/reducers.js b/app/addons/documents/index-results/reducers.js
index 2f90d3a..5d040c3 100644
--- a/app/addons/documents/index-results/reducers.js
+++ b/app/addons/documents/index-results/reducers.js
@@ -29,6 +29,7 @@
   selectedLayout: Constants.LAYOUT_ORIENTATION.METADATA,
   textEmptyIndex: 'No Documents Found',
   docType: Constants.INDEX_RESULTS_DOC_TYPE.VIEW,
+  resultsStyle: loadStyle(),
   fetchParams: {
     limit: getDefaultPerPage() + 1,
     skip: 0
@@ -61,11 +62,38 @@
   }
 };
 
+function loadStyle() {
+  let style = app.utils.localStorageGet('fauxton:results_style');
+  if (!style) {
+    style = {
+      textOverflow: Constants.INDEX_RESULTS_STYLE.TEXT_OVERFLOW_TRUNCATED,
+      fontSize: Constants.INDEX_RESULTS_STYLE.FONT_SIZE_MEDIUM
+    };
+  }
+  return style;
+}
+
+function storeStyle(style) {
+  app.utils.localStorageSet('fauxton:results_style', style);
+}
+
 export default function resultsState(state = initialState, action) {
   switch (action.type) {
 
+    case ActionTypes.INDEX_RESULTS_SET_STYLE:
+      const newStyle = {
+        ...state.resultsStyle,
+        ...action.resultsStyle
+      };
+      storeStyle(newStyle);
+      return {
+        ...state,
+        resultsStyle: newStyle
+      };
+
     case ActionTypes.INDEX_RESULTS_REDUX_RESET_STATE:
-      return Object.assign({}, initialState, {
+      return {
+        ...initialState,
         selectedLayout: state.selectedLayout,
         selectedDocs: [],
         fetchParams: {
@@ -77,18 +105,21 @@
         }),
         queryOptionsPanel: Object.assign({}, initialState.queryOptionsPanel,
           state.queryOptionsPanel, {reduce: false, groupLevel: 'exact', showReduce: false}),
-        isLoading: false
-      });
+        isLoading: false,
+        resultsStyle: state.resultsStyle
+      };
 
     case ActionTypes.INDEX_RESULTS_REDUX_IS_LOADING:
-      return Object.assign({}, state, {
+      return {
+        ...state,
         isLoading: true
-      });
+      };
 
     case ActionTypes.INDEX_RESULTS_REDUX_NEW_SELECTED_DOCS:
-      return Object.assign({}, state, {
+      return {
+        ...state,
         selectedDocs: action.selectedDocs
-      });
+      };
 
     case ActionTypes.INDEX_RESULTS_REDUX_NEW_RESULTS:
       let selectedLayout = state.selectedLayout;
@@ -98,7 +129,8 @@
           selectedLayout = Constants.LAYOUT_ORIENTATION.TABLE;
         }
       }
-      return Object.assign({}, state, {
+      return {
+        ...state,
         docs: action.docs,
         isLoading: false,
         isEditable: true, //TODO: determine logic for this
@@ -110,51 +142,57 @@
         selectedLayout: selectedLayout,
         executionStats: action.executionStats,
         warning: action.warning
-      });
+      };
 
     case ActionTypes.INDEX_RESULTS_REDUX_CHANGE_LAYOUT:
-      return Object.assign({}, state, {
+      return {
+        ...state,
         selectedLayout: action.layout
-      });
+      };
 
     case ActionTypes.INDEX_RESULTS_REDUX_TOGGLE_SHOW_ALL_COLUMNS:
-      return Object.assign({}, state, {
+      return {
+        ...state,
         tableView: Object.assign({}, state.tableView, {
           showAllFieldsTableView: !state.tableView.showAllFieldsTableView,
           cachedFieldsTableView: state.tableView.selectedFieldsTableView
         })
-      });
+      };
 
     case ActionTypes.INDEX_RESULTS_REDUX_CHANGE_TABLE_HEADER_ATTRIBUTE:
-      return Object.assign({}, state, {
+      return {
+        ...state,
         tableView: Object.assign({}, state.tableView, {
           selectedFieldsTableView: action.selectedFieldsTableView
         })
-      });
+      };
 
     case ActionTypes.INDEX_RESULTS_REDUX_SET_PER_PAGE:
       app.utils.localStorageSet('fauxton:perpageredux', action.perPage);
-      return Object.assign({}, state, {
+      return {
+        ...state,
         pagination: Object.assign({}, initialState.pagination, {
           perPage: action.perPage
         })
-      });
+      };
 
     case ActionTypes.INDEX_RESULTS_REDUX_PAGINATE_NEXT:
-      return Object.assign({}, state, {
+      return {
+        ...state,
         pagination: Object.assign({}, state.pagination, {
           pageStart: state.pagination.pageStart + state.pagination.perPage,
           currentPage: state.pagination.currentPage + 1
         })
-      });
+      };
 
     case ActionTypes.INDEX_RESULTS_REDUX_PAGINATE_PREVIOUS:
-      return Object.assign({}, state, {
+      return {
+        ...state,
         pagination: Object.assign({}, state.pagination, {
           pageStart: state.pagination.pageStart - state.pagination.perPage,
           currentPage: state.pagination.currentPage - 1
         })
-      });
+      };
 
     case ActionTypes.INDEX_RESULTS_REDUX_NEW_QUERY_OPTIONS:
       // includeDocs or reduce should be mutually exclusive
@@ -168,9 +206,10 @@
         // Switch off includeDocs when reduce is being set to true
         action.options.includeDocs = false;
       }
-      return Object.assign({}, state, {
+      return {
+        ...state,
         queryOptionsPanel: Object.assign({}, state.queryOptionsPanel, action.options)
-      });
+      };
 
     default:
       return state;
@@ -349,3 +388,4 @@
 export const getQueryOptionsPanel = state => state.queryOptionsPanel;
 export const getPerPage = state => state.pagination.perPage;
 export const getFetchParams = state => state.fetchParams;
+export const getResultsStyle = state => state.resultsStyle;