app/addons/documents/mango/components/ExplainPage.js (163 lines of code) (raw):
// 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, { 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 () {
prettyPrint();
}
componentDidUpdate () {
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 (
<div className="row mb-4">
<div className="col">
<ButtonGroup aria-label='Explain format selector' >
<Button type="button"
id="explain-parsed-view"
active={this.props.viewFormat === 'parsed'}
onClick={() => {this.props.onViewFormatChange('parsed');}}
variant="cf-secondary">Parsed</Button>
<Button type="button"
id="explain-json-view"
active={this.props.viewFormat === 'json'}
onClick={() => {this.props.onViewFormatChange('json');}}
variant="cf-secondary">JSON</Button>
</ButtonGroup>
</div>
</div>);
}
rawJsonResponse () {
return (
<div className="explain-json-response">
<span className="explain-plan-section-title">JSON Response</span>
<pre className="prettyprint">{JSON.stringify(this.props.explainPlan, null, ' ')}</pre>
</div>
);
}
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 "<MAX>")
// by concatenatings the start/end keys,
// removing any null/empty elements,
// and testing that we have a single element "<MAX>" left.
const max_range = [start_key, end_key].flat().filter(n => n);
return max_range.length === 1 && max_range[0] === "<MAX>";
}
return false;
}
parsedContent () {
const {index, covering, mrargs} = this.props.explainPlan;
if (!index) {
return "Invalid explain plan";
}
let extraInfo = this.isKeyRangeUnbounded(mrargs) ?
<span className='index-extra-info'><span className='fonticon-attention-circled'></span>Full index scan detected. Query time will degrade as documents are added to the index.</span> : null;
// Matching index
let matchingIndex = <IndexPanel index={index} isWinner={true} covering={covering} onReasonClick={this.props.showReasonsModal} extraInfo={extraInfo}/>;
// Candidates
const {index_candidates} = this.props.explainPlan;
//only show suitable/unsuitable indexes if index_candidates is defined
const usableIndexPanelHeader = index_candidates ? <span className="explain-plan-section-title">
Suitable Indexes<InfoIcon tooltip_content={"Other suitable indexes that were not chosen"}/>
</span> : null;
const notUsableIndexPanelHeader = index_candidates ? <span className="explain-plan-section-title">
Unsuitable Indexes<InfoIcon tooltip_content={"Indexes that do not match the given query"}/>
</span> : 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 <IndexPanel key={`${index.ddoc}"-"${index.name}`} isWinner={false} onReasonClick={this.props.showReasonsModal}
index={index} reason={reason} covering={covering}/>;
});
const sortedNotUsable = this.pickNotUsableIndexes(index_candidates);
notUsableIndexPanelList = sortedNotUsable.map((candidate) => {
const { index, analysis } = candidate;
const { reasons, covering } = analysis;
const reason = reasons[0].name;
return <IndexPanel key={`${index.ddoc}"-"${index.name}`} isWinner={false} onReasonClick={this.props.showReasonsModal}
index={index} reason={reason} covering={covering}/>;
});
}
if ((usableIndexPanelList && usableIndexPanelList.length === 0) || (index_candidates && !usableIndexPanelList)) {
usableIndexPanelList = <div className='explain-index-panel'>
No other suitable indexes found.
</div>;
}
if ((notUsableIndexPanelList && notUsableIndexPanelList.length === 0) || (index_candidates && !notUsableIndexPanelList)) {
notUsableIndexPanelList = <div className='explain-index-panel'>
No other indexes found.
</div>;
}
return (
<>
<span className="explain-plan-section-title">
Selected Index<InfoIcon tooltip_content={"The index used when running the query"}/>
</span>
{matchingIndex}
<br/>
{usableIndexPanelHeader}
{usableIndexPanelList}
<br/>
{notUsableIndexPanelHeader}
{notUsableIndexPanelList}
</>
);
}
render () {
return (
<div id="explain-plan-wrapper">
<ExplainReasonsLegendModal isVisible={this.props.isReasonsModalVisible} onHide={this.props.hideReasonsModal}/>
{this.toggleButtons()}
{this.props.viewFormat === 'parsed' ? this.parsedContent() : null}
{this.props.viewFormat === 'json' ? this.rawJsonResponse() : null}
</div>
);
}
}
ExplainPage.propTypes = {
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 id="graveyard-tooltip">{tooltip_content}</Tooltip>;
return (
<OverlayTrigger placement="top" overlay={tooltip}>
<i className="fonticon fonticon-info-circled"></i>
</OverlayTrigger>
);
};