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>"null"</code>,
<code>"boolean"</code>,
+ <code>"number"</code>,
<code>"string"</code>, <code>"array"</code> and
<code>"object"</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;
}