diff --git a/app/addons/components/__tests__/badges.test.js b/app/addons/components/__tests__/badges.test.js index b0d6eb027..e2b960f78 100644 --- a/app/addons/components/__tests__/badges.test.js +++ b/app/addons/components/__tests__/badges.test.js @@ -24,7 +24,7 @@ describe('Badges', () => { it('supports custom label formatters', () => { const el = mount( - {}} getLabel={(el) => { return el + 'foo'; }} /> + {}} showClose={true} getLabel={(el) => { return el + 'foo'; }} /> ); expect(el.find('.badge').first().text()).toBe('foofoo×'); diff --git a/app/addons/components/components/badge.js b/app/addons/components/components/badge.js index 0f56c3d6d..84fc1d010 100644 --- a/app/addons/components/components/badge.js +++ b/app/addons/components/components/badge.js @@ -11,6 +11,7 @@ // the License. import PropTypes from 'prop-types'; +import { Tooltip, OverlayTrigger } from 'react-bootstrap'; import React from "react"; import ReactDOM from "react-dom"; @@ -18,7 +19,9 @@ import ReactDOM from "react-dom"; export class BadgeList extends React.Component { static propTypes = { elements: PropTypes.array.isRequired, - removeBadge: PropTypes.func.isRequired + removeBadge: PropTypes.func.isRequired, + showClose: PropTypes.bool, + tagExplanations: PropTypes.object }; static defaultProps = { @@ -28,7 +31,9 @@ export class BadgeList extends React.Component { getId (el) { return el; - } + }, + showClose: false, + tagExplanations: null }; getBadges = () => { @@ -37,7 +42,10 @@ export class BadgeList extends React.Component { label={this.props.getLabel(el)} key={i} id={el} - remove={this.removeBadge} />; + remove={this.removeBadge} + showClose={this.props.showClose} + showTooltip={!!this.props.tagExplanations} + tooltip={this.props.tagExplanations ? this.props.tagExplanations[el] : ''} />; }.bind(this)); }; @@ -57,7 +65,15 @@ export class BadgeList extends React.Component { export class Badge extends React.Component { static propTypes = { label: PropTypes.string.isRequired, - remove: PropTypes.func.isRequired + remove: PropTypes.func.isRequired, + showClose: PropTypes.bool, + showTooltip: PropTypes.bool, + tooltip: PropTypes.string + }; + static defaultProps = { + showClose: false, + showTooltip: false, + tooltip: '' }; remove = (e) => { @@ -66,17 +82,26 @@ export class Badge extends React.Component { }; render() { + const className = "badge " + this.props.label.replace(' ', '-'); + const tooltip = {this.props.tooltip}; + return ( -
  • +
  • - {this.props.label} - + {this.props.showTooltip ? + + {this.props.label} + : + {this.props.label}} + { this.props.showClose ? + × - + + : null}
  • ); diff --git a/app/addons/documents/assets/scss/mango-query.scss b/app/addons/documents/assets/scss/mango-query.scss index 436a5d806..cf304f475 100644 --- a/app/addons/documents/assets/scss/mango-query.scss +++ b/app/addons/documents/assets/scss/mango-query.scss @@ -61,3 +61,96 @@ } } } + +.explain-view-toggle { + margin-bottom: 1rem; +} + +.explain-json-response { + padding-left: 0rem; + margin-top: 1rem; +} + +.explain-plan-section-title { + font-size: 1rem; + i { + margin-left: 0.5rem; + } +} + +.explain-reasons-legend-modal { + max-width: none!important; + width: 50%!important; + + .table-wrapper { + max-height: 300px; + overflow-y: scroll; + } +} + +.explain-index-panel { + background-color: $cf-white; + margin-left: 0.25rem !important; + margin-right: 0.25rem !important; + padding: 0.5rem; + border-top: 1px solid $cf-border-color01; + border-bottom: 1px solid $cf-border-color01; + word-break: break-all; + font-size: 0.875rem; + + .index-ddoc-name { + font-size: 0.75rem; + } + + .index-extra-info { + word-break: normal; + } + + i.icon { + margin-left: 0.5rem; + } + + ul.component-badgelist { + margin-bottom: 0; + margin-top: 0; + } + + li.badge { + margin-left: 0; + padding-left: 0; + + div.remove-filter { + padding: 0.5rem; + background-color: $cf-info; + border-radius: 5px; + span { + padding: 0; + margin: 0; + color: $cf-white; + font-size: 14px; + } + } + } + + li.covering { + div.remove-filter { + background-color: $cf-primary; + span { + color: $cf-white; + } + } + } + + li.partitioned { + div.remove-filter { + background-color: $cf-tab-element-badge; + span { + color: $cf-white; + } + } + } + + .fonticon-attention-circled { + margin-right: 4px; + } +} diff --git a/app/addons/documents/changes/components/ChangesTabContent.js b/app/addons/documents/changes/components/ChangesTabContent.js index 205fe443e..b6067624c 100644 --- a/app/addons/documents/changes/components/ChangesTabContent.js +++ b/app/addons/documents/changes/components/ChangesTabContent.js @@ -39,7 +39,7 @@ export default class ChangesTabContent extends React.Component {
    this.props.removeFilter(label)} addFilter={this.addFilter} hasFilter={this.hasFilter} /> - this.props.removeFilter(label)} /> + this.props.removeFilter(label)} showClose={true} />
    ); } diff --git a/app/addons/documents/mango/__tests__/mango.components.test.js b/app/addons/documents/mango/__tests__/mango.components.test.js index 27ddb71b2..5c2067f28 100644 --- a/app/addons/documents/mango/__tests__/mango.components.test.js +++ b/app/addons/documents/mango/__tests__/mango.components.test.js @@ -26,6 +26,8 @@ import MangoQueryEditor from '../components/MangoQueryEditor'; import MangoIndexEditor from '../components/MangoIndexEditor'; import mangoReducer from '../mango.reducers'; import '../../base'; +import ExplainPage from '../components/ExplainPage'; +import {explainPlan, explainPlanCandidates} from './sampleexplainplan'; const restore = utils.restore; const databaseName = 'testdb'; @@ -324,3 +326,59 @@ describe('MangoQueryEditor', function () { expect(warning.text()).toContain('sample warning'); }); }); + +describe('Explain Page', function() { + const defaultProps = { + viewFormat: 'parsed', + isReasonsModalVisible: false, + onViewFormatChange: (() => {}), + resetState: (() => {}), + hideReasonsModal: (() => {}), + showReasonsModal: (() => {}), + }; + + it('shows suitable/unsuitable indexes when available', function() { + const wrapper = mount( + + ); + expect(wrapper.find('#explain-parsed-view .btn.active.btn-cf-secondary')).toHaveLength(1); + const headers = wrapper.find('.explain-plan-section-title'); + const panels = wrapper.find('.row.explain-index-panel'); + expect(headers).toHaveLength(3); + expect(panels).toHaveLength(5); + + expect((headers.get(0)).props.children).toContain('Selected Index'); + expect((panels.get(1)).props.children[0]).toMatchObject(/foo-json-index/); + + expect((headers.get(1)).props.children).toContain('Suitable Indexes'); + expect((panels.get(1)).props.children[0]).toMatchObject(/foo-test-json-index/); + + expect((headers.get(2)).props.children).toContain('Unsuitable Indexes'); + expect((panels.get(2)).props.children[0]).toMatchObject(/_all_docs/); + }); + + it('toggles between parsed and json, with no candidate indexes', function() { + let spy = sinon.spy(); + const wrapper = mount( + + ); + + expect(wrapper.find('#explain-parsed-view .btn.active.btn-cf-secondary')).toHaveLength(1); + expect(wrapper.find('.explain-plan-section-title')).toHaveLength(1); + expect(((wrapper.find('.explain-plan-section-title')).get(0)).props.children).toContain('Selected Index'); + wrapper.find('button#explain-json-view').simulate('click'); + expect(spy.calledOnce).toBeTruthy(); + }); + + it('toggles between json and parsed', function() { + let spy = sinon.spy(); + const wrapper = mount( + + ); + + expect(wrapper.find('#explain-json-view .btn.active.btn-cf-secondary')).toHaveLength(1); + expect(((wrapper.find('.explain-plan-section-title')).get(0)).props.children).toContain('JSON Response'); + wrapper.find('button#explain-parsed-view').simulate('click'); + expect(spy.calledOnce).toBeTruthy(); + }); +}); diff --git a/app/addons/documents/mango/__tests__/sampleexplainplan.js b/app/addons/documents/mango/__tests__/sampleexplainplan.js new file mode 100644 index 000000000..2d3dd3e95 --- /dev/null +++ b/app/addons/documents/mango/__tests__/sampleexplainplan.js @@ -0,0 +1,186 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +export const explainPlan = { + "dbname": "/test", + "index": { + "ddoc": "_design/428a2f2506db4e77001bfe08fa0b79ea9eaf0279", + "name": "foo-json-index", + "type": "json", + "partitioned": false, + "def": { + "fields": [ + { + "foo": "asc" + } + ] + } + }, + "mrargs": { + "include_docs": true, + "view_type": "map", + "reduce": false, + "partition": null, + "start_key": [ + 0 + ], + "end_key": [ + "" + ], + "direction": "fwd", + "stable": false, + "update": true, + "conflicts": "undefined" + }, + "covering": false +}; + +export const explainPlanCandidates = { + "dbname": "/testIndexes", + "index": { + "ddoc": "_design/428a2f2506db4e77001bfe08fa0b79ea9eaf0279", + "name": "foo-json-index", + "type": "json", + "partitioned": false, + "def": { + "fields": [ + { + "foo": "asc" + } + ] + } + }, + "index_candidates": [ + { + "index": { + "ddoc": "_design/bbed516c3e93d98a9a13e5d98d4acc71d820e5ff", + "name": "bar-json-index", + "type": "json", + "partitioned": false, + "def": { + "fields": [ + { + "bar": "asc" + }, + { + "foo": "asc" + } + ] + } + }, + "analysis": { + "usable": false, + "reasons": [ + { + "name": "field_mismatch" + } + ], + "ranking": 3, + "covering": false + } + }, + { + "index": { + "ddoc": "_design/9389a767083b391afff6311ca0bc203b9becc80a", + "name": "test-json-index", + "type": "json", + "partitioned": false, + "def": { + "fields": [ + { + "test": "asc" + } + ] + } + }, + "analysis": { + "usable": false, + "reasons": [ + { + "name": "field_mismatch" + } + ], + "ranking": 3, + "covering": false + } + }, + { + "index": { + "ddoc": "_design/43919ae9a7c472f6bdda0caa86ea8f139a3aaf14", + "name": "foo-test-json-index", + "type": "json", + "partitioned": false, + "def": { + "fields": [ + { + "foo": "asc" + }, + { + "test": "asc" + } + ] + } + }, + "analysis": { + "usable": true, + "reasons": [ + { + "name": "less_overlap" + } + ], + "ranking": 1, + "covering": false + } + }, + { + "index": { + "ddoc": null, + "name": "_all_docs", + "type": "special", + "def": { + "fields": [ + { + "_id": "asc" + } + ] + } + }, + "analysis": { + "usable": true, + "reasons": [ + { + "name": "unfavored_type" + } + ], + "ranking": 2, + "covering": null + } + } + ], + "mrargs": { + "include_docs": true, + "view_type": "map", + "reduce": false, + "partition": null, + "start_key": [ + 0 + ], + "end_key": [ + "" + ], + "direction": "fwd", + "stable": false, + "update": true, + "conflicts": "undefined" + }, + "covering": false +}; diff --git a/app/addons/documents/mango/components/ExplainPage.js b/app/addons/documents/mango/components/ExplainPage.js index 927780f99..9f6d06d66 100644 --- a/app/addons/documents/mango/components/ExplainPage.js +++ b/app/addons/documents/mango/components/ExplainPage.js @@ -11,8 +11,10 @@ // the License. import PropTypes from 'prop-types'; - import React, { Component } from "react"; +import { Button, ButtonGroup, Tooltip, OverlayTrigger } from 'react-bootstrap'; +import IndexPanel from "./IndexPanel"; +import ExplainReasonsLegendModal from './ExplainReasonsLegendModal'; export default class ExplainPage extends Component { componentDidMount () { @@ -23,15 +25,182 @@ export default class ExplainPage extends Component { prettyPrint(); } + componentWillUnmount() { + this.props.resetState(); + } + + // Sort candidates indexes to show list JSON indexes not chosen first, then unusable + // JSON indexes, then all others (text, partial, etc) + sortCandidatesByRanking(a, b) { + if (a.analysis.ranking === undefined) { + return 1; + } + if (b.analysis.ranking === undefined) { + return -1; + } + const diff = a.analysis.ranking - b.analysis.ranking; + if (diff === 0) { + return a.index.name.localeCompare(b.index.name); + } + return diff; + } + + pickUsableIndexes(candidates) { + return candidates.filter(c => { + return c.index.type === 'json' && c.analysis.usable; + }).sort(this.sortCandidatesByRanking); + } + + pickNotUsableIndexes(candidates) { + return candidates.filter(c => { + return c.index.type !== 'json' || !c.analysis.usable; + }).sort(this.sortCandidatesByRanking); + } + + toggleButtons() { + return ( +
    +
    + + + + +
    +
    ); + } + + rawJsonResponse () { + return ( +
    + JSON Response +
    {JSON.stringify(this.props.explainPlan, null, ' ')}
    +
    + ); + } + + isKeyRangeUnbounded(mrargs) { + if (mrargs) { + const { start_key, end_key } = mrargs; + + // When an index is sorted descending, + // start_key and end_key are reversed. + // This detects a maximum index scan range (between null/[] and "") + // by concatenatings the start/end keys, + // removing any null/empty elements, + // and testing that we have a single element "" left. + const max_range = [start_key, end_key].flat().filter(n => n); + return max_range.length === 1 && max_range[0] === ""; + } + return false; + } + + parsedContent () { + const {index, covering, mrargs} = this.props.explainPlan; + if (!index) { + return "Invalid explain plan"; + } + + let extraInfo = this.isKeyRangeUnbounded(mrargs) ? + Full index scan detected. Query time will degrade as documents are added to the index. : null; + + // Matching index + let matchingIndex = ; + + // Candidates + const {index_candidates} = this.props.explainPlan; + + //only show suitable/unsuitable indexes if index_candidates is defined + const usableIndexPanelHeader = index_candidates ? + Suitable Indexes + : null; + const notUsableIndexPanelHeader = index_candidates ? + Unsuitable Indexes + : null; + + let usableIndexPanelList = null; + let notUsableIndexPanelList = null; + if (index_candidates && index_candidates.length > 0) { + const sortedCandidates = this.pickUsableIndexes(index_candidates); + usableIndexPanelList = sortedCandidates.map((candidate) => { + const { index, analysis } = candidate; + const { reasons, covering } = analysis; + const reason = reasons[0].name; + return ; + }); + + const sortedNotUsable = this.pickNotUsableIndexes(index_candidates); + notUsableIndexPanelList = sortedNotUsable.map((candidate) => { + const { index, analysis } = candidate; + const { reasons, covering } = analysis; + const reason = reasons[0].name; + return ; + }); + } + + if ((usableIndexPanelList && usableIndexPanelList.length === 0) || (index_candidates && !usableIndexPanelList)) { + usableIndexPanelList =
    + No other suitable indexes found. +
    ; + } + if ((notUsableIndexPanelList && notUsableIndexPanelList.length === 0) || (index_candidates && !notUsableIndexPanelList)) { + notUsableIndexPanelList =
    + No other indexes found. +
    ; + } + + return ( + <> + + Selected Index + + {matchingIndex} +
    + {usableIndexPanelHeader} + {usableIndexPanelList} +
    + {notUsableIndexPanelHeader} + {notUsableIndexPanelList} + + ); + } + render () { return (
    -
    {JSON.stringify(this.props.explainPlan, null, ' ')}
    + + {this.toggleButtons()} + {this.props.viewFormat === 'parsed' ? this.parsedContent() : null} + {this.props.viewFormat === 'json' ? this.rawJsonResponse() : null}
    ); } } ExplainPage.propTypes = { - explainPlan: PropTypes.object.isRequired + explainPlan: PropTypes.object.isRequired, + viewFormat: PropTypes.string.isRequired, + isReasonsModalVisible: PropTypes.bool.isRequired, + onViewFormatChange: PropTypes.func.isRequired, + resetState: PropTypes.func.isRequired, + hideReasonsModal: PropTypes.func.isRequired, + showReasonsModal: PropTypes.func.isRequired, +}; + +const InfoIcon = ({tooltip_content}) => { + const tooltip = {tooltip_content}; + return ( + + + + ); }; diff --git a/app/addons/documents/mango/components/ExplainPageContainer.js b/app/addons/documents/mango/components/ExplainPageContainer.js new file mode 100644 index 000000000..4b84a0a63 --- /dev/null +++ b/app/addons/documents/mango/components/ExplainPageContainer.js @@ -0,0 +1,48 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +import { connect } from 'react-redux'; + +import ExplainPage from './ExplainPage'; +import Actions from '../mango.actions'; + +const mapStateToProps = ({ mangoQuery }, ownProps) => { + return { + explainPlan: ownProps.explainPlan, + viewFormat: mangoQuery.explainViewFormat, + isReasonsModalVisible: mangoQuery.isReasonsModalVisible + }; +}; + +const mapDispatchToProps = (dispatch) => { + return { + onViewFormatChange: (options) => { + dispatch(Actions.setExplainViewFormat(options)); + }, + resetState: () => { + dispatch(Actions.resetState()); + }, + hideReasonsModal: () => { + dispatch(Actions.hideReasonsModal()); + }, + showReasonsModal: () => { + dispatch(Actions.showReasonsModal()); + }, + }; +}; + +const ExplainPageContainer = connect( + mapStateToProps, + mapDispatchToProps +)(ExplainPage); + +export default ExplainPageContainer; diff --git a/app/addons/documents/mango/components/ExplainReasonsLegendModal.js b/app/addons/documents/mango/components/ExplainReasonsLegendModal.js new file mode 100644 index 000000000..0dd4aa247 --- /dev/null +++ b/app/addons/documents/mango/components/ExplainReasonsLegendModal.js @@ -0,0 +1,83 @@ +// 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 ExplainReasonsLegendModal({isVisible, onHide}) { + return + + Reasons for Unsuitable Indexes + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    CodeExplanation
    alphabetically_comes_afterIf multiple indexes are equally valid, the first index sorted alphanumerically by name is selected.
    empty_selectorThe selector is empty.
    excluded_by_userIndex did not match the specified “use_index” value.
    field_mismatchThe index does contain the fields required to answer the query.
    less_overlapThe index has less field coverage than the selected index.
    is_partialPartial indexes cannot be selected automatically.
    needs_text_search Index did not match the specified “text” operator.
    scope_mismatchThe index scope does not match the query scope (e.g. the query is against a partition and the index is global).
    sort_order_mismatchThe fields in the index are not sorted in the order as required by the query.
    too_many_fieldsThe index has more fields than the chosen one.
    unfavored_typeAn index with a preferred type was selected. Order of index type preference: json, text, special (_all_docs).
    +
    +
    + + + +
    ; +} diff --git a/app/addons/documents/mango/components/IndexFields.js b/app/addons/documents/mango/components/IndexFields.js new file mode 100644 index 000000000..ff885a458 --- /dev/null +++ b/app/addons/documents/mango/components/IndexFields.js @@ -0,0 +1,40 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +import PropTypes from 'prop-types'; +import React from "react"; + +export default function IndexFields ({fields, isTextIndex}) { + if (!fields) { + return null; + } + if (fields.length === 0) { + return
    {isTextIndex ? 'All fields' : 'No fields'}
    ; + } + + const fieldsList = fields.map((field) => { + const fieldName = Object.getOwnPropertyNames(field)[0]; + const sortOrder = field[fieldName]; + const icon = sortOrder === 'asc' ? + : + ; + return
    {fieldName}{icon}
    ; + // return {fieldEntry}; + }); + + return
    {fieldsList}
    ; +} + +IndexFields.propTypes = { + fields: PropTypes.arrayOf(PropTypes.object).isRequired, + isTextIndex: PropTypes.bool, +}; diff --git a/app/addons/documents/mango/components/IndexPanel.js b/app/addons/documents/mango/components/IndexPanel.js new file mode 100644 index 000000000..702ea70ed --- /dev/null +++ b/app/addons/documents/mango/components/IndexPanel.js @@ -0,0 +1,77 @@ +// 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 PropTypes from 'prop-types'; +import React from "react"; +import IndexFields from "./IndexFields"; +import ReactComponents from '../../../components/react-components'; + +function ReasonValue({reason, onClick}) { + const _onClick = (ev) => { + ev.preventDefault(); + onClick(); + }; + return {formatReason(reason)}; +} + +export default function IndexPanel ({index, isWinner, reason, covering, onReasonClick, extraInfo}) { + const columnClass = 'col-md-12 col-lg-3 mb-4 mb-lg-0'; + const tags = [ + index.partitioned ? 'partitioned' : 'global', + ]; + if (covering) { + tags.push('covering'); + } + const tagExplanations = { + 'partitioned': 'Index queries over a single data partition', + 'global': 'Index queries over all data within database', + 'covering': 'Index can return all data needed by the query without having to load document bodies' + }; + + return ( +
    +
    + {index.type}: {index.name} +
    + {index.ddoc ? (<>{index.ddoc}
    ) : null} +
    +
    + {}} /> +
    +
    + +
    + {isWinner ?
    {extraInfo ? extraInfo : ' '}
    : +
    + {reason ? : reason not available} +
    } +
    + ); +} + +function formatReason(reason) { + if (typeof reason === 'string') { + return reason; + } else if (reason && reason.length > 0) { + return reason.join(', '); + } + return 'n/a'; +} + +IndexPanel.propTypes = { + index: PropTypes.object.isRequired, + onReasonClick: PropTypes.func.isRequired, + reason: PropTypes.string, + covering: PropTypes.bool, + isWinner: PropTypes.bool, // 'true' if this is the winning index from the explain response + extraInfo: PropTypes.element, // extra info to display in the last column +}; diff --git a/app/addons/documents/mango/mango.actions.js b/app/addons/documents/mango/mango.actions.js index d0ee6387d..49fa1e25c 100644 --- a/app/addons/documents/mango/mango.actions.js +++ b/app/addons/documents/mango/mango.actions.js @@ -129,6 +129,31 @@ export default { }); }); }; - } + }, + + setExplainViewFormat: function (options) { + return { + type: ActionTypes.SET_EXPLAIN_VIEW_FORMAT, + options: options + }; + }, + + hideReasonsModal: function () { + return { + type: ActionTypes.HIDE_EXPLAIN_REASONS_MODAL + }; + }, + + showReasonsModal: function () { + return { + type: ActionTypes.SHOW_EXPLAIN_REASONS_MODAL + }; + }, + + resetState: function () { + return { + type: ActionTypes.EXPLAIN_RESULTS_REDUX_RESET_STATE + }; + }, }; diff --git a/app/addons/documents/mango/mango.actiontypes.js b/app/addons/documents/mango/mango.actiontypes.js index f1d87acb3..d25b57f2e 100644 --- a/app/addons/documents/mango/mango.actiontypes.js +++ b/app/addons/documents/mango/mango.actiontypes.js @@ -19,4 +19,8 @@ export default { 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', + SET_EXPLAIN_VIEW_FORMAT: 'SET_EXPLAIN_VIEW_FORMAT', + EXPLAIN_RESULTS_REDUX_RESET_STATE: 'EXPLAIN_RESULTS_REDUX_RESET_STATE', + HIDE_EXPLAIN_REASONS_MODAL: 'HIDE_EXPLAIN_REASONS_MODAL', + SHOW_EXPLAIN_REASONS_MODAL: 'SHOW_EXPLAIN_REASONS_MODAL' }; diff --git a/app/addons/documents/mango/mango.components.js b/app/addons/documents/mango/mango.components.js index 94dce2872..34dff18e4 100644 --- a/app/addons/documents/mango/mango.components.js +++ b/app/addons/documents/mango/mango.components.js @@ -12,10 +12,10 @@ import MangoQueryEditorContainer from "./components/MangoQueryEditorContainer"; import MangoIndexEditorContainer from "./components/MangoIndexEditorContainer"; -import ExplainPage from "./components/ExplainPage"; +import ExplainPageContainer from "./components/ExplainPageContainer"; export default { MangoIndexEditorContainer, MangoQueryEditorContainer, - ExplainPage + ExplainPageContainer }; diff --git a/app/addons/documents/mango/mango.reducers.js b/app/addons/documents/mango/mango.reducers.js index 7fd05774b..c965f55a0 100644 --- a/app/addons/documents/mango/mango.reducers.js +++ b/app/addons/documents/mango/mango.reducers.js @@ -81,6 +81,8 @@ const initialState = { historyKey: 'default', queryIndexTemplates: getDefaultQueryIndexTemplates(), executionStatsSupported: false, + explainViewFormat: 'parsed', + isReasonsModalVisible: false }; const loadQueryHistory = (databaseName) => { @@ -206,6 +208,31 @@ export default function mangoquery(state = initialState, action) { executionStatsSupported: true, }; + case ActionTypes.SET_EXPLAIN_VIEW_FORMAT: + return { + ...state, + explainViewFormat: options, + }; + + case ActionTypes.SHOW_EXPLAIN_REASONS_MODAL: + return { + ...state, + isReasonsModalVisible: true + }; + + case ActionTypes.HIDE_EXPLAIN_REASONS_MODAL: + return { + ...state, + isReasonsModalVisible: false + }; + + case ActionTypes.EXPLAIN_RESULTS_REDUX_RESET_STATE: + return { + ...state, + explainPlan: undefined, + isReasonsModalVisible: false + }; + default: return state; } diff --git a/app/addons/documents/mangolayout.js b/app/addons/documents/mangolayout.js index 70b3e93b7..c44900a19 100644 --- a/app/addons/documents/mangolayout.js +++ b/app/addons/documents/mangolayout.js @@ -136,7 +136,7 @@ export const MangoContent = ({ edit, designDocs, explainPlan, databaseName, fetc queryDocs={queryDocs} />; if (explainPlan) { - resultsPage = ; + resultsPage = ; mangoFooter = null; } diff --git a/app/addons/documents/tests/nightwatch/explainPage.js b/app/addons/documents/tests/nightwatch/explainPage.js new file mode 100644 index 000000000..83585d4d0 --- /dev/null +++ b/app/addons/documents/tests/nightwatch/explainPage.js @@ -0,0 +1,46 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +module.exports = { + + 'Explain mango query results': function (client) { + var waitTime = 10000, + newDatabaseName = client.globals.testDatabaseName, + baseUrl = client.options.launch_url; + + client + .populateDatabase(newDatabaseName) + .loginToGUI() + .url(baseUrl + '/#database/' + newDatabaseName + '/_find') + .waitForElementPresent('.watermark-logo', waitTime, false) + .execute('\ + var json = \'{\ + "selector": {\ + "ente_ente_mango_ananas": {"$gt": null}\ + }\ + }\';\ + var editor = ace.edit("query-field");\ + editor.getSession().setValue(json);\ + ') + .clickWhenVisible('#create-index-btn') + .clickWhenVisible('#explain-btn') + .waitForElementPresent('#explain-parsed-view', waitTime, false) + .assert.textContains('#explain-plan-wrapper', 'Selected Index') + .assert.textContains('#explain-plan-wrapper', 'rocko-artischockbert') + + .clickWhenVisible('#explain-json-view') + .waitForElementPresent('.prettyprint', waitTime, false) + .assert.textContains('#explain-plan-wrapper', 'dbname') + .assert.textContains('#explain-plan-wrapper', 'rocko-artischockbert') + .end(); + } +}; diff --git a/jest-config.json b/jest-config.json index 934b8bf22..8ad76086f 100644 --- a/jest-config.json +++ b/jest-config.json @@ -1,6 +1,6 @@ { "roots": ["app"], - "testPathIgnorePatterns": ["/node_modules/", "stub", "fakeActiveTaskResponse", "fixtures"], + "testPathIgnorePatterns": ["/node_modules/", "stub", "fakeActiveTaskResponse", "fixtures", "sampleexplainplan"], "setupFiles": ["./jest-shim.js"], "setupFilesAfterEnv": ["./jest-setup.js"],