Improve feedback in Discover (#16771)

* Add Painless scripted field error callout to Discover. Remove recursive beginSegmentedFetch call.
* Add getDocLink service. EUIfy Discover 'no results' state.
* Rename initSegmentedFetch to handleSegmentedFetch to differentiate it from beginSegmentedFetch.
This commit is contained in:
CJ Cenizal 2018-03-22 12:48:19 -07:00 committed by GitHub
parent aeaf57dd97
commit a7147f2ca7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 1175 additions and 192 deletions

View file

@ -0,0 +1,63 @@
import 'ngreact';
import React, { Fragment } from 'react';
import { uiModules } from 'ui/modules';
import chrome from 'ui/chrome';
import {
EuiFlexGroup,
EuiFlexItem,
EuiCallOut,
EuiCodeBlock,
EuiSpacer,
} from '@elastic/eui';
import './fetch_error.less';
const DiscoverFetchError = ({ fetchError }) => {
if (!fetchError) {
return null;
}
let body;
if (fetchError.lang === 'painless') {
const managementUrl = chrome.getNavLinkById('kibana:management').url;
const url = `${managementUrl}/kibana/indices`;
body = (
<p>
You can address this error by editing the &lsquo;{fetchError.script}&rsquo; field
in <a href={url}>Management &gt; Index Patterns</a>,
under the &ldquo;Scripted fields&rdquo; tab.
</p>
);
}
return (
<Fragment>
<EuiSpacer size="xl" />
<EuiFlexGroup justifyContent="center" data-test-subj="discoverFetchError">
<EuiFlexItem grow={false} className="discoverFetchError">
<EuiCallOut
title={fetchError.message}
color="danger"
iconType="cross"
>
{body}
<EuiCodeBlock>
{fetchError.error}
</EuiCodeBlock>
</EuiCallOut>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="xl" />
</Fragment>
);
};
const app = uiModules.get('apps/discover', ['react']);
app.directive('discoverFetchError', reactDirective => reactDirective(DiscoverFetchError));

View file

@ -0,0 +1,3 @@
.discoverFetchError {
max-width: 1000px;
}

View file

@ -0,0 +1 @@
import './fetch_error';

View file

@ -15,7 +15,7 @@ import 'ui/state_management/app_state';
import 'ui/timefilter';
import 'ui/share';
import 'ui/query_bar';
import { toastNotifications } from 'ui/notify';
import { toastNotifications, getPainlessError } from 'ui/notify';
import { VisProvider } from 'ui/vis';
import { BasicResponseHandlerProvider } from 'ui/vis/response_handlers/basic';
import { DocTitleProvider } from 'ui/doc_title';
@ -31,6 +31,8 @@ import { migrateLegacyQuery } from 'ui/utils/migrateLegacyQuery';
import { FilterManagerProvider } from 'ui/filter_manager';
import { SavedObjectsClientProvider } from 'ui/saved_objects';
import { recentlyAccessed } from 'ui/persisted_log';
import { getDocLink } from 'ui/documentation_links';
import '../components/fetch_error';
const app = uiModules.get('apps/discover', [
'kibana/notify',
@ -57,14 +59,14 @@ uiRoutes
})
.then(({ savedObjects }) => {
/**
* In making the indexPattern modifiable it was placed in appState. Unfortunately,
* the load order of AppState conflicts with the load order of many other things
* so in order to get the name of the index we should use, and to switch to the
* default if necessary, we parse the appState with a temporary State object and
* then destroy it immediatly after we're done
*
* @type {State}
*/
* In making the indexPattern modifiable it was placed in appState. Unfortunately,
* the load order of AppState conflicts with the load order of many other things
* so in order to get the name of the index we should use, and to switch to the
* default if necessary, we parse the appState with a temporary State object and
* then destroy it immediatly after we're done
*
* @type {State}
*/
const state = new State('_a', {});
const specified = !!state.index;
@ -135,6 +137,7 @@ function discoverController(
location: 'Discover'
});
$scope.getDocLink = getDocLink;
$scope.intervalOptions = Private(AggTypesBucketsIntervalOptionsProvider);
$scope.showInterval = false;
$scope.minimumVisibleRows = 50;
@ -292,15 +295,6 @@ function discoverController(
};
const init = _.once(function () {
const showTotal = 5;
$scope.failuresShown = showTotal;
$scope.showAllFailures = function () {
$scope.failuresShown = $scope.failures.length;
};
$scope.showLessFailures = function () {
$scope.failuresShown = showTotal;
};
stateMonitor = stateMonitorFactory.create($state, getStateDefaults());
stateMonitor.onChange((status) => {
$appStatus.dirty = status.dirty || !savedSearch.id;
@ -453,6 +447,8 @@ function discoverController(
// ignore requests to fetch before the app inits
if (!init.complete) return;
$scope.fetchError = undefined;
$scope.updateTime();
$scope.updateDataSource()
@ -470,8 +466,9 @@ function discoverController(
};
function initSegmentedFetch(segmented) {
function handleSegmentedFetch(segmented) {
function flushResponseData() {
$scope.fetchError = undefined;
$scope.hits = 0;
$scope.faliures = [];
$scope.rows = [];
@ -584,10 +581,17 @@ function discoverController(
function beginSegmentedFetch() {
$scope.searchSource.onBeginSegmentedFetch(initSegmentedFetch)
$scope.searchSource.onBeginSegmentedFetch(handleSegmentedFetch)
.catch((error) => {
notify.error(error);
// Restart.
const fetchError = getPainlessError(error);
if (fetchError) {
$scope.fetchError = fetchError;
} else {
notify.error(error);
}
// Restart. This enables auto-refresh functionality.
beginSegmentedFetch();
});
}

View file

@ -0,0 +1,393 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`DiscoverNoResults props queryLanguage supports lucene and renders doc link 1`] = `
Array [
<div
class="euiSpacer euiSpacer--xl"
/>,
<div
class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--justifyContentCenter euiFlexGroup--responsive"
>
<div
class="euiFlexItem euiFlexItem--flexGrowZero discoverNoResults"
>
<div
class="euiCallOut euiCallOut--warning"
data-test-subj="discoverNoResults"
>
<div
class="euiCallOutHeader"
>
<svg
aria-hidden="true"
class="euiIcon euiCallOutHeader__icon euiIcon--medium"
height="16"
viewBox="0 0 16 16"
width="16"
xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
>
<defs>
<path
d="M13.6 12.186l-1.357-1.358c-.025-.025-.058-.034-.084-.056.53-.794.84-1.746.84-2.773a4.977 4.977 0 0 0-.84-2.772c.026-.02.059-.03.084-.056L13.6 3.813a6.96 6.96 0 0 1 0 8.373zM8 15A6.956 6.956 0 0 1 3.814 13.6l1.358-1.358c.025-.025.034-.057.055-.084C6.02 12.688 6.974 13 8 13a4.978 4.978 0 0 0 2.773-.84c.02.026.03.058.056.083l1.357 1.358A6.956 6.956 0 0 1 8 15zm-5.601-2.813a6.963 6.963 0 0 1 0-8.373l1.359 1.358c.024.025.057.035.084.056A4.97 4.97 0 0 0 3 8c0 1.027.31 1.98.842 2.773-.027.022-.06.031-.084.056l-1.36 1.358zm5.6-.187A4 4 0 1 1 8 4a4 4 0 0 1 0 8zM8 1c1.573 0 3.019.525 4.187 1.4l-1.357 1.358c-.025.025-.035.057-.056.084A4.979 4.979 0 0 0 8 3a4.979 4.979 0 0 0-2.773.842c-.021-.027-.03-.059-.055-.084L3.814 2.4A6.957 6.957 0 0 1 8 1zm0-1a8.001 8.001 0 1 0 .003 16.002A8.001 8.001 0 0 0 8 0z"
id="help-a"
/>
</defs>
<use
fill-rule="evenodd"
href="#help-a"
/>
</svg>
<span
class="euiCallOutHeader__title"
>
No results match your search criteria
</span>
</div>
</div>
<div
class="euiSpacer euiSpacer--xl"
/>
<div
class="euiText"
>
<h3>
Refine your query
</h3>
<p>
The search bar at the top uses Elasticsearchs support for Lucene
<a
class="euiLink euiLink--primary"
href="documentation-link"
rel="noopener noreferrer"
target="_blank"
>
Query String syntax
</a>
. Here are some examples of how you can search for web server logs that have been parsed into a few fields.
</p>
</div>
<div
class="euiSpacer euiSpacer--m"
/>
<dl
class="euiDescriptionList euiDescriptionList--column"
>
<dt
class="euiDescriptionList__title"
>
<div
class="euiText"
>
<strong>
Find requests that contain the number 200, in any field
</strong>
</div>
</dt>
<dd
class="euiDescriptionList__description"
>
<span
class="euiCodeBlock euiCodeBlock--fontSmall euiCodeBlock--paddingLarge euiCodeBlock--inline"
>
<code
class="euiCodeBlock__code"
>
200
</code>
</span>
</dd>
<dt
class="euiDescriptionList__title"
>
<div
class="euiText"
>
<strong>
Find 200 in the status field
</strong>
</div>
</dt>
<dd
class="euiDescriptionList__description"
>
<span
class="euiCodeBlock euiCodeBlock--fontSmall euiCodeBlock--paddingLarge euiCodeBlock--inline"
>
<code
class="euiCodeBlock__code"
>
status:200
</code>
</span>
</dd>
<dt
class="euiDescriptionList__title"
>
<div
class="euiText"
>
<strong>
Find all status codes between 400-499
</strong>
</div>
</dt>
<dd
class="euiDescriptionList__description"
>
<span
class="euiCodeBlock euiCodeBlock--fontSmall euiCodeBlock--paddingLarge euiCodeBlock--inline"
>
<code
class="euiCodeBlock__code"
>
status:[400 TO 499]
</code>
</span>
</dd>
<dt
class="euiDescriptionList__title"
>
<div
class="euiText"
>
<strong>
Find status codes 400-499 with the extension php
</strong>
</div>
</dt>
<dd
class="euiDescriptionList__description"
>
<span
class="euiCodeBlock euiCodeBlock--fontSmall euiCodeBlock--paddingLarge euiCodeBlock--inline"
>
<code
class="euiCodeBlock__code"
>
status:[400 TO 499] AND extension:PHP
</code>
</span>
</dd>
<dt
class="euiDescriptionList__title"
>
<div
class="euiText"
>
<strong>
Find status codes 400-499 with the extension php or html
</strong>
</div>
</dt>
<dd
class="euiDescriptionList__description"
>
<span
class="euiCodeBlock euiCodeBlock--fontSmall euiCodeBlock--paddingLarge euiCodeBlock--inline"
>
<code
class="euiCodeBlock__code"
>
status:[400 TO 499] AND (extension:php OR extension:html)
</code>
</span>
</dd>
</dl>
<div
class="euiSpacer euiSpacer--xl"
/>
</div>
</div>,
]
`;
exports[`DiscoverNoResults props shardFailures renders failures list 1`] = `
Array [
<div
class="euiSpacer euiSpacer--xl"
/>,
<div
class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--justifyContentCenter euiFlexGroup--responsive"
>
<div
class="euiFlexItem euiFlexItem--flexGrowZero discoverNoResults"
>
<div
class="euiCallOut euiCallOut--warning"
data-test-subj="discoverNoResults"
>
<div
class="euiCallOutHeader"
>
<svg
aria-hidden="true"
class="euiIcon euiCallOutHeader__icon euiIcon--medium"
height="16"
viewBox="0 0 16 16"
width="16"
xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
>
<defs>
<path
d="M13.6 12.186l-1.357-1.358c-.025-.025-.058-.034-.084-.056.53-.794.84-1.746.84-2.773a4.977 4.977 0 0 0-.84-2.772c.026-.02.059-.03.084-.056L13.6 3.813a6.96 6.96 0 0 1 0 8.373zM8 15A6.956 6.956 0 0 1 3.814 13.6l1.358-1.358c.025-.025.034-.057.055-.084C6.02 12.688 6.974 13 8 13a4.978 4.978 0 0 0 2.773-.84c.02.026.03.058.056.083l1.357 1.358A6.956 6.956 0 0 1 8 15zm-5.601-2.813a6.963 6.963 0 0 1 0-8.373l1.359 1.358c.024.025.057.035.084.056A4.97 4.97 0 0 0 3 8c0 1.027.31 1.98.842 2.773-.027.022-.06.031-.084.056l-1.36 1.358zm5.6-.187A4 4 0 1 1 8 4a4 4 0 0 1 0 8zM8 1c1.573 0 3.019.525 4.187 1.4l-1.357 1.358c-.025.025-.035.057-.056.084A4.979 4.979 0 0 0 8 3a4.979 4.979 0 0 0-2.773.842c-.021-.027-.03-.059-.055-.084L3.814 2.4A6.957 6.957 0 0 1 8 1zm0-1a8.001 8.001 0 1 0 .003 16.002A8.001 8.001 0 0 0 8 0z"
id="help-a"
/>
</defs>
<use
fill-rule="evenodd"
href="#help-a"
/>
</svg>
<span
class="euiCallOutHeader__title"
>
No results match your search criteria
</span>
</div>
</div>
<div
class="euiSpacer euiSpacer--xl"
/>
<div
class="euiText"
>
<h3>
Address shard failures
</h3>
<p>
The following shard failures occurred:
</p>
<div>
<div
class="euiText euiText--extraSmall"
>
<strong>
Index A
</strong>
, shard 1
</div>
<div
class="euiSpacer euiSpacer--s"
/>
<div
class="euiCodeBlock euiCodeBlock--fontSmall euiCodeBlock--paddingSmall"
>
<pre
class="euiCodeBlock__pre"
>
<code
class="euiCodeBlock__code"
>
Awful error
</code>
</pre>
</div>
<div
class="euiSpacer euiSpacer--l"
/>
</div>
<div>
<div
class="euiText euiText--extraSmall"
>
<strong>
Index B
</strong>
, shard 2
</div>
<div
class="euiSpacer euiSpacer--s"
/>
<div
class="euiCodeBlock euiCodeBlock--fontSmall euiCodeBlock--paddingSmall"
>
<pre
class="euiCodeBlock__pre"
>
<code
class="euiCodeBlock__code"
>
Bad error
</code>
</pre>
</div>
</div>
</div>
</div>
</div>,
]
`;
exports[`DiscoverNoResults props timeFieldName renders time range feedback 1`] = `
Array [
<div
class="euiSpacer euiSpacer--xl"
/>,
<div
class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--justifyContentCenter euiFlexGroup--responsive"
>
<div
class="euiFlexItem euiFlexItem--flexGrowZero discoverNoResults"
>
<div
class="euiCallOut euiCallOut--warning"
data-test-subj="discoverNoResults"
>
<div
class="euiCallOutHeader"
>
<svg
aria-hidden="true"
class="euiIcon euiCallOutHeader__icon euiIcon--medium"
height="16"
viewBox="0 0 16 16"
width="16"
xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
>
<defs>
<path
d="M13.6 12.186l-1.357-1.358c-.025-.025-.058-.034-.084-.056.53-.794.84-1.746.84-2.773a4.977 4.977 0 0 0-.84-2.772c.026-.02.059-.03.084-.056L13.6 3.813a6.96 6.96 0 0 1 0 8.373zM8 15A6.956 6.956 0 0 1 3.814 13.6l1.358-1.358c.025-.025.034-.057.055-.084C6.02 12.688 6.974 13 8 13a4.978 4.978 0 0 0 2.773-.84c.02.026.03.058.056.083l1.357 1.358A6.956 6.956 0 0 1 8 15zm-5.601-2.813a6.963 6.963 0 0 1 0-8.373l1.359 1.358c.024.025.057.035.084.056A4.97 4.97 0 0 0 3 8c0 1.027.31 1.98.842 2.773-.027.022-.06.031-.084.056l-1.36 1.358zm5.6-.187A4 4 0 1 1 8 4a4 4 0 0 1 0 8zM8 1c1.573 0 3.019.525 4.187 1.4l-1.357 1.358c-.025.025-.035.057-.056.084A4.979 4.979 0 0 0 8 3a4.979 4.979 0 0 0-2.773.842c-.021-.027-.03-.059-.055-.084L3.814 2.4A6.957 6.957 0 0 1 8 1zm0-1a8.001 8.001 0 1 0 .003 16.002A8.001 8.001 0 0 0 8 0z"
id="help-a"
/>
</defs>
<use
fill-rule="evenodd"
href="#help-a"
/>
</svg>
<span
class="euiCallOutHeader__title"
>
No results match your search criteria
</span>
</div>
</div>
<div
class="euiSpacer euiSpacer--xl"
/>
<div
class="euiText"
>
<h3>
Expand your time range
</h3>
<p>
One or more of the indices youre looking at contains a date field. Your query may not match anything in the current time range, or there may not be any data at all in the currently selected time range. You can try
<button
aria-expanded="false"
class="euiLink euiLink--primary"
data-test-subj="discoverNoResultsTimefilter"
type="button"
>
opening the time picker
</button>
and changing the time range to one which contains data.
</p>
</div>
</div>
</div>,
]
`;

View file

@ -0,0 +1,13 @@
import 'ngreact';
import './no_results.less';
import { uiModules } from 'ui/modules';
import {
DiscoverNoResults,
} from './no_results';
import './timechart';
const app = uiModules.get('apps/discover', ['react']);
app.directive('discoverNoResults', reactDirective => reactDirective(DiscoverNoResults));

View file

@ -1,12 +1,183 @@
import { uiModules } from 'ui/modules';
import noResultsTemplate from '../partials/no_results.html';
import 'ui/directives/documentation_href';
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
uiModules
.get('apps/discover')
.directive('discoverNoResults', function () {
return {
restrict: 'E',
template: noResultsTemplate
};
});
import {
EuiCallOut,
EuiCode,
EuiCodeBlock,
EuiDescriptionList,
EuiFlexGroup,
EuiFlexItem,
EuiLink,
EuiSpacer,
EuiText,
} from '@elastic/eui';
export class DiscoverNoResults extends Component {
static propTypes = {
shardFailures: PropTypes.array,
timeFieldName: PropTypes.string,
queryLanguage: PropTypes.string,
isTimePickerOpen: PropTypes.bool.isRequired,
getDocLink: PropTypes.func.isRequired,
topNavToggle: PropTypes.func.isRequired,
};
onClickTimePickerButton = () => {
this.props.topNavToggle('filter');
};
render() {
const {
shardFailures,
timeFieldName,
queryLanguage,
getDocLink,
isTimePickerOpen,
} = this.props;
let shardFailuresMessage;
if (shardFailures) {
const failures = shardFailures.map((failure, index) => (
<div key={`${failure.index}${failure.shard}${failure.reason}`}>
<EuiText size="xs">
<strong>Index &lsquo;{failure.index}&rsquo;</strong>, shard &lsquo;{failure.shard}&rsquo;
</EuiText>
<EuiSpacer size="s" />
<EuiCodeBlock paddingSize="s">
{failure.reason}
</EuiCodeBlock>
{index < shardFailures.length - 1 ? <EuiSpacer size="l" /> : undefined}
</div>
));
shardFailuresMessage = (
<Fragment>
<EuiSpacer size="xl" />
<EuiText>
<h3>
Address shard failures
</h3>
<p>
The following shard failures occurred:
</p>
{failures}
</EuiText>
</Fragment>
);
}
let timeFieldMessage;
if (timeFieldName) {
timeFieldMessage = (
<Fragment>
<EuiSpacer size="xl" />
<EuiText>
<h3>
Expand your time range
</h3>
<p>
One or more of the indices you&rsquo;re looking at contains a date field. Your query may
not match anything in the current time range, or there may not be any data at all in
the currently selected time range. You can try {(
<EuiLink
data-test-subj="discoverNoResultsTimefilter"
onClick={this.onClickTimePickerButton}
aria-expanded={isTimePickerOpen}
>
opening the time picker
</EuiLink>
)} and changing the time range to one which contains data.
</p>
</EuiText>
</Fragment>
);
}
let luceneQueryMessage;
if (queryLanguage === 'lucene') {
const searchExamples = [{
description: <EuiCode>200</EuiCode>,
title: <EuiText><strong>Find requests that contain the number 200, in any field</strong></EuiText>,
}, {
description: <EuiCode>status:200</EuiCode>,
title: <EuiText><strong>Find 200 in the status field</strong></EuiText>,
}, {
description: <EuiCode>status:[400 TO 499]</EuiCode>,
title: <EuiText><strong>Find all status codes between 400-499</strong></EuiText>,
}, {
description: <EuiCode>status:[400 TO 499] AND extension:PHP</EuiCode>,
title: <EuiText><strong>Find status codes 400-499 with the extension php</strong></EuiText>,
}, {
description: <EuiCode>status:[400 TO 499] AND (extension:php OR extension:html)</EuiCode>,
title: <EuiText><strong>Find status codes 400-499 with the extension php or html</strong></EuiText>,
}];
luceneQueryMessage = (
<Fragment>
<EuiSpacer size="xl" />
<EuiText>
<h3>
Refine your query
</h3>
<p>
The search bar at the top uses Elasticsearch&rsquo;s support for Lucene {(
<EuiLink
target="_blank"
href={getDocLink('query.luceneQuerySyntax')}
>
Query String syntax
</EuiLink>
)}. Here are some examples of how you can search for web server logs that have been
parsed into a few fields.
</p>
</EuiText>
<EuiSpacer size="m" />
<EuiDescriptionList
type="column"
listItems={searchExamples}
/>
<EuiSpacer size="xl" />
</Fragment>
);
}
return (
<Fragment>
<EuiSpacer size="xl" />
<EuiFlexGroup justifyContent="center">
<EuiFlexItem grow={false} className="discoverNoResults">
<EuiCallOut
title="No results match your search criteria"
color="warning"
iconType="help"
data-test-subj="discoverNoResults"
/>
{shardFailuresMessage}
{timeFieldMessage}
{luceneQueryMessage}
</EuiFlexItem>
</EuiFlexGroup>
</Fragment>
);
}
}

View file

@ -0,0 +1,12 @@
.discoverNoResults {
max-width: 1000px;
}
.discoverNoResultsShardFailure {
display: block;
.euiFlexItem:last-child {
display: block;
flex-grow: 1;
}
}

View file

@ -0,0 +1,116 @@
import React from 'react';
import { render, mount } from 'enzyme';
import sinon from 'sinon';
import { findTestSubject } from '@elastic/eui/lib/test';
import {
DiscoverNoResults,
} from './no_results';
describe('DiscoverNoResults', () => {
describe('props', () => {
describe('shardFailures', () => {
test('renders failures list', () => {
const shardFailures = [{
index: 'A',
shard: '1',
reason: 'Awful error',
}, {
index: 'B',
shard: '2',
reason: 'Bad error',
}];
const component = render(
<DiscoverNoResults
shardFailures={shardFailures}
isTimePickerOpen={false}
topNavToggle={() => {}}
getDocLink={() => ''}
/>
);
expect(component).toMatchSnapshot();
});
});
describe('isTimePickerOpen', () => {
test('false is reflected in the aria-expanded state of the button', () => {
const component = render(
<DiscoverNoResults
timeFieldName="awesome_time_field"
isTimePickerOpen={false}
topNavToggle={() => {}}
getDocLink={() => ''}
/>
);
expect(
component.find('[data-test-subj="discoverNoResultsTimefilter"]')[0].attribs['aria-expanded']
).toBe('false');
});
test('true is reflected in the aria-expanded state of the button', () => {
const component = render(
<DiscoverNoResults
timeFieldName="awesome_time_field"
isTimePickerOpen={true}
topNavToggle={() => {}}
getDocLink={() => ''}
/>
);
expect(
component.find('[data-test-subj="discoverNoResultsTimefilter"]')[0].attribs['aria-expanded']
).toBe('true');
});
});
describe('timeFieldName', () => {
test('renders time range feedback', () => {
const component = render(
<DiscoverNoResults
timeFieldName="awesome_time_field"
isTimePickerOpen={false}
topNavToggle={() => {}}
getDocLink={() => ''}
/>
);
expect(component).toMatchSnapshot();
});
});
describe('queryLanguage', () => {
test('supports lucene and renders doc link', () => {
const component = render(
<DiscoverNoResults
queryLanguage="lucene"
isTimePickerOpen={false}
topNavToggle={() => {}}
getDocLink={() => 'documentation-link'}
/>
);
expect(component).toMatchSnapshot();
});
});
describe('topNavToggle', () => {
test('is called whe time picker button is clicked', () => {
const topNavToggleSpy = sinon.stub();
const component = mount(
<DiscoverNoResults
timeFieldName="awesome_time_field"
isTimePickerOpen={false}
topNavToggle={topNavToggleSpy}
getDocLink={() => ''}
/>
);
findTestSubject(component, 'discoverNoResultsTimefilter').simulate('click');
sinon.assert.calledOnce(topNavToggleSpy);
});
});
});
});

View file

@ -60,12 +60,28 @@
<div class="discover-wrapper col-md-10">
<div class="discover-content">
<discover-no-results ng-show="resultState === 'none'"></discover-no-results>
<discover-no-results
ng-show="resultState === 'none'"
top-nav-toggle="kbnTopNav.toggle"
is-time-picker-open="kbnTopNav.isCurrent('filter')"
shard-failures="failures"
time-field-name="opts.timefield"
query-language="state.query.language"
get-doc-link="getDocLink"
></discover-no-results>
<!-- loading -->
<div ng-show="resultState === 'loading'">
<div class="discover-overlay">
<div class="euiTitle">
<discover-fetch-error
ng-show="fetchError"
fetch-error="fetchError"
></discover-fetch-error>
<div
ng-hide="fetchError"
class="discover-overlay"
>
<div class="euiTitle" >
<h2>Searching</h2>
</div>
<div class="euiSpacer euiSpacer--m"></div>

View file

@ -1,6 +1,5 @@
import 'plugins/kibana/discover/saved_searches/saved_searches';
import 'plugins/kibana/discover/directives/no_results';
import 'plugins/kibana/discover/directives/timechart';
import 'plugins/kibana/discover/directives';
import 'ui/collapsible_sidebar';
import 'plugins/kibana/discover/components/field_chooser/field_chooser';
import 'plugins/kibana/discover/controllers/discover';

View file

@ -1,110 +0,0 @@
<div class="euiPage">
<div class="euiText" data-test-subj="discoverNoResults">
<h1>No results found <i aria-hidden="true" class="fa fa-meh-o"></i></h1>
<p>
Unfortunately I could not find any results matching your search. I tried really hard. I looked all over the place and frankly, I just couldn't find anything good. Help me, help you. Here are some ideas:
</p>
<div class="shard-failures" ng-show="failures">
<h3>
Shard Failures
</h3>
<p>
The following shard failures ocurred:
</p>
<ul>
<li ng-repeat="failure in failures | limitTo: failuresShown"><strong>Index:</strong> {{failure.index}} <strong>Shard:</strong> {{failure.shard}} <strong>Reason:</strong> {{failure.reason}} </li>
</ul>
<a
ng-click="showAllFailures()"
ng-if="failures.length > failuresShown"
>
Show More
</a>
<a
ng-click="showLessFailures()"
ng-if="failures.length === failuresShown && failures.length > 5"
>
Show Less
</a>
</div>
<div ng-show="opts.timefield">
<h3>
Expand your time range
</h3>
<p>
I see you are looking at an index with a date field. It is possible your query does not match anything in the current time range, or that there is no data at all in the currently selected time range. Click the button below to open the time picker. For future reference you can open the time picker by clicking on the
<button
class="kuiButton kuiButton--primary kuiButton--small"
ng-click="kbnTopNav.toggle('filter')"
aria-expanded="{{kbnTopNav.isCurrent('filter')}}"
data-test-subj="discoverNoResultsTimefilter"
>
<span aria-hidden="true" class="kuiIcon fa-clock-o"></span> time picker
</button>
button in the top right corner of your screen.
</p>
</div>
<div ng-if="state.query.language === 'lucene'">
<h3>
Refine your query
</h3>
<p>
The search bar at the top uses Elasticsearch's support for Lucene <a class="kuiLink" documentation-href="query.luceneQuerySyntax" target="_blank" rel="noopener noreferrer">Query String syntax</a>. Let's say we're searching web server logs that have been parsed into a few fields.
</p>
<h4>Examples</h4>
<p>Find requests that contain the number 200, in any field:</p>
<div class="euiCodeBlock euiCodeBlock--light euiCodeBlock--paddingSmall">
<code class="euiCodeBlock__code">
<pre class="euiCodeBlock__pre">200</pre>
</code>
</div>
<div class="euiSpacer euiSpacer--l"></div>
<p>Or we can search in a specific field. Find 200 in the status field:</p>
<div class="euiCodeBlock euiCodeBlock--light euiCodeBlock--paddingSmall">
<code class="euiCodeBlock__code">
<pre class="euiCodeBlock__pre">status:200</pre>
</code>
</div>
<div class="euiSpacer euiSpacer--l"></div>
<p>Find all status codes between 400-499:</p>
<div class="euiCodeBlock euiCodeBlock--light euiCodeBlock--paddingSmall">
<code class="euiCodeBlock__code">
<pre class="euiCodeBlock__pre">status:[400 TO 499]</pre>
</code>
</div>
<div class="euiSpacer euiSpacer--l"></div>
<p>Find status codes 400-499 with the extension php:</p>
<div class="euiCodeBlock euiCodeBlock--light euiCodeBlock--paddingSmall">
<code class="euiCodeBlock__code">
<pre class="euiCodeBlock__pre">status:[400 TO 499] AND extension:PHP</pre>
</code>
</div>
<div class="euiSpacer euiSpacer--l"></div>
<p>Or HTML</p>
<div class="euiCodeBlock euiCodeBlock--light euiCodeBlock--paddingSmall">
<code class="euiCodeBlock__code">
<pre class="euiCodeBlock__pre">status:[400 TO 499] AND (extension:php OR extension:html)</pre>
</code>
</div>
</div>
</div>
</div>

View file

@ -2,7 +2,7 @@
@import "~ui/styles/local_search.less";
.tab-discover {
overflow-x: hidden;
overflow: hidden;
}
.discover-sidebar-list-header {
@ -149,18 +149,6 @@
}
}
.shard-failures {
color: @discover-shard-failures-color;
background-color: @discover-shard-failures-bg !important;
border: 1px solid;
border-color: @discover-shard-failures-border;
border-radius: @border-radius-base;
padding: 0 20px 20px;
li {
padding-bottom: 5px;
}
}
/**
* 1. Override sidebar-item-title styles.
*/

View file

@ -1,5 +1,4 @@
import { get } from 'lodash';
import { documentationLinks } from '../documentation_links';
import { getDocLink } from '../documentation_links';
import { uiModules } from 'ui/modules';
const module = uiModules.get('kibana');
@ -8,7 +7,7 @@ module.directive('documentationHref', function () {
return {
restrict: 'A',
link: function (scope, element, attributes) {
element.attr('href', get(documentationLinks, attributes.documentationHref));
element.attr('href', getDocLink(attributes.documentationHref));
}
};
});

View file

@ -0,0 +1,4 @@
import { get } from 'lodash';
import { documentationLinks } from './documentation_links';
export const getDocLink = id => get(documentationLinks, id);

View file

@ -1 +1,9 @@
export { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION, documentationLinks } from './documentation_links';
export {
ELASTIC_WEBSITE_URL,
DOC_LINK_VERSION,
documentationLinks,
} from './documentation_links';
export {
getDocLink,
} from './get_doc_link';

View file

@ -43,29 +43,29 @@ export function KbnTopNavControllerProvider($compile) {
}
// change the current key and rerender
setCurrent(key) {
setCurrent = (key) => {
if (key && !this.templates.hasOwnProperty(key)) {
throw new TypeError(`KbnTopNav: unknown template key "${key}"`);
}
this.currentKey = key || null;
this._render();
}
};
// little usability helpers
getCurrent() { return this.currentKey; }
isCurrent(key) { return this.getCurrent() === key; }
open(key) { this.setCurrent(key); }
close(key) { (!key || this.isCurrent(key)) && this.setCurrent(null); }
toggle(key) { this.setCurrent(this.isCurrent(key) ? null : key); }
click(key) { this.handleClick(this.getItem(key)); }
getItem(key) { return this.menuItems.find(i => i.key === key); }
handleClick(menuItem) {
getCurrent = () => { return this.currentKey; };
isCurrent = (key) => { return this.getCurrent() === key; };
open = (key) => { this.setCurrent(key); };
close = (key) => { (!key || this.isCurrent(key)) && this.setCurrent(null); };
toggle = (key) => { this.setCurrent(this.isCurrent(key) ? null : key); };
click = (key) => { this.handleClick(this.getItem(key)); };
getItem = (key) => { return this.menuItems.find(i => i.key === key); };
handleClick = (menuItem) => {
if (menuItem.disableButton()) {
return false;
}
menuItem.run(menuItem, this);
}
};
// apply the defaults to individual options
_applyOptDefault(opt = {}) {
const defaultedOpt = {

View file

@ -1,5 +1,6 @@
export { notify } from './notify';
export { Notifier } from './notifier';
export { getPainlessError } from './lib';
export { fatalError, fatalErrorInternals, addFatalErrorCallback } from './fatal_error';
export { GlobalToastList, toastNotifications } from './toasts';
export { GlobalBannerList, banners } from './banners';

View file

@ -1,16 +1,40 @@
import _ from 'lodash';
const getRootCause = err => _.get(err, 'resp.error.root_cause');
/**
* Utilize the extended error information returned from elasticsearch
* @param {Error|String} err
* @returns {string}
*/
export function formatESMsg(err) {
const rootCause = _.get(err, 'resp.error.root_cause');
export const formatESMsg = (err) => {
const rootCause = getRootCause(err);
if (!rootCause) {
return; //undefined
return;
}
const result = _.pluck(rootCause, 'reason').join('\n');
return result;
}
};
export const getPainlessError = (err) => {
const rootCause = getRootCause(err);
if (!rootCause) {
return;
}
const { lang, script } = rootCause[0];
if (lang !== 'painless') {
return;
}
return {
lang,
script,
message: `Error with Painless scripted field '${script}'`,
error: err.message,
};
};

View file

@ -6,36 +6,36 @@ const has = _.has;
* Formats the error message from an error object, extended elasticsearch
* object or simple string; prepends optional second parameter to the message
* @param {Error|String} err
* @param {String} from - Prefix for message indicating source (optional)
* @param {String} source - Prefix for message indicating source (optional)
* @returns {string}
*/
export function formatMsg(err, from) {
let rtn = '';
if (from) {
rtn += from + ': ';
export function formatMsg(err, source) {
let message = '';
if (source) {
message += source + ': ';
}
const esMsg = formatESMsg(err);
if (typeof err === 'string') {
rtn += err;
message += err;
} else if (esMsg) {
rtn += esMsg;
message += esMsg;
} else if (err instanceof Error) {
rtn += formatMsg.describeError(err);
message += formatMsg.describeError(err);
} else if (has(err, 'status') && has(err, 'data')) {
// is an Angular $http "error object"
if (err.status === -1) {
// status = -1 indicates that the request was failed to reach the server
rtn += 'An HTTP request has failed to connect. ' +
message += 'An HTTP request has failed to connect. ' +
'Please check if the Kibana server is running and that your browser has a working connection, ' +
'or contact your system administrator.';
} else {
rtn += 'Error ' + err.status + ' ' + err.statusText + ': ' + err.data.message;
message += 'Error ' + err.status + ' ' + err.statusText + ': ' + err.data.message;
}
}
return rtn;
return message;
}
formatMsg.describeError = function (err) {

View file

@ -1,3 +1,3 @@
export { formatESMsg } from './format_es_msg';
export { formatESMsg, getPainlessError } from './format_es_msg';
export { formatMsg } from './format_msg';
export { formatStack } from './format_stack';

View file

@ -0,0 +1,25 @@
import expect from 'expect.js';
export default function ({ getService, getPageObjects }) {
const esArchiver = getService('esArchiver');
const testSubjects = getService('testSubjects');
const PageObjects = getPageObjects(['common']);
describe('errors', function describeIndexTests() {
before(async function () {
await esArchiver.load('invalid_scripted_field');
await PageObjects.common.navigateToApp('discover');
});
after(async function () {
await esArchiver.unload('invalid_scripted_field');
});
describe('invalid scripted field error', () => {
it('is rendered', async () => {
const isFetchErrorVisible = await testSubjects.exists('discoverFetchError');
expect(isFetchErrorVisible).to.be(true);
});
});
});
}

View file

@ -12,6 +12,7 @@ export default function ({ getService, loadTestFile }) {
});
loadTestFile(require.resolve('./_discover'));
loadTestFile(require.resolve('./_errors'));
loadTestFile(require.resolve('./_field_data'));
loadTestFile(require.resolve('./_shared_links'));
loadTestFile(require.resolve('./_collapse_expand'));

View file

@ -0,0 +1,252 @@
{
"type": "index",
"value": {
"index": ".kibana",
"settings": {
"index": {
"number_of_shards": "1",
"number_of_replicas": "0"
}
},
"mappings": {
"doc": {
"dynamic": "strict",
"properties": {
"config": {
"dynamic": "true",
"properties": {
"buildNum": {
"type": "keyword"
},
"defaultIndex": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
}
}
},
"dashboard": {
"properties": {
"description": {
"type": "text"
},
"hits": {
"type": "integer"
},
"kibanaSavedObjectMeta": {
"properties": {
"searchSourceJSON": {
"type": "text"
}
}
},
"optionsJSON": {
"type": "text"
},
"panelsJSON": {
"type": "text"
},
"refreshInterval": {
"properties": {
"display": {
"type": "keyword"
},
"pause": {
"type": "boolean"
},
"section": {
"type": "integer"
},
"value": {
"type": "integer"
}
}
},
"timeFrom": {
"type": "keyword"
},
"timeRestore": {
"type": "boolean"
},
"timeTo": {
"type": "keyword"
},
"title": {
"type": "text"
},
"uiStateJSON": {
"type": "text"
},
"version": {
"type": "integer"
}
}
},
"index-pattern": {
"properties": {
"fieldFormatMap": {
"type": "text"
},
"fields": {
"type": "text"
},
"intervalName": {
"type": "keyword"
},
"notExpandable": {
"type": "boolean"
},
"sourceFilters": {
"type": "text"
},
"timeFieldName": {
"type": "keyword"
},
"title": {
"type": "text"
}
}
},
"search": {
"properties": {
"columns": {
"type": "keyword"
},
"description": {
"type": "text"
},
"hits": {
"type": "integer"
},
"kibanaSavedObjectMeta": {
"properties": {
"searchSourceJSON": {
"type": "text"
}
}
},
"sort": {
"type": "keyword"
},
"title": {
"type": "text"
},
"version": {
"type": "integer"
}
}
},
"server": {
"properties": {
"uuid": {
"type": "keyword"
}
}
},
"timelion-sheet": {
"properties": {
"description": {
"type": "text"
},
"hits": {
"type": "integer"
},
"kibanaSavedObjectMeta": {
"properties": {
"searchSourceJSON": {
"type": "text"
}
}
},
"timelion_chart_height": {
"type": "integer"
},
"timelion_columns": {
"type": "integer"
},
"timelion_interval": {
"type": "keyword"
},
"timelion_other_interval": {
"type": "keyword"
},
"timelion_rows": {
"type": "integer"
},
"timelion_sheet": {
"type": "text"
},
"title": {
"type": "text"
},
"version": {
"type": "integer"
}
}
},
"type": {
"type": "keyword"
},
"updated_at": {
"type": "date"
},
"url": {
"properties": {
"accessCount": {
"type": "long"
},
"accessDate": {
"type": "date"
},
"createDate": {
"type": "date"
},
"url": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 2048
}
}
}
}
},
"visualization": {
"properties": {
"description": {
"type": "text"
},
"kibanaSavedObjectMeta": {
"properties": {
"searchSourceJSON": {
"type": "text"
}
}
},
"savedSearchId": {
"type": "keyword"
},
"title": {
"type": "text"
},
"uiStateJSON": {
"type": "text"
},
"version": {
"type": "integer"
},
"visState": {
"type": "text"
}
}
}
}
}
}
}
}