This is an automated email from the ASF dual-hosted git repository.

amaranhao pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/couchdb-fauxton.git


The following commit(s) were added to refs/heads/main by this push:
     new 613a056f Mango query UX improvements (#1402)
613a056f is described below

commit 613a056fa3214dfa9ef12d445200499420ab101e
Author: Antonio Maranhao <[email protected]>
AuthorDate: Mon Jul 17 15:32:21 2023 -0400

    Mango query UX improvements (#1402)
    
    - Set editor max height so buttons and stats are not hidden when the
    query code is longer than the page height.
    - Add cheatsheet modal that shows query operators.
    - Display exec stats and warning directly below the editor instead of
    in a popover to give easier access to that information, particularly
    when trying to optimize queries.
---
 .../components/__tests__/codeEditorPanel.test.js   |  23 +++-
 app/addons/components/assets/scss/code-editor.scss |  10 ++
 .../components/components/codeeditorpanel.js       |  27 +++-
 app/addons/documents/__tests__/json-view.test.js   |   4 -
 app/addons/documents/assets/scss/documents.scss    |   1 +
 .../documents/assets/scss/index-results.scss       |  28 -----
 app/addons/documents/assets/scss/mango-query.scss  |  63 ++++++++++
 app/addons/documents/assets/scss/view-editor.scss  |   7 ++
 .../documents/index-results/actions/fetch.js       |   7 +-
 app/addons/documents/index-results/reducers.js     |   6 +-
 .../mango/__tests__/mango.components.test.js       |  84 ++++++++++++-
 .../documents/mango/components/ExecutionStats.js   | 127 -------------------
 .../mango/components/ExecutionStatsPanel.js        | 107 ++++++++++++++++
 .../documents/mango/components/MangoIndexEditor.js |   4 +-
 .../mango/components/MangoQueryCheatsheetModal.js  | 138 +++++++++++++++++++++
 .../documents/mango/components/MangoQueryEditor.js |  30 ++++-
 .../mango/components/MangoQueryEditorContainer.js  |   7 +-
 app/addons/documents/mango/mango.actions.js        |  15 +++
 app/addons/documents/mango/mango.actiontypes.js    |   1 +
 app/addons/documents/mango/mango.api.js            |   2 +-
 app/addons/documents/mango/mango.helper.js         |   9 +-
 app/addons/documents/mango/mango.reducers.js       |   9 +-
 22 files changed, 525 insertions(+), 184 deletions(-)

diff --git a/app/addons/components/__tests__/codeEditorPanel.test.js 
b/app/addons/components/__tests__/codeEditorPanel.test.js
index 53ca0824..5df08564 100644
--- a/app/addons/components/__tests__/codeEditorPanel.test.js
+++ b/app/addons/components/__tests__/codeEditorPanel.test.js
@@ -12,6 +12,7 @@
 import ReactComponents from "../react-components";
 import React from "react";
 import {mount} from 'enzyme';
+import sinon from "sinon";
 
 var codeNoNewlines = 'function (doc) {emit(doc._id, 1);}';
 var code = 'function (doc) {\n  emit(doc._id, 1);\n}';
@@ -26,7 +27,7 @@ describe('CodeEditorPanel', () => {
       );
       expect(codeEditorEl.find('.fonticon-help-circled').length).toBe(0);
     });
-    it('hidden by default', () => {
+    it('is displayed when docLink is provided', () => {
 
       const codeEditorEl = mount(
         <ReactComponents.CodeEditorPanel defaultCode={code} 
docLink="http://link.com"; />
@@ -72,4 +73,24 @@ describe('CodeEditorPanel', () => {
     });
   });
 
+  describe('Cheatsheet icon', () => {
+    it('hidden by default', () => {
+
+      const codeEditorEl = mount(
+        <ReactComponents.CodeEditorPanel defaultCode={code}/>
+      );
+      expect(codeEditorEl.find('.fonticon-help-circled').length).toBe(0);
+    });
+    it('is displayed and onClick handle is called', () => {
+      const onClickMock = sinon.mock();
+
+      const codeEditorEl = mount(
+        <ReactComponents.CodeEditorPanel defaultCode={code} showCheatSheetIcon 
onCheatsheatIconClick={onClickMock}/>
+      );
+      expect(codeEditorEl.find('.cheatsheet-icon').length).toBe(1);
+      codeEditorEl.find('.cheatsheet-icon').simulate('click');
+      sinon.assert.calledOnce(onClickMock);
+    });
+  });
+
 });
diff --git a/app/addons/components/assets/scss/code-editor.scss 
b/app/addons/components/assets/scss/code-editor.scss
index 98558928..2f82f2d7 100644
--- a/app/addons/components/assets/scss/code-editor.scss
+++ b/app/addons/components/assets/scss/code-editor.scss
@@ -19,6 +19,16 @@
   }
 }
 
