[Discover] Migrate discover.html Angular template to React (#75784)

Co-authored-by: Andrea Del Rio <delrio.andre@gmail.com>
This commit is contained in:
Matthias Wilhelm 2020-09-17 07:51:31 +02:00 committed by GitHub
parent dd8db10253
commit 0dc0f1f34c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
47 changed files with 841 additions and 969 deletions

View file

@ -5,6 +5,11 @@
overflow: hidden;
}
.dscAppContainer {
> * {
position: relative;
}
}
discover-app {
flex-grow: 1;
}
@ -17,9 +22,12 @@ discover-app {
// SASSTODO: replace the z-index value with a variable
.dscWrapper {
padding-left: $euiSizeXL;
padding-right: $euiSizeS;
padding-left: 21px;
z-index: 1;
@include euiBreakpoint('xs', 's', 'm') {
padding-left: $euiSizeS;
}
}
@include euiPanel('.dscWrapper__content');
@ -104,14 +112,51 @@ discover-app {
top: $euiSizeXS;
}
[fixed-scroll] {
.dscTableFixedScroll {
overflow-x: auto;
padding-bottom: 0;
+ .fixed-scroll-scroller {
+ .dscTableFixedScroll__scroller {
position: fixed;
bottom: 0;
overflow-x: auto;
overflow-y: hidden;
}
}
.dscCollapsibleSidebar {
position: relative;
z-index: $euiZLevel1;
.dscCollapsibleSidebar__collapseButton {
position: absolute;
top: 0;
right: -$euiSizeXL + 4;
cursor: pointer;
z-index: -1;
min-height: $euiSizeM;
min-width: $euiSizeM;
padding: $euiSizeXS * .5;
}
&.closed {
width: 0 !important;
border-right-width: 0;
border-left-width: 0;
.dscCollapsibleSidebar__collapseButton {
right: -$euiSizeL + 4;
}
}
}
@include euiBreakpoint('xs', 's', 'm') {
.dscCollapsibleSidebar {
&.closed {
display: none;
}
.dscCollapsibleSidebar__collapseButton {
display: none;
}
}
}

View file

@ -167,132 +167,6 @@ Array [
]
`;
exports[`DiscoverNoResults props shardFailures doesn't render failures list when there are no failures 1`] = `
Array [
<div
class="euiSpacer euiSpacer--xl"
/>,
<div
class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--justifyContentCenter euiFlexGroup--directionRow euiFlexGroup--responsive"
>
<div
class="euiFlexItem euiFlexItem--flexGrowZero dscNoResults"
>
<div
class="euiCallOut euiCallOut--warning"
data-test-subj="discoverNoResults"
>
<div
class="euiCallOutHeader"
>
<div
aria-hidden="true"
class="euiCallOutHeader__icon"
data-euiicon-type="help"
/>
<span
class="euiCallOutHeader__title"
>
No results match your search criteria
</span>
</div>
</div>
</div>
</div>,
]
`;
exports[`DiscoverNoResults props shardFailures renders failures list when there are failures 1`] = `
Array [
<div
class="euiSpacer euiSpacer--xl"
/>,
<div
class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--justifyContentCenter euiFlexGroup--directionRow euiFlexGroup--responsive"
>
<div
class="euiFlexItem euiFlexItem--flexGrowZero dscNoResults"
>
<div
class="euiCallOut euiCallOut--warning"
data-test-subj="discoverNoResults"
>
<div
class="euiCallOutHeader"
>
<div
aria-hidden="true"
class="euiCallOutHeader__icon"
data-euiicon-type="help"
/>
<span
class="euiCallOutHeader__title"
>
No results match your search criteria
</span>
</div>
</div>
<div
class="euiSpacer euiSpacer--xl"
/>
<div
class="euiText euiText--medium"
>
<h3>
Address shard failures
</h3>
<p>
The following shard failures occurred:
</p>
<div>
<div
class="euiText euiText--extraSmall"
>
<strong>
Index &lsquo;A&rsquo;
</strong>
, shard &lsquo;1&rsquo;
</div>
<div
class="euiSpacer euiSpacer--s"
/>
<div>
<pre>
<code>
{"reason":"Awful error"}
</code>
</pre>
</div>
<div
class="euiSpacer euiSpacer--l"
/>
</div>
<div>
<div
class="euiText euiText--extraSmall"
>
<strong>
Index &lsquo;B&rsquo;
</strong>
, shard &lsquo;2&rsquo;
</div>
<div
class="euiSpacer euiSpacer--s"
/>
<div>
<pre>
<code>
{"reason":"Bad error"}
</code>
</pre>
</div>
</div>
</div>
</div>
</div>,
]
`;
exports[`DiscoverNoResults props timeFieldName renders time range feedback 1`] = `
Array [
<div

View file

@ -1,3 +1,2 @@
@import 'no_results';
@import 'histogram';
@import './collapsible_sidebar/index';

View file

@ -1,44 +0,0 @@
// SASSTODO: Can't rename main class
// because it's also the name of the angular directive
.collapsible-sidebar {
position: relative;
z-index: $kbnDiscoverSidebarDepth;
.kbnCollapsibleSidebar__collapseButton {
position: absolute;
top: 0;
right: -21px;
cursor: pointer;
z-index: -1;
}
&.closed {
width: 0 !important;
border-right-width: 0;
border-left-width: 0;
> * {
visibility: hidden;
}
.kbnCollapsibleSidebar__collapseButton {
visibility: visible;
.chevron-cont:before {
content: "\F138";
}
}
}
}
@include euiBreakpoint('xs', 's', 'm') {
.collapsible-sidebar {
&.closed {
display: none;
}
.kbnCollapsibleSidebar__collapseButton {
display: none;
}
}
}

View file

@ -1,13 +0,0 @@
/**
* 1. The local nav contains tooltips which should pop over the filter bar.
* 2. The filter and local nav components should always appear above the dashboard grid items.
* 3. The filter and local nav components should always appear above the discover content.
* 4. The sidebar collapser button should appear above the main Discover content but below the top elements.
* 5. Dragged panels in dashboard should always appear above other panels.
*/
$kbnFilterBarDepth: 4; /* 1 */
$kbnLocalNavDepth: 5; /* 1 */
$kbnDashboardGridDepth: 1; /* 2 */
$kbnDashboardDraggingGridDepth: 2; /* 5 */
$kbnDiscoverWrapperDepth: 1; /* 3 */
$kbnDiscoverSidebarDepth: 2; /* 4 */

View file

@ -1,2 +0,0 @@
@import 'depth';
@import 'collapsible_sidebar';

View file

@ -1,88 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you 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 _ from 'lodash';
import $ from 'jquery';
import { IScope } from 'angular';
interface LazyScope extends IScope {
[key: string]: any;
}
export function CollapsibleSidebarProvider() {
// simply a list of all of all of angulars .col-md-* classes except 12
const listOfWidthClasses = _.times(11, function (i) {
return 'col-md-' + i;
});
return {
restrict: 'C',
link: ($scope: LazyScope, $elem: any) => {
let isCollapsed = false;
const $collapser = $(
`<button
data-test-subj="collapseSideBarButton"
type="button"
aria-expanded="true"
aria-label="Toggle sidebar"
class="kuiCollapseButton kbnCollapsibleSidebar__collapseButton"
></button>`
);
// If the collapsable element has an id, also set aria-controls
if ($elem.attr('id')) {
$collapser.attr('aria-controls', $elem.attr('id'));
}
const $icon = $('<span class="kuiIcon fa-chevron-circle-left"></span>');
$collapser.append($icon);
const $siblings = $elem.siblings();
const siblingsClass = listOfWidthClasses.reduce((prev: string, className: string) => {
if (prev) return prev;
return $siblings.hasClass(className) && className;
}, '');
// If there is are only two elements we can assume the other one will take 100% of the width.
const hasSingleSibling = $siblings.length === 1 && siblingsClass;
$collapser.on('click', function () {
if (isCollapsed) {
isCollapsed = false;
$elem.removeClass('closed');
$icon.addClass('fa-chevron-circle-left');
$icon.removeClass('fa-chevron-circle-right');
$collapser.attr('aria-expanded', 'true');
} else {
isCollapsed = true;
$elem.addClass('closed');
$icon.removeClass('fa-chevron-circle-left');
$icon.addClass('fa-chevron-circle-right');
$collapser.attr('aria-expanded', 'false');
}
if (hasSingleSibling) {
$siblings.toggleClass(siblingsClass + ' col-md-12');
}
if ($scope.toggleSidebar) $scope.toggleSidebar();
});
$collapser.appendTo($elem);
},
};
}

View file

@ -21,7 +21,7 @@ import _ from 'lodash';
// Debounce service, angularized version of lodash debounce
// borrowed heavily from https://github.com/shahata/angular-debounce
export function DebounceProviderTimeout($timeout) {
export function createDebounceProviderTimeout($timeout) {
return function (func, wait, options) {
let timeout;
let args;
@ -66,7 +66,3 @@ export function DebounceProviderTimeout($timeout) {
return debounce;
};
}
export function DebounceProvider(debounce) {
return debounce;
}

View file

@ -24,7 +24,7 @@ import 'angular-sanitize';
import 'angular-route';
// @ts-ignore
import { DebounceProvider } from './index';
import { createDebounceProviderTimeout } from './debounce';
import { coreMock } from '../../../../../../../core/public/mocks';
import { initializeInnerAngularModule } from '../../../../get_inner_angular';
import { navigationPluginMock } from '../../../../../../navigation/public/mocks';
@ -33,7 +33,6 @@ import { initAngularBootstrap } from '../../../../../../kibana_legacy/public';
describe('debounce service', function () {
let debounce: (fn: () => void, timeout: number, options?: any) => any;
let debounceFromProvider: (fn: () => void, timeout: number, options?: any) => any;
let $timeout: ITimeoutService;
let spy: SinonSpy;
@ -51,22 +50,17 @@ describe('debounce service', function () {
angular.mock.module('app/discover');
angular.mock.inject(
($injector: auto.IInjectorService, _$timeout_: ITimeoutService, Private: any) => {
$timeout = _$timeout_;
angular.mock.inject(($injector: auto.IInjectorService, _$timeout_: ITimeoutService) => {
$timeout = _$timeout_;
debounce = $injector.get('debounce');
debounceFromProvider = Private(DebounceProvider);
}
);
debounce = createDebounceProviderTimeout($timeout);
});
});
it('should have a cancel method', function () {
const bouncer = debounce(() => {}, 100);
const bouncerFromProvider = debounceFromProvider(() => {}, 100);
expect(bouncer).toHaveProperty('cancel');
expect(bouncerFromProvider).toHaveProperty('cancel');
});
describe('delayed execution', function () {
@ -77,7 +71,6 @@ describe('debounce service', function () {
it('should delay execution', function () {
const bouncer = debounce(spy, 100);
const bouncerFromProvider = debounceFromProvider(spy, 100);
bouncer();
sinon.assert.notCalled(spy);
@ -85,16 +78,10 @@ describe('debounce service', function () {
sinon.assert.calledOnce(spy);
spy.resetHistory();
bouncerFromProvider();
sinon.assert.notCalled(spy);
$timeout.flush();
sinon.assert.calledOnce(spy);
});
it('should fire on leading edge', function () {
const bouncer = debounce(spy, 100, { leading: true });
const bouncerFromProvider = debounceFromProvider(spy, 100, { leading: true });
bouncer();
sinon.assert.calledOnce(spy);
@ -102,19 +89,10 @@ describe('debounce service', function () {
sinon.assert.calledTwice(spy);
spy.resetHistory();
bouncerFromProvider();
sinon.assert.calledOnce(spy);
$timeout.flush();
sinon.assert.calledTwice(spy);
});
it('should only fire on leading edge', function () {
const bouncer = debounce(spy, 100, { leading: true, trailing: false });
const bouncerFromProvider = debounceFromProvider(spy, 100, {
leading: true,
trailing: false,
});
bouncer();
sinon.assert.calledOnce(spy);
@ -122,17 +100,11 @@ describe('debounce service', function () {
sinon.assert.calledOnce(spy);
spy.resetHistory();
bouncerFromProvider();
sinon.assert.calledOnce(spy);
$timeout.flush();
sinon.assert.calledOnce(spy);
});
it('should reset delayed execution', function () {
const cancelSpy = sinon.spy($timeout, 'cancel');
const bouncer = debounce(spy, 100);
const bouncerFromProvider = debounceFromProvider(spy, 100);
bouncer();
sandbox.clock.tick(1);
@ -145,15 +117,6 @@ describe('debounce service', function () {
spy.resetHistory();
cancelSpy.resetHistory();
bouncerFromProvider();
sandbox.clock.tick(1);
bouncerFromProvider();
sinon.assert.notCalled(spy);
$timeout.flush();
sinon.assert.calledOnce(spy);
sinon.assert.calledOnce(cancelSpy);
});
});
@ -161,7 +124,6 @@ describe('debounce service', function () {
it('should cancel the $timeout', function () {
const cancelSpy = sinon.spy($timeout, 'cancel');
const bouncer = debounce(spy, 100);
const bouncerFromProvider = debounceFromProvider(spy, 100);
bouncer();
bouncer.cancel();
@ -170,12 +132,6 @@ describe('debounce service', function () {
$timeout.verifyNoPendingTasks();
cancelSpy.resetHistory();
bouncerFromProvider();
bouncerFromProvider.cancel();
sinon.assert.calledOnce(cancelSpy);
// throws if pending timeouts
$timeout.verifyNoPendingTasks();
});
});
});

View file

@ -17,6 +17,4 @@
* under the License.
*/
import './debounce';
export { DebounceProvider } from './debounce';
export { createDebounceProviderTimeout } from './debounce';

View file

@ -19,7 +19,7 @@
import $ from 'jquery';
import _ from 'lodash';
import { DebounceProvider } from './debounce';
import { createDebounceProviderTimeout } from './debounce';
const SCROLLER_HEIGHT = 20;
@ -28,124 +28,128 @@ const SCROLLER_HEIGHT = 20;
* to the target element's real scrollbar. This is useful when the target element's horizontal scrollbar
* might be waaaay down the page, like the doc table on Discover.
*/
export function FixedScrollProvider(Private) {
const debounce = Private(DebounceProvider);
export function FixedScrollProvider($timeout) {
return {
restrict: 'A',
link: function ($scope, $el) {
let $window = $(window);
let $scroller = $('<div class="fixed-scroll-scroller">').height(SCROLLER_HEIGHT);
/**
* Remove the listeners bound in listen()
* @type {function}
*/
let unlisten = _.noop;
/**
* Listen for scroll events on the $scroller and the $el, sets unlisten()
*
* unlisten must be called before calling or listen() will throw an Error
*
* Since the browser emits "scroll" events after setting scrollLeft
* the listeners also prevent tug-of-war
*
* @throws {Error} If unlisten was not called first
* @return {undefined}
*/
function listen() {
if (unlisten !== _.noop) {
throw new Error(
'fixedScroll listeners were not cleaned up properly before re-listening!'
);
}
let blockTo;
function bind($from, $to) {
function handler() {
if (blockTo === $to) return (blockTo = null);
$to.scrollLeft((blockTo = $from).scrollLeft());
}
$from.on('scroll', handler);
return function () {
$from.off('scroll', handler);
};
}
unlisten = _.flow(bind($el, $scroller), bind($scroller, $el), function () {
unlisten = _.noop;
});
}
/**
* Revert DOM changes and event listeners
* @return {undefined}
*/
function cleanUp() {
unlisten();
$scroller.detach();
$el.css('padding-bottom', 0);
}
/**
* Modify the DOM and attach event listeners based on need.
* Is called many times to re-setup, must be idempotent
* @return {undefined}
*/
function setup() {
cleanUp();
const containerWidth = $el.width();
const contentWidth = $el.prop('scrollWidth');
const containerHorizOverflow = contentWidth - containerWidth;
const elTop = $el.offset().top - $window.scrollTop();
const elBottom = elTop + $el.height();
const windowVertOverflow = elBottom - $window.height();
const requireScroller = containerHorizOverflow > 0 && windowVertOverflow > 0;
if (!requireScroller) return;
// push the content away from the scroller
$el.css('padding-bottom', SCROLLER_HEIGHT);
// fill the scroller with a dummy element that mimics the content
$scroller
.width(containerWidth)
.html($('<div>').css({ width: contentWidth, height: SCROLLER_HEIGHT }))
.insertAfter($el);
// listen for scroll events
listen();
}
let width;
let scrollWidth;
function checkWidth() {
const newScrollWidth = $el.prop('scrollWidth');
const newWidth = $el.width();
if (scrollWidth !== newScrollWidth || width !== newWidth) {
$scope.$apply(setup);
scrollWidth = newScrollWidth;
width = newWidth;
}
}
const debouncedCheckWidth = debounce(checkWidth, 100, {
invokeApply: false,
});
$scope.$watch(debouncedCheckWidth);
// cleanup when the scope is destroyed
$scope.$on('$destroy', function () {
cleanUp();
debouncedCheckWidth.cancel();
$scroller = $window = null;
});
return createFixedScroll($scope, $timeout)($el);
},
};
}
export function createFixedScroll($scope, $timeout) {
const debounce = createDebounceProviderTimeout($timeout);
return function (el) {
const $el = typeof el.css === 'function' ? el : $(el);
let $window = $(window);
let $scroller = $('<div class="dscTableFixedScroll__scroller">').height(SCROLLER_HEIGHT);
/**
* Remove the listeners bound in listen()
* @type {function}
*/
let unlisten = _.noop;
/**
* Listen for scroll events on the $scroller and the $el, sets unlisten()
*
* unlisten must be called before calling or listen() will throw an Error
*
* Since the browser emits "scroll" events after setting scrollLeft
* the listeners also prevent tug-of-war
*
* @throws {Error} If unlisten was not called first
* @return {undefined}
*/
function listen() {
if (unlisten !== _.noop) {
throw new Error('fixedScroll listeners were not cleaned up properly before re-listening!');
}
let blockTo;
function bind($from, $to) {
function handler() {
if (blockTo === $to) return (blockTo = null);
$to.scrollLeft((blockTo = $from).scrollLeft());
}
$from.on('scroll', handler);
return function () {
$from.off('scroll', handler);
};
}
unlisten = _.flow(bind($el, $scroller), bind($scroller, $el), function () {
unlisten = _.noop;
});
}
/**
* Revert DOM changes and event listeners
* @return {undefined}
*/
function cleanUp() {
unlisten();
$scroller.detach();
$el.css('padding-bottom', 0);
}
/**
* Modify the DOM and attach event listeners based on need.
* Is called many times to re-setup, must be idempotent
* @return {undefined}
*/
function setup() {
cleanUp();
const containerWidth = $el.width();
const contentWidth = $el.prop('scrollWidth');
const containerHorizOverflow = contentWidth - containerWidth;
const elTop = $el.offset().top - $window.scrollTop();
const elBottom = elTop + $el.height();
const windowVertOverflow = elBottom - $window.height();
const requireScroller = containerHorizOverflow > 0 && windowVertOverflow > 0;
if (!requireScroller) return;
// push the content away from the scroller
$el.css('padding-bottom', SCROLLER_HEIGHT);
// fill the scroller with a dummy element that mimics the content
$scroller
.width(containerWidth)
.html($('<div>').css({ width: contentWidth, height: SCROLLER_HEIGHT }))
.insertAfter($el);
// listen for scroll events
listen();
}
let width;
let scrollWidth;
function checkWidth() {
const newScrollWidth = $el.prop('scrollWidth');
const newWidth = $el.width();
if (scrollWidth !== newScrollWidth || width !== newWidth) {
$scope.$apply(setup);
scrollWidth = newScrollWidth;
width = newWidth;
}
}
const debouncedCheckWidth = debounce(checkWidth, 100, {
invokeApply: false,
});
$scope.$watch(debouncedCheckWidth);
function destroy() {
cleanUp();
debouncedCheckWidth.cancel();
$scroller = $window = null;
}
return destroy;
};
}

View file

@ -23,17 +23,12 @@ import $ from 'jquery';
import sinon from 'sinon';
import { PrivateProvider, initAngularBootstrap } from '../../../../../kibana_legacy/public';
import { initAngularBootstrap } from '../../../../../kibana_legacy/public';
import { FixedScrollProvider } from './fixed_scroll';
import { DebounceProviderTimeout } from './debounce/debounce';
const testModuleName = 'fixedScroll';
angular
.module(testModuleName, [])
.provider('Private', PrivateProvider)
.service('debounce', ['$timeout', DebounceProviderTimeout])
.directive('fixedScroll', FixedScrollProvider);
angular.module(testModuleName, []).directive('fixedScroll', FixedScrollProvider);
describe('FixedScroll directive', function () {
const sandbox = sinon.createSandbox();
@ -127,7 +122,7 @@ describe('FixedScroll directive', function () {
return {
$container: $el,
$content: $content,
$scroller: $parent.find('.fixed-scroll-scroller'),
$scroller: $parent.find('.dscTableFixedScroll__scroller'),
};
};
});

View file

@ -24,7 +24,6 @@ import PropTypes from 'prop-types';
import {
EuiCallOut,
EuiCode,
EuiCodeBlock,
EuiDescriptionList,
EuiFlexGroup,
EuiFlexItem,
@ -37,72 +36,12 @@ import { getServices } from '../../../kibana_services';
// eslint-disable-next-line react/prefer-stateless-function
export class DiscoverNoResults extends Component {
static propTypes = {
shardFailures: PropTypes.array,
timeFieldName: PropTypes.string,
queryLanguage: PropTypes.string,
};
render() {
const { shardFailures, timeFieldName, queryLanguage } = this.props;
let shardFailuresMessage;
if (shardFailures && shardFailures.length) {
const failures = shardFailures.map((failure, index) => (
<div key={`${failure.index}${failure.shard}${failure.reason}`}>
<EuiText size="xs">
<FormattedMessage
id="discover.noResults.indexFailureShardText"
defaultMessage="{index}, shard {failureShard}"
values={{
index: (
<strong>
<FormattedMessage
id="discover.noResults.indexFailureIndexText"
defaultMessage="Index {failureIndex}"
values={{
failureIndex: `&lsquo;${failure.index}&rsquo;`,
}}
/>
</strong>
),
failureShard: `&lsquo;${failure.shard}&rsquo;`,
}}
/>
</EuiText>
<EuiSpacer size="s" />
<EuiCodeBlock paddingSize="s">{JSON.stringify(failure.reason)}</EuiCodeBlock>
{index < shardFailures.length - 1 ? <EuiSpacer size="l" /> : undefined}
</div>
));
shardFailuresMessage = (
<Fragment>
<EuiSpacer size="xl" />
<EuiText>
<h3>
<FormattedMessage
id="discover.noResults.addressShardFailuresTitle"
defaultMessage="Address shard failures"
/>
</h3>
<p>
<FormattedMessage
id="discover.noResults.shardFailuresDescription"
defaultMessage="The following shard failures occurred:"
/>
</p>
{failures}
</EuiText>
</Fragment>
);
}
const { timeFieldName, queryLanguage } = this.props;
let timeFieldMessage;
@ -264,8 +203,6 @@ export class DiscoverNoResults extends Component {
iconType="help"
data-test-subj="discoverNoResults"
/>
{shardFailuresMessage}
{timeFieldMessage}
{luceneQueryMessage}
</EuiFlexItem>

View file

@ -42,35 +42,6 @@ beforeEach(() => {
describe('DiscoverNoResults', () => {
describe('props', () => {
describe('shardFailures', () => {
test('renders failures list when there are failures', () => {
const shardFailures = [
{
index: 'A',
shard: '1',
reason: { reason: 'Awful error' },
},
{
index: 'B',
shard: '2',
reason: { reason: 'Bad error' },
},
];
const component = renderWithIntl(<DiscoverNoResults shardFailures={shardFailures} />);
expect(component).toMatchSnapshot();
});
test(`doesn't render failures list when there are no failures`, () => {
const shardFailures = [];
const component = renderWithIntl(<DiscoverNoResults shardFailures={shardFailures} />);
expect(component).toMatchSnapshot();
});
});
describe('timeFieldName', () => {
test('renders time range feedback', () => {
const component = renderWithIntl(<DiscoverNoResults timeFieldName="awesome_time_field" />);

View file

@ -1,160 +0,0 @@
<discover-app class="app-container" data-fetch-counter="{{fetchCounter}}">
<h1 class="euiScreenReaderOnly">{{screenTitle}}</h1>
<!-- Local nav. -->
<kbn-top-nav
app-name="'discover'"
config="topNavMenu"
set-menu-mount-point="setHeaderActionMenu"
index-patterns="[indexPattern]"
on-query-submit="handleRefresh"
on-saved-query-id-change="updateSavedQueryId"
saved-query-id="state.savedQuery"
screen-title="screenTitle"
show-date-picker="indexPattern.isTimeBased()"
show-save-query="showSaveQuery"
show-search-bar="true"
use-default-behaviors="true"
>
</kbn-top-nav>
<main class="container-fluid">
<div class="row">
<div class="col-md-2 dscSidebar__container collapsible-sidebar" id="discover-sidebar" data-test-subj="discover-sidebar">
<div class="dscFieldChooser">
<discover-sidebar
columns="state.columns"
field-counts="fieldCounts"
hits="rows"
index-pattern-list="opts.indexPatternList"
on-add-field="addColumn"
on-add-filter="filterQuery"
on-remove-field="removeColumn"
selected-index-pattern="searchSource.getField('index')"
set-index-pattern="setIndexPattern"
>
</discover-sidebar>
</div>
</div>
<div class="dscWrapper col-md-10">
<discover-no-results
ng-show="resultState === 'none'"
shard-failures="failures"
time-field-name="opts.timefield"
query-language="state.query.language"
get-doc-link="getDocLink"
></discover-no-results>
<discover-uninitialized
ng-show="resultState === 'uninitialized'"
on-refresh="fetch"
></discover-uninitialized>
<!-- loading -->
<div ng-show="resultState === 'loading'">
<discover-fetch-error
ng-show="fetchError"
fetch-error="fetchError"
></discover-fetch-error>
<loading-spinner ng-hide="fetchError" class="dscOverlay"></loading-spinner>
</div>
<div class="dscWrapper__content" ng-show="resultState === 'ready'">
<!-- result -->
<div class="dscResults">
<skip-bottom-button on-click="onSkipBottomButtonClick"></skip-bottom-button>
<hits-counter
hits="hits || 0"
show-reset-button="opts.savedSearch.id"
on-reset-query="resetQuery"
>
</hits-counter>
<section
aria-label="{{::'discover.histogramOfFoundDocumentsAriaLabel' | i18n: {defaultMessage: 'Histogram of found documents'} }}"
class="dscTimechart"
ng-if="opts.timefield"
>
<timechart-header
from="toMoment(timeRange.from)"
to="toMoment(timeRange.to)"
options="intervalOptions"
on-change-interval="changeInterval"
state-interval="state.interval"
show-scaled-info="bucketInterval.scaled"
bucket-interval-description="bucketInterval.description"
bucket-interval-scale="bucketInterval.scale"
>
</timechart-header>
<discover-histogram
class="dscHistogram"
ng-show="vis && rows.length !== 0"
chart-data="histogramData"
timefilter-update-handler="timefilterUpdateHandler"
watch-depth="reference"
data-test-subj="discoverChart"
></discover-histogram>
</section>
<section
class="dscTable"
fixed-scroll
aria-labelledby="documentsAriaLabel"
>
<h2 class="euiScreenReaderOnly"
id="documentsAriaLabel"
i18n-id="discover.documentsAriaLabel"
i18n-default-message="Documents"
></h2>
<doc-table
hits="rows"
index-pattern="indexPattern"
sorting="state.sort"
columns="state.columns"
infinite-scroll="true"
filter="filterQuery"
data-shared-item
data-title="{{opts.savedSearch.lastSavedTitle}}"
data-description="{{opts.savedSearch.description}}"
data-test-subj="discoverDocTable"
minimum-visible-rows="minimumVisibleRows"
render-complete
on-add-column="addColumn"
on-change-sort-order="setSortOrder"
on-move-column="moveColumn"
on-remove-column="removeColumn"
></doc-table>
<a tabindex="0" id="discoverBottomMarker">&#8203;</a>
<div
ng-if="rows.length == opts.sampleSize"
class="dscTable__footer"
data-test-subj="discoverDocTableFooter"
>
<span
i18n-id="discover.howToSeeOtherMatchingDocumentsDescription"
i18n-default-message="These are the first {sampleSize} documents matching
your search, refine your search to see others. "
i18n-values="{
sampleSize: opts.sampleSize,
}"
></span>
<a
kbn-accessible-click
ng-click="scrollToTop()"
i18n-id="discover.backToTopLinkText"
i18n-default-message="Back to top."
></a>
</div>
</section>
</div>
</div>
</div>
</div>
</main>
</discover-app>

View file

@ -29,12 +29,11 @@ import { getState, splitState } from './discover_state';
import { RequestAdapter } from '../../../../inspector/public';
import { SavedObjectSaveModal, showSaveModal } from '../../../../saved_objects/public';
import { getSortArray, getSortForSearchSource } from './doc_table';
import { createFixedScroll } from './directives/fixed_scroll';
import * as columnActions from './doc_table/actions/columns';
import indexTemplate from './discover.html';
import indexTemplateLegacy from './discover_legacy.html';
import { showOpenSearchPanel } from '../components/top_nav/show_open_search_panel';
import { addHelpMenuToAppChrome } from '../components/help_menu/help_menu_util';
import '../components/fetch_error';
import { getPainlessError } from './get_painless_error';
import { discoverResponseHandler } from './response_handler';
import {
@ -71,7 +70,6 @@ import {
indexPatterns as indexPatternsUtils,
connectToQueryState,
syncQueryStateWithUrl,
search,
} from '../../../../data/public';
import { getIndexPatternId } from '../helpers/get_index_pattern_id';
import { addFatalError } from '../../../../kibana_legacy/public';
@ -115,7 +113,7 @@ app.config(($routeProvider) => {
};
const discoverRoute = {
...defaults,
template: indexTemplate,
template: indexTemplateLegacy,
reloadOnSearch: false,
resolve: {
savedObjects: function ($route, Promise) {
@ -308,18 +306,10 @@ function discoverController($element, $route, $scope, $timeout, $window, Promise
mode: 'absolute',
});
};
$scope.intervalOptions = search.aggs.intervalOptions;
$scope.minimumVisibleRows = 50;
$scope.fetchStatus = fetchStatuses.UNINITIALIZED;
$scope.showSaveQuery = uiCapabilities.discover.saveQuery;
$scope.$watch(
() => uiCapabilities.discover.saveQuery,
(newCapability) => {
$scope.showSaveQuery = newCapability;
}
);
let abortController;
$scope.$on('$destroy', () => {
if (abortController) abortController.abort();
@ -471,7 +461,6 @@ function discoverController($element, $route, $scope, $timeout, $window, Promise
];
};
$scope.topNavMenu = getTopNavLinks();
$scope.setHeaderActionMenu = getHeaderActionMenuMounter();
$scope.searchSource
.setField('index', $scope.indexPattern)
@ -515,8 +504,6 @@ function discoverController($element, $route, $scope, $timeout, $window, Promise
]);
}
$scope.screenTitle = savedSearch.title;
const getFieldCounts = async () => {
// the field counts aren't set until we have the data back,
// so we wait for the fetch to be done before proceeding
@ -612,6 +599,9 @@ function discoverController($element, $route, $scope, $timeout, $window, Promise
timefield: getTimeField(),
savedSearch: savedSearch,
indexPatternList: $route.current.locals.savedObjects.ip.list,
config: config,
fixedScroll: createFixedScroll($scope, $timeout),
setHeaderActionMenu: getHeaderActionMenuMounter(),
};
const shouldSearchOnPageLoad = () => {
@ -771,6 +761,7 @@ function discoverController($element, $route, $scope, $timeout, $window, Promise
if (!init.complete) return;
$scope.fetchCounter++;
$scope.fetchError = undefined;
$scope.minimumVisibleRows = 50;
if (!validateTimeRange(timefilter.getTime(), toastNotifications)) {
$scope.resultState = 'none';
return;
@ -868,9 +859,6 @@ function discoverController($element, $route, $scope, $timeout, $window, Promise
tabifiedData,
getDimensions($scope.vis.data.aggs.aggs, $scope.timeRange)
);
if ($scope.vis.data.aggs.aggs[1]) {
$scope.bucketInterval = $scope.vis.data.aggs.aggs[1].buckets.getInterval();
}
$scope.updateTime();
}

View file

@ -0,0 +1,36 @@
<discover-app>
<discover-legacy
add-column="addColumn"
fetch="fetch"
fetch-counter="fetchCounter"
fetch-error="fetchError"
field-counts="fieldCounts"
histogram-data="histogramData"
hits="hits"
index-pattern="indexPattern"
minimum-visible-rows="minimumVisibleRows"
on-add-filter="filterQuery"
on-move-column="moveColumn"
on-change-interval="changeInterval"
on-remove-column="removeColumn"
on-set-columns="setColumns"
on-skip-bottom-button-click="onSkipBottomButtonClick"
on-sort="setSortOrder"
opts="opts"
reset-query="resetQuery"
result-state="resultState"
rows="rows"
saved-search="savedSearch"
search-source="searchSource"
set-index-pattern="setIndexPattern"
show-save-query="showSaveQuery"
state="state"
time-filter-update-handler="timefilterUpdateHandler"
time-range="timeRange"
top-nav-menu="topNavMenu"
update-query="handleRefresh"
update-saved-query-id="updateSavedQueryId"
vis="vis"
>
</discover-legacy>
</discover-app>

View file

@ -55,6 +55,10 @@ export interface AppState {
* Array of the used sorting [[field,direction],...]
*/
sort?: string[][];
/**
* id of the used saved query
*/
savedQuery?: string;
}
interface GetStateParams {

View file

@ -0,0 +1,131 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you 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 angular, { auto, ICompileService, IScope } from 'angular';
import { render } from 'react-dom';
import React, { useRef, useEffect } from 'react';
import { getServices, IIndexPattern } from '../../../kibana_services';
import { IndexPatternField } from '../../../../../data/common/index_patterns';
export type AngularScope = IScope;
export interface AngularDirective {
template: string;
}
/**
* Compiles and injects the give angular template into the given dom node
* returns a function to cleanup the injected angular element
*/
export async function injectAngularElement(
domNode: Element,
template: string,
scopeProps: any,
getInjector: () => Promise<auto.IInjectorService>
): Promise<() => void> {
const $injector = await getInjector();
const rootScope: AngularScope = $injector.get('$rootScope');
const $compile: ICompileService = $injector.get('$compile');
const newScope = Object.assign(rootScope.$new(), scopeProps);
const $target = angular.element(domNode);
const $element = angular.element(template);
newScope.$apply(() => {
const linkFn = $compile($element);
$target.empty().append($element);
linkFn(newScope);
});
return () => {
newScope.$destroy();
};
}
/**
* Converts a given legacy angular directive to a render function
* for usage in a react component. Note that the rendering is async
*/
export function convertDirectiveToRenderFn(
directive: AngularDirective,
getInjector: () => Promise<auto.IInjectorService>
) {
return (domNode: Element, props: any) => {
let rejected = false;
const cleanupFnPromise = injectAngularElement(domNode, directive.template, props, getInjector);
cleanupFnPromise.catch(() => {
rejected = true;
render(<div>error</div>, domNode);
});
return () => {
if (!rejected) {
// for cleanup
// http://roubenmeschian.com/rubo/?p=51
cleanupFnPromise.then((cleanup) => cleanup());
}
};
};
}
export interface DocTableLegacyProps {
columns: string[];
searchDescription?: string;
searchTitle?: string;
onFilter: (field: IndexPatternField | string, value: string, type: '+' | '-') => void;
rows: Array<Record<string, unknown>>;
indexPattern: IIndexPattern;
minimumVisibleRows: number;
onAddColumn: (column: string) => void;
onSort: (sort: string[][]) => void;
onMoveColumn: (columns: string, newIdx: number) => void;
onRemoveColumn: (column: string) => void;
sort?: string[][];
}
export function DocTableLegacy(renderProps: DocTableLegacyProps) {
const renderFn = convertDirectiveToRenderFn(
{
template: `<doc-table
columns="columns"
data-description="{{searchDescription}}"
data-shared-item
data-test-subj="discoverDocTable"
data-title="{{searchTitle}}"
filter="onFilter"
hits="rows"
index-pattern="indexPattern"
infinite-scroll="true"
minimum-visible-rows="minimumVisibleRows"
on-add-column="onAddColumn"
on-change-sort-order="onSort"
on-move-column="onMoveColumn"
on-remove-column="onRemoveColumn"
render-complete
sorting="sort"></doc_table>`,
},
() => getServices().getEmbeddableInjector()
);
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
if (ref && ref.current) {
return renderFn(ref.current, renderProps);
}
}, [renderFn, renderProps]);
return <div ref={ref} />;
}

View file

@ -50,10 +50,6 @@ export function createDocTableDirective(pagerFactory: any, $filter: any) {
inspectorAdapters: '=?',
},
link: ($scope: LazyScope, $el: JQuery) => {
$scope.$watch('minimumVisibleRows', (minimumVisibleRows: number) => {
$scope.limit = Math.max(minimumVisibleRows || 50, $scope.limit || 50);
});
$scope.persist = {
sorting: $scope.sorting,
columns: $scope.columns,
@ -77,7 +73,7 @@ export function createDocTableDirective(pagerFactory: any, $filter: any) {
if (!hits) return;
// Reset infinite scroll limit
$scope.limit = 50;
$scope.limit = $scope.minimumVisibleRows || 50;
if (hits.length === 0) {
dispatchRenderComplete($el[0]);

View file

@ -0,0 +1,56 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you 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 { DiscoverLegacy } from './discover_legacy';
export function createDiscoverLegacyDirective(reactDirective: any) {
return reactDirective(DiscoverLegacy, [
['addColumn', { watchDepth: 'reference' }],
['fetch', { watchDepth: 'reference' }],
['fetchCounter', { watchDepth: 'reference' }],
['fetchError', { watchDepth: 'reference' }],
['fieldCounts', { watchDepth: 'reference' }],
['histogramData', { watchDepth: 'reference' }],
['hits', { watchDepth: 'reference' }],
['indexPattern', { watchDepth: 'reference' }],
['minimumVisibleRows', { watchDepth: 'reference' }],
['onAddFilter', { watchDepth: 'reference' }],
['onChangeInterval', { watchDepth: 'reference' }],
['onMoveColumn', { watchDepth: 'reference' }],
['onRemoveColumn', { watchDepth: 'reference' }],
['onSetColumns', { watchDepth: 'reference' }],
['onSkipBottomButtonClick', { watchDepth: 'reference' }],
['onSort', { watchDepth: 'reference' }],
['opts', { watchDepth: 'reference' }],
['resetQuery', { watchDepth: 'reference' }],
['resultState', { watchDepth: 'reference' }],
['rows', { watchDepth: 'reference' }],
['savedSearch', { watchDepth: 'reference' }],
['searchSource', { watchDepth: 'reference' }],
['setIndexPattern', { watchDepth: 'reference' }],
['showSaveQuery', { watchDepth: 'reference' }],
['state', { watchDepth: 'reference' }],
['timefilterUpdateHandler', { watchDepth: 'reference' }],
['timeRange', { watchDepth: 'reference' }],
['topNavMenu', { watchDepth: 'reference' }],
['updateQuery', { watchDepth: 'reference' }],
['updateSavedQueryId', { watchDepth: 'reference' }],
['vis', { watchDepth: 'reference' }],
]);
}

View file

@ -0,0 +1,324 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you 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, { useState, useCallback, useEffect } from 'react';
import classNames from 'classnames';
import { EuiButtonEmpty, EuiButtonIcon } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage, I18nProvider } from '@kbn/i18n/react';
import { IUiSettingsClient, MountPoint } from 'kibana/public';
import { HitsCounter } from './hits_counter';
import { TimechartHeader } from './timechart_header';
import { DiscoverSidebar } from './sidebar';
import { getServices, IIndexPattern } from '../../kibana_services';
// @ts-ignore
import { DiscoverNoResults } from '../angular/directives/no_results';
import { DiscoverUninitialized } from '../angular/directives/uninitialized';
import { DiscoverHistogram } from '../angular/directives/histogram';
import { LoadingSpinner } from './loading_spinner/loading_spinner';
import { DiscoverFetchError, FetchError } from './fetch_error/fetch_error';
import { DocTableLegacy } from '../angular/doc_table/create_doc_table_react';
import { SkipBottomButton } from './skip_bottom_button';
import {
IndexPatternField,
search,
ISearchSource,
TimeRange,
Query,
IndexPatternAttributes,
} from '../../../../data/public';
import { Chart } from '../angular/helpers/point_series';
import { AppState } from '../angular/discover_state';
import { SavedSearch } from '../../saved_searches';
import { SavedObject } from '../../../../../core/types';
import { Vis } from '../../../../visualizations/public';
import { TopNavMenuData } from '../../../../navigation/public';
export interface DiscoverLegacyProps {
addColumn: (column: string) => void;
fetch: () => void;
fetchCounter: number;
fetchError: FetchError;
fieldCounts: Record<string, number>;
histogramData: Chart;
hits: number;
indexPattern: IIndexPattern;
minimumVisibleRows: number;
onAddFilter: (field: IndexPatternField | string, value: string, type: '+' | '-') => void;
onChangeInterval: (interval: string) => void;
onMoveColumn: (columns: string, newIdx: number) => void;
onRemoveColumn: (column: string) => void;
onSetColumns: (columns: string[]) => void;
onSkipBottomButtonClick: () => void;
onSort: (sort: string[][]) => void;
opts: {
savedSearch: SavedSearch;
config: IUiSettingsClient;
indexPatternList: Array<SavedObject<IndexPatternAttributes>>;
timefield: string;
sampleSize: number;
fixedScroll: (el: HTMLElement) => void;
setHeaderActionMenu: (menuMount: MountPoint | undefined) => void;
};
resetQuery: () => void;
resultState: string;
rows: Array<Record<string, unknown>>;
searchSource: ISearchSource;
setIndexPattern: (id: string) => void;
showSaveQuery: boolean;
state: AppState;
timefilterUpdateHandler: (ranges: { from: number; to: number }) => void;
timeRange?: { from: string; to: string };
topNavMenu: TopNavMenuData[];
updateQuery: (payload: { dateRange: TimeRange; query?: Query }, isUpdate?: boolean) => void;
updateSavedQueryId: (savedQueryId?: string) => void;
vis?: Vis;
}
export function DiscoverLegacy({
addColumn,
fetch,
fetchCounter,
fetchError,
fieldCounts,
histogramData,
hits,
indexPattern,
minimumVisibleRows,
onAddFilter,
onChangeInterval,
onMoveColumn,
onRemoveColumn,
onSkipBottomButtonClick,
onSort,
opts,
resetQuery,
resultState,
rows,
searchSource,
setIndexPattern,
showSaveQuery,
state,
timefilterUpdateHandler,
timeRange,
topNavMenu,
updateQuery,
updateSavedQueryId,
vis,
}: DiscoverLegacyProps) {
const [isSidebarClosed, setIsSidebarClosed] = useState(false);
const { TopNavMenu } = getServices().navigation.ui;
const { savedSearch, indexPatternList } = opts;
const bucketAggConfig = vis?.data?.aggs?.aggs[1];
const bucketInterval =
bucketAggConfig && search.aggs.isDateHistogramBucketAggConfig(bucketAggConfig)
? bucketAggConfig.buckets?.getInterval()
: undefined;
const [fixedScrollEl, setFixedScrollEl] = useState<HTMLElement | undefined>();
useEffect(() => (fixedScrollEl ? opts.fixedScroll(fixedScrollEl) : undefined), [
fixedScrollEl,
opts,
]);
const fixedScrollRef = useCallback(
(node: HTMLElement) => {
if (node !== null) {
setFixedScrollEl(node);
}
},
[setFixedScrollEl]
);
const sidebarClassName = classNames({
closed: isSidebarClosed,
});
const mainSectionClassName = classNames({
'col-md-10': !isSidebarClosed,
'col-md-12': isSidebarClosed,
});
return (
<I18nProvider>
<div className="dscAppContainer" data-fetch-counter={fetchCounter}>
<h1 className="euiScreenReaderOnly">{savedSearch.title}</h1>
<TopNavMenu
appName="discover"
config={topNavMenu}
indexPatterns={[indexPattern]}
onQuerySubmit={updateQuery}
onSavedQueryIdChange={updateSavedQueryId}
query={state.query}
setMenuMountPoint={opts.setHeaderActionMenu}
savedQueryId={state.savedQuery}
screenTitle={savedSearch.title}
showDatePicker={indexPattern.isTimeBased()}
showSaveQuery={showSaveQuery}
showSearchBar={true}
useDefaultBehaviors={true}
/>
<main className="container-fluid">
<div className="row">
<div
className={`col-md-2 dscSidebar__container dscCollapsibleSidebar ${sidebarClassName}`}
id="discover-sidebar"
data-test-subj="discover-sidebar"
>
{!isSidebarClosed && (
<div className="dscFieldChooser">
<DiscoverSidebar
columns={state.columns || []}
fieldCounts={fieldCounts}
hits={rows}
indexPatternList={indexPatternList}
onAddField={addColumn}
onAddFilter={onAddFilter}
onRemoveField={onRemoveColumn}
selectedIndexPattern={searchSource && searchSource.getField('index')}
setIndexPattern={setIndexPattern}
/>
</div>
)}
<EuiButtonIcon
iconType={isSidebarClosed ? 'menuRight' : 'menuLeft'}
iconSize="m"
size="s"
onClick={() => setIsSidebarClosed(!isSidebarClosed)}
data-test-subj="collapseSideBarButton"
aria-controls="discover-sidebar"
aria-expanded={isSidebarClosed ? 'false' : 'true'}
aria-label="Toggle sidebar"
className="dscCollapsibleSidebar__collapseButton"
/>
</div>
<div className={`dscWrapper ${mainSectionClassName}`}>
{resultState === 'none' && (
<DiscoverNoResults
timeFieldName={opts.timefield}
queryLanguage={state.query ? state.query.language : ''}
/>
)}
{resultState === 'uninitialized' && <DiscoverUninitialized onRefresh={fetch} />}
{/* @TODO: Solved in the Angular way to satisfy functional test - should be improved*/}
<span style={{ display: resultState !== 'loading' ? 'none' : '' }}>
{fetchError && <DiscoverFetchError fetchError={fetchError} />}
<div className="dscOverlay" style={{ display: fetchError ? 'none' : '' }}>
<LoadingSpinner />
</div>
</span>
{resultState === 'ready' && (
<div className="dscWrapper__content">
<SkipBottomButton onClick={onSkipBottomButtonClick} />
<HitsCounter
hits={hits > 0 ? hits : 0}
showResetButton={!!(savedSearch && savedSearch.id)}
onResetQuery={resetQuery}
/>
{opts.timefield && (
<TimechartHeader
dateFormat={opts.config.get('dateFormat')}
timeRange={timeRange}
options={search.aggs.intervalOptions}
onChangeInterval={onChangeInterval}
stateInterval={state.interval || ''}
bucketInterval={bucketInterval}
/>
)}
{opts.timefield && (
<section
aria-label={i18n.translate('discover.histogramOfFoundDocumentsAriaLabel', {
defaultMessage: 'Histogram of found documents',
})}
className="dscTimechart"
>
{vis && rows.length !== 0 && (
<div className="dscHistogram" data-test-subj="discoverChart">
<DiscoverHistogram
chartData={histogramData}
timefilterUpdateHandler={timefilterUpdateHandler}
/>
</div>
)}
</section>
)}
<div className="dscResults">
<section
className="dscTable dscTableFixedScroll"
aria-labelledby="documentsAriaLabel"
ref={fixedScrollRef}
>
<h2 className="euiScreenReaderOnly" id="documentsAriaLabel">
<FormattedMessage
id="discover.documentsAriaLabel"
defaultMessage="Documents"
/>
</h2>
{rows && rows.length && (
<div className="dscDiscover">
<DocTableLegacy
columns={state.columns || []}
indexPattern={indexPattern}
minimumVisibleRows={minimumVisibleRows}
rows={rows}
sort={state.sort || []}
searchDescription={opts.savedSearch.description}
searchTitle={opts.savedSearch.lastSavedTitle}
onAddColumn={addColumn}
onFilter={onAddFilter}
onMoveColumn={onMoveColumn}
onRemoveColumn={onRemoveColumn}
onSort={onSort}
/>
<a tabIndex={0} id="discoverBottomMarker">
&#8203;
</a>
{rows.length === opts.sampleSize && (
<div
className="dscTable__footer"
data-test-subj="discoverDocTableFooter"
>
<FormattedMessage
id="discover.howToSeeOtherMatchingDocumentsDescription"
defaultMessage="These are the first {sampleSize} documents matching
your search, refine your search to see others."
values={{ sampleSize: opts.sampleSize }}
/>
<EuiButtonEmpty onClick={() => window.scrollTo(0, 0)}>
<FormattedMessage
id="discover.backToTopLinkText"
defaultMessage="Back to top."
/>
</EuiButtonEmpty>
</div>
)}
</div>
)}
</section>
</div>
</div>
)}
</div>
</div>
</main>
</div>
</I18nProvider>
);
}

View file

@ -20,18 +20,20 @@ import './fetch_error.scss';
import React, { Fragment } from 'react';
import { FormattedMessage, I18nProvider } from '@kbn/i18n/react';
import { EuiFlexGroup, EuiFlexItem, EuiCallOut, EuiCodeBlock, EuiSpacer } from '@elastic/eui';
import { getAngularModule, getServices } from '../../../kibana_services';
import { getServices } from '../../../kibana_services';
interface Props {
fetchError: {
lang: string;
script: string;
message: string;
error: string;
};
export interface FetchError {
lang: string;
script: string;
message: string;
error: string;
}
const DiscoverFetchError = ({ fetchError }: Props) => {
interface Props {
fetchError: FetchError;
}
export const DiscoverFetchError = ({ fetchError }: Props) => {
if (!fetchError) {
return null;
}
@ -92,9 +94,3 @@ const DiscoverFetchError = ({ fetchError }: Props) => {
</I18nProvider>
);
};
export function createFetchErrorDirective(reactDirective: any) {
return reactDirective(DiscoverFetchError);
}
getAngularModule().directive('discoverFetchError', createFetchErrorDirective);

View file

@ -1,27 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you 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 { HitsCounter } from './hits_counter';
export function createHitsCounterDirective(reactDirective: any) {
return reactDirective(HitsCounter, [
['hits', { watchDepth: 'reference' }],
['showResetButton', { watchDepth: 'reference' }],
['onResetQuery', { watchDepth: 'reference' }],
]);
}

View file

@ -18,4 +18,3 @@
*/
export { HitsCounter } from './hits_counter';
export { createHitsCounterDirective } from './hits_counter_directive';

View file

@ -18,24 +18,18 @@
*/
import React from 'react';
import { EuiLoadingSpinner, EuiTitle, EuiSpacer } from '@elastic/eui';
import { FormattedMessage, I18nProvider } from '@kbn/i18n/react';
import { FormattedMessage } from '@kbn/i18n/react';
export function LoadingSpinner() {
return (
<I18nProvider>
<>
<EuiTitle size="s" data-test-subj="loadingSpinnerText">
<h2>
<FormattedMessage id="discover.searchingTitle" defaultMessage="Searching" />
</h2>
</EuiTitle>
<EuiSpacer size="m" />
<EuiLoadingSpinner size="l" data-test-subj="loadingSpinner" />
</>
</I18nProvider>
<>
<EuiTitle size="s" data-test-subj="loadingSpinnerText">
<h2>
<FormattedMessage id="discover.searchingTitle" defaultMessage="Searching" />
</h2>
</EuiTitle>
<EuiSpacer size="m" />
<EuiLoadingSpinner size="l" data-test-subj="loadingSpinner" />
</>
);
}
export function createLoadingSpinnerDirective(reactDirective: any) {
return reactDirective(LoadingSpinner);
}

View file

@ -68,7 +68,7 @@ export interface DiscoverSidebarProps {
/**
* Currently selected index pattern
*/
selectedIndexPattern: IndexPattern;
selectedIndexPattern?: IndexPattern;
/**
* Callback function to select another index pattern
*/

View file

@ -1,33 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you 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 { DiscoverSidebar } from './discover_sidebar';
export function createDiscoverSidebarDirective(reactDirective: any) {
return reactDirective(DiscoverSidebar, [
['columns', { watchDepth: 'reference' }],
['fieldCounts', { watchDepth: 'reference' }],
['hits', { watchDepth: 'reference' }],
['indexPatternList', { watchDepth: 'reference' }],
['onAddField', { watchDepth: 'reference' }],
['onAddFilter', { watchDepth: 'reference' }],
['onRemoveField', { watchDepth: 'reference' }],
['selectedIndexPattern', { watchDepth: 'reference' }],
['setIndexPattern', { watchDepth: 'reference' }],
]);
}

View file

@ -18,4 +18,3 @@
*/
export { DiscoverSidebar } from './discover_sidebar';
export { createDiscoverSidebarDirective } from './discover_sidebar_directive';

View file

@ -25,8 +25,11 @@ export function getDetails(
field: IndexPatternField,
hits: Array<Record<string, unknown>>,
columns: string[],
indexPattern: IndexPattern
indexPattern?: IndexPattern
) {
if (!indexPattern) {
return {};
}
const details = {
...fieldCalculator.getFieldValueCounts({
hits,

View file

@ -20,8 +20,8 @@ import { difference } from 'lodash';
import { IndexPattern, IndexPatternField } from 'src/plugins/data/public';
export function getIndexPatternFieldList(
indexPattern: IndexPattern,
fieldCounts: Record<string, number>
indexPattern?: IndexPattern,
fieldCounts?: Record<string, number>
) {
if (!indexPattern || !fieldCounts) return [];

View file

@ -18,4 +18,3 @@
*/
export { SkipBottomButton } from './skip_bottom_button';
export { createSkipBottomButtonDirective } from './skip_bottom_button_directive';

View file

@ -1,23 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you 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 { SkipBottomButton } from './skip_bottom_button';
export function createSkipBottomButtonDirective(reactDirective: any) {
return reactDirective(SkipBottomButton, [['onClick', { watchDepth: 'reference' }]]);
}

View file

@ -18,4 +18,3 @@
*/
export { TimechartHeader } from './timechart_header';
export { createTimechartHeaderDirective } from './timechart_header_directive';

View file

@ -29,8 +29,10 @@ describe('timechart header', function () {
beforeAll(() => {
props = {
from: 'May 14, 2020 @ 11:05:13.590',
to: 'May 14, 2020 @ 11:20:13.590',
timeRange: {
from: 'May 14, 2020 @ 11:05:13.590',
to: 'May 14, 2020 @ 11:20:13.590',
},
stateInterval: 's',
options: [
{
@ -47,9 +49,11 @@ describe('timechart header', function () {
},
],
onChangeInterval: jest.fn(),
showScaledInfo: undefined,
bucketIntervalDescription: 'second',
bucketIntervalScale: undefined,
bucketInterval: {
scaled: undefined,
description: 'second',
scale: undefined,
},
};
});
@ -58,8 +62,8 @@ describe('timechart header', function () {
expect(component.find(EuiIconTip).length).toBe(0);
});
it('TimechartHeader renders an info text by providing the showScaledInfo property', () => {
props.showScaledInfo = true;
it('TimechartHeader renders an info when bucketInterval.scale is set to true', () => {
props.bucketInterval!.scaled = true;
component = mountWithIntl(<TimechartHeader {...props} />);
expect(component.find(EuiIconTip).length).toBe(1);
});

View file

@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useCallback } from 'react';
import {
EuiFlexGroup,
EuiFlexItem,
@ -27,16 +27,28 @@ import {
} from '@elastic/eui';
import { I18nProvider } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import moment from 'moment';
export interface TimechartHeaderProps {
/**
* the query from date string
* Format of date to be displayed
*/
from: string;
dateFormat?: string;
/**
* the query to date string
* Interval for the buckets of the recent request
*/
to: string;
bucketInterval?: {
scaled?: boolean;
description?: string;
scale?: number;
};
/**
* Range of dates to be displayed
*/
timeRange?: {
from: string;
to: string;
};
/**
* Interval Options
*/
@ -49,31 +61,29 @@ export interface TimechartHeaderProps {
* selected interval
*/
stateInterval: string;
/**
* displays the scaled info of the interval
*/
showScaledInfo: boolean | undefined;
/**
* scaled info description
*/
bucketIntervalDescription: string;
/**
* bucket interval scale
*/
bucketIntervalScale: number | undefined;
}
export function TimechartHeader({
from,
to,
bucketInterval,
dateFormat,
timeRange,
options,
onChangeInterval,
stateInterval,
showScaledInfo,
bucketIntervalDescription,
bucketIntervalScale,
}: TimechartHeaderProps) {
const [interval, setInterval] = useState(stateInterval);
const toMoment = useCallback(
(datetime: string) => {
if (!datetime) {
return '';
}
if (!dateFormat) {
return datetime;
}
return moment(datetime).format(dateFormat);
},
[dateFormat]
);
useEffect(() => {
setInterval(stateInterval);
@ -84,6 +94,10 @@ export function TimechartHeader({
onChangeInterval(e.target.value);
};
if (!timeRange || !bucketInterval) {
return null;
}
return (
<I18nProvider>
<EuiFlexGroup gutterSize="s" responsive justifyContent="center" alignItems="center">
@ -95,7 +109,7 @@ export function TimechartHeader({
delay="long"
>
<EuiText data-test-subj="discoverIntervalDateRange" size="s">
{`${from} - ${to} ${
{`${toMoment(timeRange.from)} - ${toMoment(timeRange.to)} ${
interval !== 'auto'
? i18n.translate('discover.timechartHeader.timeIntervalSelect.per', {
defaultMessage: 'per',
@ -125,7 +139,7 @@ export function TimechartHeader({
value={interval}
onChange={handleIntervalChange}
append={
showScaledInfo ? (
bucketInterval.scaled ? (
<EuiIconTip
id="discoverIntervalIconTip"
content={i18n.translate('discover.bucketIntervalTooltip', {
@ -133,14 +147,14 @@ export function TimechartHeader({
'This interval creates {bucketsDescription} to show in the selected time range, so it has been scaled to {bucketIntervalDescription}.',
values: {
bucketsDescription:
bucketIntervalScale && bucketIntervalScale > 1
bucketInterval!.scale && bucketInterval!.scale > 1
? i18n.translate('discover.bucketIntervalTooltip.tooLargeBucketsText', {
defaultMessage: 'buckets that are too large',
})
: i18n.translate('discover.bucketIntervalTooltip.tooManyBucketsText', {
defaultMessage: 'too many buckets',
}),
bucketIntervalDescription,
bucketIntervalDescription: bucketInterval.description,
},
})}
color="warning"

View file

@ -1,32 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you 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 { TimechartHeader } from './timechart_header';
export function createTimechartHeaderDirective(reactDirective: any) {
return reactDirective(TimechartHeader, [
['from', { watchDepth: 'reference' }],
['to', { watchDepth: 'reference' }],
['options', { watchDepth: 'reference' }],
['onChangeInterval', { watchDepth: 'reference' }],
['stateInterval', { watchDepth: 'reference' }],
['showScaledInfo', { watchDepth: 'reference' }],
['bucketIntervalDescription', { watchDepth: 'reference' }],
['bucketIntervalScale', { watchDepth: 'reference' }],
]);
}

View file

@ -44,6 +44,7 @@ import { createSavedSearchesLoader, SavedSearch } from './saved_searches';
import { getHistory } from './kibana_services';
import { KibanaLegacyStart } from '../../kibana_legacy/public';
import { UrlForwardingStart } from '../../url_forwarding/public';
import { NavigationPublicPluginStart } from '../../navigation/public';
export interface DiscoverServices {
addBasePath: (path: string) => string;
@ -58,6 +59,7 @@ export interface DiscoverServices {
indexPatterns: IndexPatternsContract;
inspector: InspectorPublicPluginStart;
metadata: { branch: string };
navigation: NavigationPublicPluginStart;
share?: SharePluginStart;
kibanaLegacy: KibanaLegacyStart;
urlForwarding: UrlForwardingStart;
@ -65,6 +67,7 @@ export interface DiscoverServices {
toastNotifications: ToastsStart;
getSavedSearchById: (id: string) => Promise<SavedSearch>;
getSavedSearchUrlById: (id: string) => Promise<string>;
getEmbeddableInjector: any;
uiSettings: IUiSettingsClient;
visualizations: VisualizationsStart;
}
@ -72,7 +75,8 @@ export interface DiscoverServices {
export async function buildServices(
core: CoreStart,
plugins: DiscoverStartPlugins,
context: PluginInitializerContext
context: PluginInitializerContext,
getEmbeddableInjector: any
): Promise<DiscoverServices> {
const services: SavedObjectKibanaServices = {
savedObjectsClient: core.savedObjects.client,
@ -92,6 +96,7 @@ export async function buildServices(
docLinks: core.docLinks,
theme: plugins.charts.theme,
filterManager: plugins.data.query.filterManager,
getEmbeddableInjector,
getSavedSearchById: async (id: string) => savedObjectService.get(id),
getSavedSearchUrlById: async (id: string) => savedObjectService.urlFor(id),
history: getHistory,
@ -100,6 +105,7 @@ export async function buildServices(
metadata: {
branch: context.env.packageInfo.branch,
},
navigation: plugins.navigation,
share: plugins.share,
kibanaLegacy: plugins.kibanaLegacy,
urlForwarding: plugins.urlForwarding,

View file

@ -40,16 +40,10 @@ import { createTableRowDirective } from './application/angular/doc_table/compone
import { createPagerFactory } from './application/angular/doc_table/lib/pager/pager_factory';
import { createInfiniteScrollDirective } from './application/angular/doc_table/infinite_scroll';
import { createDocViewerDirective } from './application/angular/doc_viewer';
import { CollapsibleSidebarProvider } from './application/angular/directives/collapsible_sidebar/collapsible_sidebar';
// @ts-ignore
import { FixedScrollProvider } from './application/angular/directives/fixed_scroll';
// @ts-ignore
import { DebounceProviderTimeout } from './application/angular/directives/debounce/debounce';
import { createRenderCompleteDirective } from './application/angular/directives/render_complete';
import {
initAngularBootstrap,
configureAppAngularModule,
KbnAccessibleClickProvider,
PrivateProvider,
PromiseServiceCreator,
registerListenEventListener,
@ -57,14 +51,10 @@ import {
createTopNavDirective,
createTopNavHelper,
} from '../../kibana_legacy/public';
import { createDiscoverSidebarDirective } from './application/components/sidebar';
import { createHitsCounterDirective } from '././application/components/hits_counter';
import { createLoadingSpinnerDirective } from '././application/components/loading_spinner/loading_spinner';
import { createTimechartHeaderDirective } from './application/components/timechart_header';
import { createContextErrorMessageDirective } from './application/components/context_error_message';
import { DiscoverStartPlugins } from './plugin';
import { getScopedHistory } from './kibana_services';
import { createSkipBottomButtonDirective } from './application/components/skip_bottom_button';
import { createDiscoverLegacyDirective } from './application/components/create_discover_legacy_directive';
/**
* returns the main inner angular module, it contains all the parts of Angular Discover
@ -88,11 +78,9 @@ export function getInnerAngularModule(
export function getInnerAngularModuleEmbeddable(
name: string,
core: CoreStart,
deps: DiscoverStartPlugins,
context: PluginInitializerContext
deps: DiscoverStartPlugins
) {
const module = initializeInnerAngularModule(name, core, deps.navigation, deps.data, true);
return module;
return initializeInnerAngularModule(name, core, deps.navigation, deps.data, true);
}
let initialized = false;
@ -129,8 +117,7 @@ export function initializeInnerAngularModule(
])
.config(watchMultiDecorator)
.directive('icon', (reactDirective) => reactDirective(EuiIcon))
.directive('renderComplete', createRenderCompleteDirective)
.service('debounce', ['$timeout', DebounceProviderTimeout]);
.directive('renderComplete', createRenderCompleteDirective);
}
return angular
@ -149,18 +136,9 @@ export function initializeInnerAngularModule(
])
.config(watchMultiDecorator)
.run(registerListenEventListener)
.directive('icon', (reactDirective) => reactDirective(EuiIcon))
.directive('kbnAccessibleClick', KbnAccessibleClickProvider)
.directive('collapsibleSidebar', CollapsibleSidebarProvider)
.directive('fixedScroll', FixedScrollProvider)
.directive('renderComplete', createRenderCompleteDirective)
.directive('discoverSidebar', createDiscoverSidebarDirective)
.directive('skipBottomButton', createSkipBottomButtonDirective)
.directive('hitsCounter', createHitsCounterDirective)
.directive('loadingSpinner', createLoadingSpinnerDirective)
.directive('timechartHeader', createTimechartHeaderDirective)
.directive('contextErrorMessage', createContextErrorMessageDirective)
.service('debounce', ['$timeout', DebounceProviderTimeout]);
.directive('discoverLegacy', createDiscoverLegacyDirective)
.directive('contextErrorMessage', createContextErrorMessageDirective);
}
function createLocalPromiseModule() {

View file

@ -327,7 +327,12 @@ export class DiscoverPlugin
if (this.servicesInitialized) {
return { core, plugins };
}
const services = await buildServices(core, plugins, this.initializerContext);
const services = await buildServices(
core,
plugins,
this.initializerContext,
this.getEmbeddableInjector
);
setServices(services);
this.servicesInitialized = true;
@ -380,12 +385,7 @@ export class DiscoverPlugin
const { core, plugins } = await this.initializeServices();
getServices().kibanaLegacy.loadFontAwesome();
const { getInnerAngularModuleEmbeddable } = await import('./get_inner_angular');
getInnerAngularModuleEmbeddable(
embeddableAngularName,
core,
plugins,
this.initializerContext
);
getInnerAngularModuleEmbeddable(embeddableAngularName, core, plugins);
const mountpoint = document.createElement('div');
this.embeddableInjector = angular.bootstrap(mountpoint, [embeddableAngularName]);
}

View file

@ -28,6 +28,7 @@ export interface SavedSearch {
columns: string[];
sort: SortOrder[];
destroy: () => void;
lastSavedTitle?: string;
}
export interface SavedSearchLoader {
get: (id: string) => Promise<SavedSearch>;

View file

@ -254,7 +254,7 @@ export function DiscoverPageProvider({ getService, getPageObjects }: FtrProvider
}
public async getSidebarWidth() {
const sidebar = await find.byCssSelector('.sidebar-list');
const sidebar = await testSubjects.find('discover-sidebar');
return await sidebar.getAttribute('clientWidth');
}

View file

@ -36,7 +36,7 @@ filter-bar,
/* hide unusable controls */
discover-app .dscTimechart,
discover-app .dscSidebar__container,
discover-app .kbnCollapsibleSidebar__collapseButton,
discover-app .dscCollapsibleSidebar__collapseButton,
discover-app .discover-table-footer {
display: none;
}

View file

@ -35,7 +35,7 @@ filter-bar,
/* hide unusable controls */
discover-app .dscTimechart,
discover-app .dscSidebar__container,
discover-app .kbnCollapsibleSidebar__collapseButton,
discover-app .dscCollapsibleSidebar__collapseButton,
discover-app .discover-table-footer {
display: none;
}

View file

@ -1430,10 +1430,7 @@
"discover.localMenu.saveTitle": "保存",
"discover.localMenu.shareSearchDescription": "検索を共有します",
"discover.localMenu.shareTitle": "共有",
"discover.noResults.addressShardFailuresTitle": "シャードエラーの解決",
"discover.noResults.expandYourTimeRangeTitle": "時間範囲を拡大",
"discover.noResults.indexFailureIndexText": "インデックス {failureIndex}",
"discover.noResults.indexFailureShardText": "{index}、シャード {failureShard}",
"discover.noResults.queryMayNotMatchTitle": "1つ以上の表示されているインデックスに日付フィールドが含まれています。クエリが現在の時間範囲のデータと一致しないか、現在選択された時間範囲にデータが全く存在しない可能性があります。データが存在する時間範囲に変えることができます。",
"discover.noResults.searchExamples.400to499StatusCodeExampleTitle": "400-499のすべてのステータスコードを検索",
"discover.noResults.searchExamples.400to499StatusCodeWithPhpExtensionExampleTitle": "400-499のphp拡張子のステータスコードを検索",
@ -1444,7 +1441,6 @@
"discover.noResults.searchExamples.queryStringSyntaxLinkText": "クエリ文字列の構文",
"discover.noResults.searchExamples.refineYourQueryTitle": "クエリの調整",
"discover.noResults.searchExamples.statusField200StatusCodeExampleTitle": "ステータスフィールドの200を検索",
"discover.noResults.shardFailuresDescription": "次のシャードエラーが発生しました。",
"discover.notifications.invalidTimeRangeText": "指定された時間範囲が無効です。(開始:'{from}'、終了:'{to}'",
"discover.notifications.invalidTimeRangeTitle": "無効な時間範囲",
"discover.notifications.notSavedSearchTitle": "検索「{savedSearchTitle}」は保存されませんでした。",

View file

@ -1431,10 +1431,7 @@
"discover.localMenu.saveTitle": "保存",
"discover.localMenu.shareSearchDescription": "共享搜索",
"discover.localMenu.shareTitle": "共享",
"discover.noResults.addressShardFailuresTitle": "解决分片错误",
"discover.noResults.expandYourTimeRangeTitle": "展开时间范围",
"discover.noResults.indexFailureIndexText": "索引 {failureIndex}",
"discover.noResults.indexFailureShardText": "{index},分片 {failureShard}",
"discover.noResults.queryMayNotMatchTitle": "您正在查看的一个或多个索引包含日期字段。您的查询在当前时间范围内可能不匹配任何数据,也可能在当前选定的时间范围内没有任何数据。您可以尝试将时间范围更改为包含数据的时间范围。",
"discover.noResults.searchExamples.400to499StatusCodeExampleTitle": "查找所有介于 400-499 之间的状态代码",
"discover.noResults.searchExamples.400to499StatusCodeWithPhpExtensionExampleTitle": "查找状态代码 400-499 以及扩展名 php",
@ -1445,7 +1442,6 @@
"discover.noResults.searchExamples.queryStringSyntaxLinkText": "查询字符串语法",
"discover.noResults.searchExamples.refineYourQueryTitle": "优化您的查询",
"discover.noResults.searchExamples.statusField200StatusCodeExampleTitle": "在状态字段中查找 200",
"discover.noResults.shardFailuresDescription": "发生了以下分片错误:",
"discover.notifications.invalidTimeRangeText": "提供的时间范围无效。(起始:“{from}”,结束:“{to}”)",
"discover.notifications.invalidTimeRangeTitle": "时间范围无效",
"discover.notifications.notSavedSearchTitle": "搜索“{savedSearchTitle}”未保存。",