+.cheatsheet-icon {
+  float: right;
+  padding-right: 0.5rem;
+  font-size: 13px;
+  margin-top: 3px;
+  &:hover {
+    color: $cf-brand-highlight;
+  }
+}
+
 .full-page-editor-modal-wrapper {
   position: fixed;
   padding: 110px;
diff --git a/app/addons/components/components/codeeditorpanel.js 
b/app/addons/components/components/codeeditorpanel.js
index eb8deac8..cc00f2ee 100644
--- a/app/addons/components/components/codeeditorpanel.js
+++ b/app/addons/components/components/codeeditorpanel.js
@@ -34,13 +34,17 @@ export class CodeEditorPanel extends React.Component {
     title: '',
     docLink: '',
     allowZenMode: true,
+    syntaxMode: 'javascript',
+    onCheatsheatIconClick: () => {},
+    showCheatSheetIcon: false,
+    setHeightToLineCount: true,
     blur () {}
   };
 
   getStoreState = () => {
     return {
       zenModeEnabled: false,
-      code: this.props.defaultCode
+      code: this.props.defaultCode,
     };
   };
 
@@ -56,6 +60,13 @@ export class CodeEditorPanel extends React.Component {
     }
   };
 
+  getCheatsheetIcon = () => {
+    if (!this.props.showCheatSheetIcon) {
+      return null;
+    }
+    return <span className="fonticon fonticon-bookmark cheatsheet-icon" 
title="Show cheatsheet" onClick={this.props.onCheatsheatIconClick}></span>;
+  };
+
   getDocIcon = () => {
     if (this.props.docLink) {
       return (
@@ -119,23 +130,31 @@ export class CodeEditorPanel extends React.Component {
     if (this.props.className) {
       classes = this.props.className;
     }
+    const heightSettings = {};
+    if (this.props.setHeightToLineCount) {
+      heightSettings.setHeightToLineCount = true;
+      heightSettings.maxLines = 1000;
+    } else {
+      heightSettings.setHeightToLineCount = false;
+      heightSettings.minLines = 30;
+    }
     return (
       <div className={classes}>
         <label>
           <span>{this.props.title}</span>
           {this.getDocIcon()}
           {this.getZenModeIcon()}
+          {this.getCheatsheetIcon()}
         </label>
         <CodeEditor
           id={this.props.id}
           ref={node => this.codeEditor = node}
-          mode="javascript"
+          mode={this.props.syntaxMode}
           defaultCode={this.state.code}
           showGutter={true}
           ignorableErrors={ignorableErrors}
-          setHeightToLineCount={true}
-          maxLines={10000}
           blur={this.props.blur}
+          {...heightSettings}
         />
         <Beautify code={this.state.code} beautifiedCode={this.beautify} />
         {this.getZenModeOverlay()}
diff --git a/app/addons/documents/__tests__/json-view.test.js 
b/app/addons/documents/__tests__/json-view.test.js
index 6384bb54..6c3b2c97 100644
--- a/app/addons/documents/__tests__/json-view.test.js
+++ b/app/addons/documents/__tests__/json-view.test.js
@@ -105,11 +105,7 @@ describe('Docs JSON View', () => {
     testDocs[1].type = 'special';
 
     const idx0 = { ...testDocs[0] };
-    delete idx0.ddoc;
-    delete idx0.name;
     const idx1 = { ...testDocs[1] };
-    delete idx1.ddoc;
-    delete idx1.name;
 
     expect(getJsonViewData(testDocs, {databaseName, docType})).toEqual({
       displayedFields: null,
diff --git a/app/addons/documents/assets/scss/documents.scss 
b/app/addons/documents/assets/scss/documents.scss
index a139ea0d..4dae3a6f 100644
--- a/app/addons/documents/assets/scss/documents.scss
+++ b/app/addons/documents/assets/scss/documents.scss
@@ -19,6 +19,7 @@
 @import "index-results";
 @import "doc-editor";
 @import "header";
+@import "mango-query";
 @import "revision-browser";
 @import "header-docs-left";
 @import "partition-key";
diff --git a/app/addons/documents/assets/scss/index-results.scss 
b/app/addons/documents/assets/scss/index-results.scss
index f60ec0c1..40a5afcd 100644
--- a/app/addons/documents/assets/scss/index-results.scss
+++ b/app/addons/documents/assets/scss/index-results.scss
@@ -195,31 +195,3 @@
     width: 300px;
   }
 }
-
-.execution-stats-popup {
-  font-size: 14px;
-  .execution-stats-popup-component {
-    margin: 0.5rem;
-    [data-status="false"] {
-      color: $cf-text01-muted;
-    }
-    [data-status="true"] {
-      .value {
-        font-weight: bold;
-      }
-    }
-  }
-  .warning {
-    color: $cf-alert-error-color;
-    margin: 0.5rem;
-  }
-}
-
-.execution-stats {
-  font-size: 13px;
-  padding: 8px 8px 8px 0;
-
-  .fonticon-attention-circled {
-    margin-right: 4px;
-  }
-}
diff --git a/app/addons/documents/assets/scss/mango-query.scss 
b/app/addons/documents/assets/scss/mango-query.scss
new file mode 100644
index 00000000..436a5d80
--- /dev/null
+++ b/app/addons/documents/assets/scss/mango-query.scss
@@ -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 "../../../../../assets/scss/variables";
+
+.mango-cheatsheet-modal {
+  max-width: none!important;
+  width: 50%!important;
+
+  .table-wrapper {
+    max-height: 300px;
+    overflow-y: scroll;
+  }
+}
+
+.execution-stats {
+  font-size: 13px;
+  padding: 8px 8px 8px 0;
+
+  .execution-stats-header {
+    font-size: 16px;
+    margin-top: 0.5rem;
+    margin-bottom: 0.5rem;
+  }
+
+  .execution-stats-empty-body {
+    font-style: italic;
+  }
+
+  .execution-stats-body {
+    [data-status="false"] {
+      color: $cf-text01-muted;
+    }
+    [data-status="true"] {
+      .value {
+        font-weight: bold;
+      }
+    }
+  }
+  .warning {
+    padding: 0.5rem;
+    margin-top: 0.5rem;
+    font-weight: 600;
+    background-color: $cf-alert-bg;
+    color: $cf-alert-color;
+    border-left: 4px solid $cf-warning;
+    .fonticon-attention-circled {
+      margin-right: 4px;
+    }
+    i::before {
+      color: $cf-warning;
+    }
+  }
+}
diff --git a/app/addons/documents/assets/scss/view-editor.scss 
b/app/addons/documents/assets/scss/view-editor.scss
index 54526158..19d8ebe9 100644
--- a/app/addons/documents/assets/scss/view-editor.scss
+++ b/app/addons/documents/assets/scss/view-editor.scss
@@ -62,6 +62,13 @@
   .help-link {
     margin-left: 4px;
   }
+
+  .mango-code-editor {
+    height: 300px;
+    & > div {
+      height: 90%;
+    }
+  }
 }
 
 .mango-select {
diff --git a/app/addons/documents/index-results/actions/fetch.js 
b/app/addons/documents/index-results/actions/fetch.js
index 148ee439..933ce61d 100644
--- a/app/addons/documents/index-results/actions/fetch.js
+++ b/app/addons/documents/index-results/actions/fetch.js
@@ -100,8 +100,13 @@ export const fetchDocs = (queryDocs, fetchParams, 
queryOptionsParams) => {
       if (layout) {
         dispatch(changeLayout(layout));
       }
+      const augmentedStats = executionStats ? {
+        ...executionStats,
+        // inject when stats were fetched
+        ts: new Date().getTime(),
+      } : null;
       // dispatch that we're all done
-      dispatch(newResultsAvailable(finalDocList, params, canShowNext, docType, 
executionStats, warning));
+      dispatch(newResultsAvailable(finalDocList, params, canShowNext, docType, 
augmentedStats, warning));
     }).catch((error) => {
       if (error && error.message.includes('`partition` parameter is not 
supported')) {
         dispatch(partitionParamNotSupported());
diff --git a/app/addons/documents/index-results/reducers.js 
b/app/addons/documents/index-results/reducers.js
index 838dbc8a..d45efe59 100644
--- a/app/addons/documents/index-results/reducers.js
+++ b/app/addons/documents/index-results/reducers.js
@@ -60,7 +60,9 @@ const initialState = {
     showReduce: false,
     stable: false,
     update: 'true'
-  }
+  },
+  executionStats: null,
+  warning: null,
 };
 
 function loadStyle() {
@@ -95,6 +97,8 @@ export default function resultsState(state = initialState, 
action) {
     case ActionTypes.INDEX_RESULTS_REDUX_RESET_STATE:
       return {
         ...initialState,
+        executionStats: state.executionStats,
+        warning: state.warning,
         noResultsWarning: state.noResultsWarning,
         selectedLayout: state.selectedLayout,
         selectedDocs: [],
diff --git a/app/addons/documents/mango/__tests__/mango.components.test.js 
b/app/addons/documents/mango/__tests__/mango.components.test.js
index f4a575f4..27ddb71b 100644
--- a/app/addons/documents/mango/__tests__/mango.components.test.js
+++ b/app/addons/documents/mango/__tests__/mango.components.test.js
@@ -12,6 +12,7 @@
 
 import { mount, shallow } from 'enzyme';
 import React from 'react';
+import { act } from 'react-dom/test-utils';
 import { Provider } from 'react-redux';
 import { createStore, applyMiddleware, combineReducers } from 'redux';
 import thunk from 'redux-thunk';
@@ -205,11 +206,13 @@ describe('MangoQueryEditor', function () {
     queryFindCodeChanged: false,
     databaseName: 'db1',
     partitionKey: '',
+    executionStatsSupported: true,
     runExplainQuery: () => {},
     runQuery: () => {},
     manageIndexes: () => {},
     loadQueryHistory: () => {},
-    clearResults: () => {}
+    clearResults: () => {},
+    checkExecutionStatsSupport: () => {},
   };
 
   it('runs explain query with partition when one is set', function () {
@@ -228,7 +231,7 @@ describe('MangoQueryEditor', function () {
     expect(partitionKey).toBe('part1');
   });
 
-  it('runs explain query with partition when one is set', function () {
+  it('runs query with partition when one is set', function () {
     const runQueryStub = sinon.stub();
     const wrapper = mount(
       <MangoQueryEditor
@@ -243,4 +246,81 @@ describe('MangoQueryEditor', function () {
     const { partitionKey } = runQueryStub.firstCall.args[0];
     expect(partitionKey).toBe('part1');
   });
+
+  it('opens cheatsheet modal', async () => {
+    const wrapper = mount(
+      <MangoQueryEditor
+        {...defaultProps}
+      />
+    );
+
+    const modalSelector = '.modal-dialog.mango-cheatsheet-modal';
+    // Confirm modal is not open yet
+    expect(wrapper.find(modalSelector).exists()).toBe(false);
+    // Open modal
+    wrapper.find('.cheatsheet-icon').simulate('click');
+    await act(async () => {
+      wrapper.update();
+    });
+    // Confirm modal is open
+    const modal = wrapper.find(modalSelector);
+    expect(modal.exists()).toBe(true);
+    expect(modal.text()).toContain('Query Cheatsheet');
+  });
+
+  it('shows execution stats empty body message', function () {
+    const wrapper = mount(
+      <MangoQueryEditor
+        {...defaultProps}
+        executionStatsSupported
+      />
+    );
+
+    expect(wrapper.find('.execution-stats 
.execution-stats-empty-body').exists()).toBe(true);
+  });
+
+  it('shows execution stats values without warning', function () {
+    const sampleStats = {
+      total_keys_examined: 1,
+      total_docs_examined: 19,
+      total_quorum_docs_examined: 0,
+      results_returned: 18,
+      execution_time_ms: 3.231,
+    };
+    const wrapper = mount(
+      <MangoQueryEditor
+        {...defaultProps}
+        executionStatsSupported
+        executionStats={sampleStats}
+      />
+    );
+
+    // warning should not be shown
+    expect(wrapper.find('.execution-stats .warning').exists()).toBe(false);
+
+    const statsPanel = wrapper.find('.execution-stats .execution-stats-body');
+    expect(statsPanel.exists()).toBe(true);
+    // Deliberately not checking for exact date values, as they are subject to 
change due to TZ formatting
+    expect(statsPanel.text()).toContain('Executed at:');
+    expect(statsPanel.text()).toContain('Execution time: 3 ms');
+    expect(statsPanel.text()).toContain('Results returned: 18');
+    expect(statsPanel.text()).toContain('Keys examined: 1');
+    expect(statsPanel.text()).toContain('Documents examined: 19');
+    expect(statsPanel.text()).toContain('Documents examined (quorum): 0');
+  });
+
+  it('shows execution stats warning when provided', function () {
+    const wrapper = mount(
+      <MangoQueryEditor
+        {...defaultProps}
+        executionStatsSupported
+        warning={"sample warning"}
+      />
+    );
+
+    // warning should not be shown
+    const warning = wrapper.find('.execution-stats .warning');
+    expect(warning.exists()).toBe(true);
+    expect(warning.text()).toContain('sample warning');
+  });
 });
diff --git a/app/addons/documents/mango/components/ExecutionStats.js 
b/app/addons/documents/mango/components/ExecutionStats.js
deleted file mode 100644
index 48b815ad..00000000
--- a/app/addons/documents/mango/components/ExecutionStats.js
+++ /dev/null
@@ -1,127 +0,0 @@
-// 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 PropTypes from 'prop-types';
-import { Popover, OverlayTrigger } from 'react-bootstrap';
-
-export default class ExecutionStats extends React.Component {
-  constructor (props) {
-    super(props);
-  }
-
-  humanizeDuration(milliseconds) {
-    if (milliseconds < 1000) {
-      return Math.round(milliseconds) + ' ms';
-    }
-    let seconds = milliseconds / 1000;
-    if (seconds < 60) {
-      return Math.floor(seconds) + ' seconds';
-    }
-    const minutes = Math.floor(seconds / 60);
-    seconds = Math.floor(seconds - (minutes * 60));
-
-    const minuteText = minutes > 1 ? 'minutes' : 'minute';
-    const secondsText = seconds > 1 ? 'seconds' : 'second';
-
-    return [minutes, ' ', minuteText, ', ', seconds, ' ', 
secondsText].join('');
-  }
-
-  getWarning(warnings) {
-    if (warnings) {
-      const lines = warnings.split('\n').map((warnText, i) => {
-        return <React.Fragment key={i}>{warnText}<br/></React.Fragment>;
-      });
-      return <span>{lines}</span>;
-    }
-  }
-
-  warningPopupComponent(warningText) {
-    if (warningText) {
-      return (<div className="warning">
-        <i className="fonticon-attention-circled"></i> {warningText}
-      </div>);
-    }
-  }
-
-  executionStatsLine(title, value, alwaysShow = false, units = "") {
-    if (typeof value === 'number') {
-      const hasValue = value === 0 && !alwaysShow ? "false" : "true";
-      return <div data-status={hasValue}>{title + ": "}<span 
className="value">{value.toLocaleString()} {units}</span></div>;
-    }
-    return null;
-  }
-
-  executionStatsPopupComponent(executionStats) {
-    if (!executionStats) return null;
-    return (
-      <div className="execution-stats-popup-component">
-        {/* keys examined always 0 so hide it for now */}
-        {/* {this.executionStatsLine("keys examined", 
executionStats.total_keys_examined)} */}
-        {this.executionStatsLine("documents examined", 
executionStats.total_docs_examined)}
-        {this.executionStatsLine("documents examined (quorum)", 
executionStats.total_quorum_docs_examined)}
-        {this.executionStatsLine("results returned", 
executionStats.results_returned, true)}
-        {this.executionStatsLine("execution time", 
Math.round(executionStats.execution_time_ms), false, "ms")}
-      </div>
-    );
-  }
-
-  popup(executionStats, warningText) {
-    return (
-      <Popover id="popover-execution-stats" title="Execution Statistics">
-        <div className="execution-stats-popup">
-          {this.executionStatsPopupComponent(executionStats)}
-          {this.warningPopupComponent(warningText)}
-        </div>
-      </Popover>
-    );
-  }
-
-  render() {
-    const {
-      executionStats,
-      warning
-    } = this.props;
-
-    const warningText = this.getWarning(warning);
-
-    let warningComponent = null;
-    if (warningText) {
-      warningComponent = <i className="fonticon-attention-circled"></i>;
-    }
-
-    let executionStatsComponent = null;
-    if (executionStats) {
-      executionStatsComponent = (
-        <span className="execution-stats-component">Executed in 
{this.humanizeDuration(executionStats.execution_time_ms)}</span>
-      );
-    } else if (warningText) {
-      executionStatsComponent = (
-        <span className="execution-stats-component">Warning</span>
-      );
-    }
-
-    const popup = this.popup(executionStats, warningText);
-    return (
-      <OverlayTrigger trigger={['hover', 'focus', 'click']} placement="right" 
overlay={popup}>
-        <span className="execution-stats">
-          {warningComponent}
-          {executionStatsComponent}
-        </span>
-      </OverlayTrigger>
-    );
-  }
-}
-
-ExecutionStats.propTypes = {
-  executionStats: PropTypes.object,
-  warning: PropTypes.string
-};
diff --git a/app/addons/documents/mango/components/ExecutionStatsPanel.js 
b/app/addons/documents/mango/components/ExecutionStatsPanel.js
new file mode 100644
index 00000000..a6059090
--- /dev/null
+++ b/app/addons/documents/mango/components/ExecutionStatsPanel.js
@@ -0,0 +1,107 @@
+// 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 PropTypes from 'prop-types';
+
+export default class ExecutionStatsPanel extends React.Component {
+  constructor (props) {
+    super(props);
+  }
+
+  humanizeDuration(milliseconds) {
+    if (milliseconds < 1000) {
+      return Math.round(milliseconds) + ' ms';
+    }
+    let seconds = milliseconds / 1000;
+    if (seconds < 60) {
+      return Math.floor(seconds) + ' seconds';
+    }
+    const minutes = Math.floor(seconds / 60);
+    seconds = Math.floor(seconds - (minutes * 60));
+
+    const minuteText = minutes > 1 ? 'minutes' : 'minute';
+    const secondsText = seconds > 1 ? 'seconds' : 'second';
+
+    return [minutes, ' ', minuteText, ', ', seconds, ' ', 
secondsText].join('');
+  }
+
+  getWarning(warnings) {
+    if (warnings) {
+      const lines = warnings.split('\n').map((warnText, i) => {
+        return <React.Fragment key={i}>{warnText}<br/></React.Fragment>;
+      });
+      return <span>{lines}</span>;
+    }
+  }
+
+  warningPopupComponent(warningText) {
+    if (warningText) {
+      return (<div className="warning">
+        <i className="fonticon-attention-circled"></i> {warningText}
+      </div>);
+    }
+  }
+
+  executionStatsLine(title, value, alwaysShow = false, units = "") {
+    const hasValue = value === 0 && !alwaysShow ? "false" : "true";
+    return <div data-status={hasValue}>{title + ": "}<span 
className="value">{value.toLocaleString()} {units}</span></div>;
+  }
+
+  executionStatsBody(executionStats) {
+
+    let content = null;
+    if (!executionStats) {
+      content = (
+        <div className='execution-stats-empty-body'>
+          Run query to display execution statistics.
+        </div>);
+    } else {
+      content = (
+        <div className="execution-stats-body">
+          {this.executionStatsLine("Executed at", new 
Date(executionStats.ts).toLocaleTimeString(), true)}
+          {this.executionStatsLine("Execution time", 
this.humanizeDuration(executionStats.execution_time_ms), true)}
+          {this.executionStatsLine("Results returned", 
executionStats.results_returned, true)}
+          {this.executionStatsLine("Keys examined", 
executionStats.total_keys_examined)}
+          {this.executionStatsLine("Documents examined", 
executionStats.total_docs_examined)}
+          {this.executionStatsLine("Documents examined (quorum)", 
executionStats.total_quorum_docs_examined)}
+        </div>);
+    }
+    return (<>
+      <div className="execution-stats-header">Execution Statistics</div>
+      {content}
+    </>);
+  }
+
+  render() {
+    const {
+      executionStatsSupported,
+      executionStats,
+      warning
+    } = this.props;
+
+    const warningText = this.getWarning(warning);
+    return (
+      <>
+        <div className="execution-stats">
+          {this.warningPopupComponent(warningText)}
+          {executionStatsSupported ? this.executionStatsBody(executionStats) : 
null}
+        </div>
+      </>
+    );
+  }
+}
+
+ExecutionStatsPanel.propTypes = {
+  executionStats: PropTypes.object,
+  warning: PropTypes.string,
+  executionStatsSupported: PropTypes.bool
+};
diff --git a/app/addons/documents/mango/components/MangoIndexEditor.js 
b/app/addons/documents/mango/components/MangoIndexEditor.js
index a53fc94f..cb6b2d8a 100644
--- a/app/addons/documents/mango/components/MangoIndexEditor.js
+++ b/app/addons/documents/mango/components/MangoIndexEditor.js
@@ -116,7 +116,9 @@ export default class MangoIndexEditor extends Component {
               ref={node => this.codeEditor = node}
               title="Index"
               docLink={getDocUrl('MANGO_INDEX')}
-              defaultCode={this.props.queryIndexCode} />
+              defaultCode={this.props.queryIndexCode}
+              setHeightToLineCount={false}
+              className="mango-code-editor"/>
             {this.partitionedCheckobx()}
           </PaddedBorderedBox>
           <div className="padded-box">
diff --git a/app/addons/documents/mango/components/MangoQueryCheatsheetModal.js 
b/app/addons/documents/mango/components/MangoQueryCheatsheetModal.js
new file mode 100644
index 00000000..22cb6b51
--- /dev/null
+++ b/app/addons/documents/mango/components/MangoQueryCheatsheetModal.js
@@ -0,0 +1,138 @@
+// 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 { Modal, Table } from 'react-bootstrap';
+
+export default function MangoQueryCheatsheetModal({isVisible, onHide}) {
+  return <Modal dialogClassName="mango-cheatsheet-modal" show={isVisible}>
+    <Modal.Header closeButton={false}>
+      <Modal.Title>Query Cheatsheet</Modal.Title>
+    </Modal.Header>
+    <Modal.Body>
+      <div className='table-wrapper'>
+
+        <Table striped>
+          <thead>
+            <tr>
+              <th>Condition Operator</th>
+              <th>Argument</th>
+              <th>Purpose</th>
+            </tr>
+          </thead>
+          <tbody>
+            <tr>
+              <td>
+                <code>$eq</code>, <code>$ne</code><br/>
+                <code>$lt</code>, <code>$lte</code><br/>
+                <code>$gt</code>, <code>$gte</code>
+              </td>
+              <td>Any JSON value</td>
+              <td>
+                Equal, Not equal<br/>
+                Lesser, Lesser or equal,<br/>
+                Greater, Greater or equal
+              </td>
+            </tr>
+            <tr>
+              <td><code>$exists</code></td>
+              <td>Boolean</td>
+              <td>Check field exists or not</td>
+            </tr>
+            <tr>
+              <td><code>$type</code></td>
+              <td>String</td>
+              <td>Check field type, accepts: <code>&quot;null&quot;</code>, 
<code>&quot;boolean&quot;</code>,
+                <code>&quot;number&quot;</code>, 
<code>&quot;string&quot;</code>, <code>&quot;array&quot;</code> and 
<code>&quot;object&quot;</code>
+              </td>
+            </tr>
+            <tr>
+              <td><code>$in</code>, <code>$nin</code></td>
+              <td>Array of JSON values</td>
+              <td>Field must exist / not exist</td>
+            </tr>
+            <tr>
+              <td><code>$size</code></td>
+              <td>Integer</td>
+              <td>Match length of an array field</td>
+            </tr>
+            <tr>
+              <td><code>$mod</code></td>
+              <td>[Divisor, Remainder]</td>
+              <td>Matches <pre>field % Divisor == Remainder</pre></td>
+            </tr>
+            <tr>
+              <td><code>$regex</code></td>
+              <td>String</td>
+              <td>String value matches a regex</td>
+            </tr>
+          </tbody>
+        </Table>
+        <br/>
+        <Table striped>
+          <thead>
+            <tr>
+              <th>Combination Operators</th>
+              <th>Argument</th>
+              <th>Purpose</th>
+            </tr>
+          </thead>
+          <tbody>
+            <tr>
+              <td><code>$and</code></td>
+              <td>Array</td>
+              <td>Matches if ALL the selectors in the array match</td>
+            </tr>
+            <tr>
+              <td><code>$or</code></td>
+              <td>Array</td>
+              <td>Matches if ANY of the selectors in the array match</td>
+            </tr>
+            <tr>
+              <td><code>$nor</code></td>
+              <td>Array</td>
+              <td>Matches if NONE of the selectors in the array match</td>
+            </tr>
+            <tr>
+              <td><code>$not</code></td>
+              <td>Selector</td>
+              <td>Matches if the given selector does not match</td>
+            </tr>
+            <tr>
+              <td><code>$all</code></td>
+              <td>Array</td>
+              <td>Matches an array value if it contains all the elements of 
the argument array</td>
+            </tr>
+            <tr>
+              <td><code>$elemMatch</code></td>
+              <td>Selector</td>
+              <td>Matches an array field with AT LEAST ONE element that 
matches ALL the specified query criteria</td>
+            </tr>
+            <tr>
+              <td><code>$allMatch</code></td>
+              <td>Selector</td>
+              <td>Matches an array field with ALL elements matching ALL the 
specified query criteria</td>
+            </tr>
+            <tr>
+              <td><code>$keyMapMatch</code></td>
+              <td>Selector</td>
+              <td>Matches a map that contains AT LEAST ONE key that matches 
ALL the specified query criteria</td>
+            </tr>
+          </tbody>
+        </Table>
+      </div>
+    </Modal.Body>
+    <Modal.Footer>
+      <button onClick={onHide} data-bypass="true" className="btn 
btn-cf-secondary">Close</button>
+    </Modal.Footer>
+  </Modal>;
+}
diff --git a/app/addons/documents/mango/components/MangoQueryEditor.js 
b/app/addons/documents/mango/components/MangoQueryEditor.js
index eb005fb3..b5d1ff44 100644
--- a/app/addons/documents/mango/components/MangoQueryEditor.js
+++ b/app/addons/documents/mango/components/MangoQueryEditor.js
@@ -18,7 +18,8 @@ import "../../../../../assets/js/plugins/prettify";
 import app from "../../../../app";
 import FauxtonAPI from "../../../../core/api";
 import ReactComponents from "../../../components/react-components";
-import ExecutionStats from './ExecutionStats';
+import ExecutionStatsPanel from './ExecutionStatsPanel';
+import MangoQueryCheatsheetModal from './MangoQueryCheatsheetModal';
 
 const PaddedBorderedBox = ReactComponents.PaddedBorderedBox;
 const CodeEditorPanel = ReactComponents.CodeEditorPanel;
@@ -35,6 +36,7 @@ export default class MangoQueryEditor extends Component {
     this.props.loadQueryHistory({ databaseName: this.props.databaseName });
     // Clear results list in case it was populated by other pages
     this.props.clearResults();
+    this.props.checkExecutionStatsSupport({ databaseName: 
this.props.databaseName });
 
     // Add key binding to run query when doing Ctrl-Enter
     const editor = this.codeEditor.codeEditor.editor;
@@ -75,9 +77,22 @@ export default class MangoQueryEditor extends Component {
     this.setEditorValue(selectedItem.value);
   }
 
+  state = {
+    isCheatsheetVisible: false,
+  };
+
+  hideCheatsheetModal = () => {
+    this.setState({isCheatsheetVisible: false});
+  };
+
+  showCheatsheetModal = () => {
+    this.setState({isCheatsheetVisible: true});
+  };
+
   editor() {
     return (
       <div className="mango-editor-wrapper">
+        <MangoQueryCheatsheetModal isVisible={this.state.isCheatsheetVisible} 
onHide={this.hideCheatsheetModal}/>
         <form className="form-horizontal" onSubmit={(ev) => 
{this.runQuery(ev);}}>
           <div className="padded-box">
             <ReactSelect
@@ -97,7 +112,12 @@ export default class MangoQueryEditor extends Component {
               ref={node => this.codeEditor = node}
               title={this.props.editorTitle}
               docLink={getDocUrl('MANGO_SEARCH')}
-              defaultCode={this.props.queryFindCode} />
+              syntaxMode="javascript"
+              defaultCode={this.props.queryFindCode}
+              showCheatSheetIcon={true}
+              onCheatsheatIconClick={this.showCheatsheetModal}
+              setHeightToLineCount={false}
+              className="mango-code-editor"/>
           </PaddedBorderedBox>
           <div className="padded-box">
             <div className="actions-panel">
@@ -109,7 +129,7 @@ export default class MangoQueryEditor extends Component {
               </div>
             </div>
             <div>
-              <ExecutionStats {...this.props} />
+              <ExecutionStatsPanel {...this.props} />
             </div>
           </div>
         </form>
@@ -190,9 +210,11 @@ MangoQueryEditor.propTypes = {
   queryFindCodeChanged: PropTypes.bool,
   databaseName: PropTypes.string.isRequired,
   partitionKey: PropTypes.string,
+  executionStatsSupported: PropTypes.bool.isRequired,
   runExplainQuery: PropTypes.func.isRequired,
   runQuery: PropTypes.func.isRequired,
   manageIndexes: PropTypes.func.isRequired,
   loadQueryHistory: PropTypes.func.isRequired,
-  clearResults: PropTypes.func.isRequired
+  clearResults: PropTypes.func.isRequired,
+  checkExecutionStatsSupport: PropTypes.func.isRequired,
 };
diff --git a/app/addons/documents/mango/components/MangoQueryEditorContainer.js 
b/app/addons/documents/mango/components/MangoQueryEditorContainer.js
index ede0e569..8272b0ba 100644
--- a/app/addons/documents/mango/components/MangoQueryEditorContainer.js
+++ b/app/addons/documents/mango/components/MangoQueryEditorContainer.js
@@ -60,7 +60,8 @@ const mapStateToProps = (state, ownProps) => {
     fetchParams: indexResults.fetchParams,
     executionStats: indexResults.executionStats,
     warning: indexResults.warning,
-    partitionKey: ownProps.partitionKey
+    partitionKey: ownProps.partitionKey,
+    executionStatsSupported: mangoQuery.executionStatsSupported,
   };
 };
 
@@ -70,6 +71,10 @@ const mapDispatchToProps = (dispatch/*, ownProps*/) => {
       dispatch(Actions.loadQueryHistory(options));
     },
 
+    checkExecutionStatsSupport: (options) => {
+      dispatch(Actions.checkExecutionStatsSupport(options));
+    },
+
     runExplainQuery: (options) => {
       dispatch(Actions.runExplainQuery(options));
     },
diff --git a/app/addons/documents/mango/mango.actions.js 
b/app/addons/documents/mango/mango.actions.js
index be79bd4a..5e4309a6 100644
--- a/app/addons/documents/mango/mango.actions.js
+++ b/app/addons/documents/mango/mango.actions.js
@@ -65,6 +65,21 @@ export default {
     };
   },
 
+  checkExecutionStatsSupport: function ({ databaseName }) {
+    return (dispatch) => {
+      return MangoAPI.supportsExecutionStats(databaseName)
+        .then(isSupported => {
+          if (isSupported) {
+            dispatch({
+              type: ActionTypes.MANGO_SET_EXECUTION_STATS_SUPPORTED,
+            });
+          }
+        }).catch(() => {
+          // ignore, assume it's not supported
+        });
+    };
+  },
+
   saveIndex: function ({ databaseName, indexCode, fetchParams }) {
     FauxtonAPI.addNotification({
       msg: 'Saving index for query...',
diff --git a/app/addons/documents/mango/mango.actiontypes.js 
b/app/addons/documents/mango/mango.actiontypes.js
index 12d93430..f1d87acb 100644
--- a/app/addons/documents/mango/mango.actiontypes.js
+++ b/app/addons/documents/mango/mango.actiontypes.js
@@ -18,4 +18,5 @@ export default {
   MANGO_SAVE_INDEX_REQUEST: 'MANGO_SAVE_INDEX_REQUEST',
   MANGO_SHOW_EXPLAIN_RESULTS: 'MANGO_SHOW_EXPLAIN_RESULTS',
   MANGO_HIDE_EXPLAIN_RESULTS: 'MANGO_HIDE_EXPLAIN_RESULTS',
+  MANGO_SET_EXECUTION_STATS_SUPPORTED: 'MANGO_SET_EXECUTION_STATS_SUPPORTED',
 };
diff --git a/app/addons/documents/mango/mango.api.js 
b/app/addons/documents/mango/mango.api.js
index 59cbebf8..9906f747 100644
--- a/app/addons/documents/mango/mango.api.js
+++ b/app/addons/documents/mango/mango.api.js
@@ -59,7 +59,7 @@ export const fetchIndexes = (databaseName, params) => {
 // assume all databases being accessed are on the same
 // host / CouchDB version
 let supportsExecutionStatsCache = null;
-const supportsExecutionStats = (databaseName) => {
+export const supportsExecutionStats = (databaseName) => {
   if (supportsExecutionStatsCache === null) {
     return new FauxtonAPI.Promise((resolve) => {
       mangoQuery(databaseName, '', {
diff --git a/app/addons/documents/mango/mango.helper.js 
b/app/addons/documents/mango/mango.helper.js
index e2a3d7cb..bd56c159 100644
--- a/app/addons/documents/mango/mango.helper.js
+++ b/app/addons/documents/mango/mango.helper.js
@@ -43,14 +43,7 @@ const formatCode = (code) => {
 };
 
 const getIndexContent = (doc) => {
-  const content = {
-    ...doc
-  };
-
-  delete content.ddoc;
-  delete content.name;
-
-  return JSON.stringify(content, null, ' ');
+  return JSON.stringify(doc, null, ' ');
 };
 
 export default {
diff --git a/app/addons/documents/mango/mango.reducers.js 
b/app/addons/documents/mango/mango.reducers.js
index 8e4cd800..7fd05774 100644
--- a/app/addons/documents/mango/mango.reducers.js
+++ b/app/addons/documents/mango/mango.reducers.js
@@ -79,7 +79,8 @@ const initialState = {
   explainPlan: undefined,
   history: getDefaultHistory(),
   historyKey: 'default',
-  queryIndexTemplates: getDefaultQueryIndexTemplates()
+  queryIndexTemplates: getDefaultQueryIndexTemplates(),
+  executionStatsSupported: false,
 };
 
 const loadQueryHistory = (databaseName) => {
@@ -199,6 +200,12 @@ export default function mangoquery(state = initialState, 
action) {
         explainPlan: false
       };
 
+    case ActionTypes.MANGO_SET_EXECUTION_STATS_SUPPORTED:
+      return {
+        ...state,
+        executionStatsSupported: true,
+      };
+
     default:
       return state;
   }


Reply via email to