diff --git a/src/plugins/discover/public/application/_discover.scss b/src/plugins/discover/public/application/_discover.scss deleted file mode 100644 index bc704439d161b..0000000000000 --- a/src/plugins/discover/public/application/_discover.scss +++ /dev/null @@ -1,162 +0,0 @@ -.dscAppWrapper { - display: flex; - flex-direction: column; - flex-grow: 1; - overflow: hidden; -} - -.dscAppContainer { - > * { - position: relative; - } -} -discover-app { - flex-grow: 1; -} - -.dscHistogram { - display: flex; - height: 200px; - padding: $euiSizeS; -} - -// SASSTODO: replace the z-index value with a variable -.dscWrapper { - padding-left: $euiSizeXL; - padding-right: $euiSizeS; - z-index: 1; - @include euiBreakpoint('xs', 's', 'm') { - padding-left: $euiSizeS; - } -} - -@include euiPanel('.dscWrapper__content'); - -.dscWrapper__content { - padding-top: $euiSizeXS; - background-color: $euiColorEmptyShade; - - .kbn-table { - margin-bottom: 0; - } -} - -.dscTimechart { - display: block; - position: relative; - - // SASSTODO: the visualizing component should have an option or a modifier - .series > rect { - fill-opacity: 0.5; - stroke-width: 1; - } -} - -.dscResultCount { - padding-top: $euiSizeXS; -} - -.dscTimechart__header { - display: flex; - justify-content: center; - min-height: $euiSizeXXL; - padding: $euiSizeXS 0; -} - -.dscOverlay { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - z-index: 20; - padding-top: $euiSizeM; - - opacity: 0.75; - text-align: center; - background-color: transparent; -} - -.dscTable { - overflow: auto; - - // SASSTODO: add a monospace modifier to the doc-table component - .kbnDocTable__row { - font-family: $euiCodeFontFamily; - font-size: $euiFontSizeXS; - } -} - -// SASSTODO: replace the padding value with a variable -.dscTable__footer { - background-color: $euiColorLightShade; - padding: 5px 10px; - text-align: center; -} - -.dscResults { - h3 { - margin: -20px 0 10px 0; - text-align: center; - } -} - -.dscResults__interval { - display: inline-block; - width: auto; -} - -.dscSkipButton { - position: absolute; - right: $euiSizeM; - top: $euiSizeXS; -} - -.dscTableFixedScroll { - overflow-x: auto; - padding-bottom: 0; - - + .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; - } - } -} diff --git a/src/plugins/discover/public/application/angular/context/components/action_bar/action_bar.tsx b/src/plugins/discover/public/application/angular/context/components/action_bar/action_bar.tsx index d294ffca86341..14e43a8aa203c 100644 --- a/src/plugins/discover/public/application/angular/context/components/action_bar/action_bar.tsx +++ b/src/plugins/discover/public/application/angular/context/components/action_bar/action_bar.tsx @@ -119,7 +119,7 @@ export function ActionBar({ - + ').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($('
').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; - }; -} diff --git a/src/plugins/discover/public/application/angular/directives/fixed_scroll.test.js b/src/plugins/discover/public/application/angular/directives/fixed_scroll.test.js deleted file mode 100644 index e44bb45cf2431..0000000000000 --- a/src/plugins/discover/public/application/angular/directives/fixed_scroll.test.js +++ /dev/null @@ -1,267 +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 angular from 'angular'; -import 'angular-mocks'; -import $ from 'jquery'; - -import sinon from 'sinon'; - -import { initAngularBootstrap } from '../../../../../kibana_legacy/public'; -import { FixedScrollProvider } from './fixed_scroll'; - -const testModuleName = 'fixedScroll'; - -angular.module(testModuleName, []).directive('fixedScroll', FixedScrollProvider); - -describe('FixedScroll directive', function () { - const sandbox = sinon.createSandbox(); - let mockWidth; - let mockHeight; - let currentWidth = 120; - let currentHeight = 120; - let currentJqLiteWidth = 120; - let spyScrollWidth; - - let compile; - let flushPendingTasks; - const trash = []; - - beforeAll(() => { - mockWidth = jest.spyOn($.prototype, 'width').mockImplementation(function (width) { - if (width === undefined) { - return currentWidth; - } else { - currentWidth = width; - return this; - } - }); - mockHeight = jest.spyOn($.prototype, 'height').mockImplementation(function (height) { - if (height === undefined) { - return currentHeight; - } else { - currentHeight = height; - return this; - } - }); - angular.element.prototype.width = jest.fn(function (width) { - if (width === undefined) { - return currentJqLiteWidth; - } else { - currentJqLiteWidth = width; - return this; - } - }); - angular.element.prototype.offset = jest.fn(() => ({ top: 0 })); - }); - - beforeEach(() => { - currentJqLiteWidth = 120; - initAngularBootstrap(); - - angular.mock.module(testModuleName); - angular.mock.inject(($compile, $rootScope, $timeout) => { - flushPendingTasks = function flushPendingTasks() { - $rootScope.$digest(); - $timeout.flush(); - }; - - compile = function (ratioY, ratioX) { - if (ratioX == null) ratioX = ratioY; - - // since the directive works at the sibling level we create a - // parent for everything to happen in - const $parent = $('
').css({ - position: 'fixed', - top: 0, - left: 0, - right: 0, - bottom: 0, - }); - - $parent.appendTo(document.body); - trash.push($parent); - - const $el = $('
') - .css({ - 'overflow-x': 'auto', - width: $parent.width(), - }) - .appendTo($parent); - - spyScrollWidth = jest.spyOn(window.HTMLElement.prototype, 'scrollWidth', 'get'); - spyScrollWidth.mockReturnValue($parent.width() * ratioX); - angular.element.prototype.height = jest.fn(() => $parent.height() * ratioY); - - const $content = $('
') - .css({ - width: $parent.width() * ratioX, - height: $parent.height() * ratioY, - }) - .appendTo($el); - - $compile($parent)($rootScope); - flushPendingTasks(); - - return { - $container: $el, - $content: $content, - $scroller: $parent.find('.dscTableFixedScroll__scroller'), - }; - }; - }); - }); - - afterEach(function () { - trash.splice(0).forEach(function ($el) { - $el.remove(); - }); - - sandbox.restore(); - spyScrollWidth.mockRestore(); - }); - - afterAll(() => { - mockWidth.mockRestore(); - mockHeight.mockRestore(); - delete angular.element.prototype.width; - delete angular.element.prototype.height; - delete angular.element.prototype.offset; - }); - - test('does nothing when not needed', function () { - let els = compile(0.5, 1.5); - expect(els.$scroller).toHaveLength(0); - - els = compile(1.5, 0.5); - expect(els.$scroller).toHaveLength(0); - }); - - test('attaches a scroller below the element when the content is larger then the container', function () { - const els = compile(1.5); - expect(els.$scroller.length).toBe(1); - }); - - test('copies the width of the container', function () { - const els = compile(1.5); - expect(els.$scroller.width()).toBe(els.$container.width()); - }); - - test('mimics the scrollWidth of the element', function () { - const els = compile(1.5); - expect(els.$scroller.prop('scrollWidth')).toBe(els.$container.prop('scrollWidth')); - }); - - describe('scroll event handling / tug of war prevention', function () { - test('listens when needed, unlistens when not needed', function (done) { - const on = sandbox.spy($.fn, 'on'); - const off = sandbox.spy($.fn, 'off'); - const jqLiteOn = sandbox.spy(angular.element.prototype, 'on'); - const jqLiteOff = sandbox.spy(angular.element.prototype, 'off'); - - const els = compile(1.5); - expect(on.callCount).toBe(1); - expect(jqLiteOn.callCount).toBe(1); - checkThisVals('$.fn.on', on, jqLiteOn); - - expect(off.callCount).toBe(0); - expect(jqLiteOff.callCount).toBe(0); - currentJqLiteWidth = els.$container.prop('scrollWidth'); - flushPendingTasks(); - expect(off.callCount).toBe(1); - expect(jqLiteOff.callCount).toBe(1); - checkThisVals('$.fn.off', off, jqLiteOff); - done(); - - function checkThisVals(namejQueryFn, spyjQueryFn, spyjqLiteFn) { - // the this values should be different - expect(spyjQueryFn.thisValues[0].is(spyjqLiteFn.thisValues[0])).toBeFalsy(); - // but they should be either $scroller or $container - const el = spyjQueryFn.thisValues[0]; - - if (el.is(els.$scroller) || el.is(els.$container)) return; - - done.fail('expected ' + namejQueryFn + ' to be called with $scroller or $container'); - } - }); - - // Turn off this row because tests failed. - // Scroll event is not catched in fixed_scroll. - // As container is jquery element in test but inside fixed_scroll it's a jqLite element. - // it would need jquery in jest to make this work. - [ - //{ from: '$container', to: '$scroller' }, - { from: '$scroller', to: '$container' }, - ].forEach(function (names) { - describe('scroll events ' + JSON.stringify(names), function () { - let spyJQueryScrollLeft; - let spyJQLiteScrollLeft; - let els; - let $from; - let $to; - - beforeEach(function () { - spyJQueryScrollLeft = sandbox.spy($.fn, 'scrollLeft'); - spyJQLiteScrollLeft = sandbox.stub(); - angular.element.prototype.scrollLeft = spyJQLiteScrollLeft; - els = compile(1.5); - $from = els[names.from]; - $to = els[names.to]; - }); - - afterAll(() => { - delete angular.element.prototype.scrollLeft; - }); - - test('transfers the scrollLeft', function () { - expect(spyJQueryScrollLeft.callCount).toBe(0); - expect(spyJQLiteScrollLeft.callCount).toBe(0); - $from.scroll(); - expect(spyJQueryScrollLeft.callCount).toBe(1); - expect(spyJQLiteScrollLeft.callCount).toBe(1); - - // first call should read the scrollLeft from the $container - const firstCall = spyJQueryScrollLeft.getCall(0); - expect(firstCall.args).toEqual([]); - - // second call should be setting the scrollLeft on the $scroller - const secondCall = spyJQLiteScrollLeft.getCall(0); - expect(secondCall.args).toEqual([firstCall.returnValue]); - }); - - /** - * In practice, calling $el.scrollLeft() causes the "scroll" event to trigger, - * but the browser seems to be very careful about triggering the event too much - * and I can't reliably recreate the browsers behavior in a test. So... faking it! - */ - test('prevents tug of war by ignoring echo scroll events', function () { - $from.scroll(); - expect(spyJQueryScrollLeft.callCount).toBe(1); - expect(spyJQLiteScrollLeft.callCount).toBe(1); - - spyJQueryScrollLeft.resetHistory(); - spyJQLiteScrollLeft.resetHistory(); - $to.scroll(); - expect(spyJQueryScrollLeft.callCount).toBe(0); - expect(spyJQLiteScrollLeft.callCount).toBe(0); - }); - }); - }); - }); -}); diff --git a/src/plugins/discover/public/application/angular/directives/uninitialized.tsx b/src/plugins/discover/public/application/angular/directives/uninitialized.tsx index d04aea0933115..f2b1f584224ef 100644 --- a/src/plugins/discover/public/application/angular/directives/uninitialized.tsx +++ b/src/plugins/discover/public/application/angular/directives/uninitialized.tsx @@ -20,7 +20,7 @@ import React from 'react'; import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; -import { EuiButton, EuiEmptyPrompt, EuiPage, EuiPageBody, EuiPageContent } from '@elastic/eui'; +import { EuiButton, EuiEmptyPrompt } from '@elastic/eui'; interface Props { onRefresh: () => void; @@ -29,39 +29,30 @@ interface Props { export const DiscoverUninitialized = ({ onRefresh }: Props) => { return ( - - - - - - - } - body={ -

- -

- } - actions={ - - - - } + + + + } + body={ +

+ - - - +

+ } + actions={ + + + + } + />
); }; diff --git a/src/plugins/discover/public/application/angular/discover.js b/src/plugins/discover/public/application/angular/discover.js index d0340c2cf4edd..2c3b8fd9606a9 100644 --- a/src/plugins/discover/public/application/angular/discover.js +++ b/src/plugins/discover/public/application/angular/discover.js @@ -33,7 +33,6 @@ import { syncQueryStateWithUrl, } from '../../../../data/public'; import { getSortArray } from './doc_table'; -import { createFixedScroll } from './directives/fixed_scroll'; import * as columnActions from './doc_table/actions/columns'; import indexTemplateLegacy from './discover_legacy.html'; import { addHelpMenuToAppChrome } from '../components/help_menu/help_menu_util'; @@ -181,7 +180,7 @@ app.directive('discoverApp', function () { }; }); -function discoverController($element, $route, $scope, $timeout, $window, Promise, uiCapabilities) { +function discoverController($element, $route, $scope, $timeout, Promise, uiCapabilities) { const { isDefault: isDefaultType } = indexPatternsUtils; const subscriptions = new Subscription(); const refetch$ = new Subject(); @@ -434,7 +433,6 @@ function discoverController($element, $route, $scope, $timeout, $window, Promise savedSearch: savedSearch, indexPatternList: $route.current.locals.savedObjects.ip.list, config: config, - fixedScroll: createFixedScroll($scope, $timeout), setHeaderActionMenu: getHeaderActionMenuMounter(), data, }; diff --git a/src/plugins/discover/public/application/angular/doc_table/infinite_scroll.ts b/src/plugins/discover/public/application/angular/doc_table/infinite_scroll.ts index 1d38d0fc534d1..f7f7d4dd90eaf 100644 --- a/src/plugins/discover/public/application/angular/doc_table/infinite_scroll.ts +++ b/src/plugins/discover/public/application/angular/doc_table/infinite_scroll.ts @@ -30,19 +30,26 @@ export function createInfiniteScrollDirective() { more: '=', }, link: ($scope: LazyScope, $element: JQuery) => { - const $window = $(window); let checkTimer: any; + /** + * depending on which version of Discover is displayed, different elements are scrolling + * and have therefore to be considered for calculation of infinite scrolling + */ + const scrollDiv = $element.parents('.dscTable'); + const scrollDivMobile = $(window); function onScroll() { if (!$scope.more) return; + const isMobileView = document.getElementsByClassName('dscSidebar__mobile').length > 0; + const usedScrollDiv = isMobileView ? scrollDivMobile : scrollDiv; + const scrollTop = usedScrollDiv.scrollTop(); - const winHeight = Number($window.height()); - const winBottom = Number(winHeight) + Number($window.scrollTop()); - const offset = $element.offset(); - const elTop = offset ? offset.top : 0; + const winHeight = Number(usedScrollDiv.height()); + const winBottom = Number(winHeight) + Number(scrollTop); + const elTop = $element.get(0).offsetTop || 0; const remaining = elTop - winBottom; - if (remaining <= winHeight * 0.5) { + if (remaining <= winHeight) { $scope[$scope.$$phase ? '$eval' : '$apply'](function () { $scope.more(); }); @@ -57,10 +64,12 @@ export function createInfiniteScrollDirective() { }, 50); } - $window.on('scroll', scheduleCheck); + scrollDiv.on('scroll', scheduleCheck); + window.addEventListener('scroll', scheduleCheck); $scope.$on('$destroy', function () { clearTimeout(checkTimer); - $window.off('scroll', scheduleCheck); + scrollDiv.off('scroll', scheduleCheck); + window.removeEventListener('scroll', scheduleCheck); }); scheduleCheck(); }, diff --git a/src/plugins/discover/public/application/angular/doc_table/lib/get_sort.ts b/src/plugins/discover/public/application/angular/doc_table/lib/get_sort.ts index 73ae691529e2b..2605ec5bf6745 100644 --- a/src/plugins/discover/public/application/angular/doc_table/lib/get_sort.ts +++ b/src/plugins/discover/public/application/angular/doc_table/lib/get_sort.ts @@ -76,6 +76,12 @@ export function getSort(sort: SortPair[] | SortPair, indexPattern: IndexPattern) * compared to getSort it doesn't return an array of objects, it returns an array of arrays * [[fieldToSort: directionToSort]] */ -export function getSortArray(sort: SortPair[], indexPattern: IndexPattern) { - return getSort(sort, indexPattern).map((sortPair) => Object.entries(sortPair).pop()); +export function getSortArray(sort: SortPair[], indexPattern: IndexPattern): SortPairArr[] { + return getSort(sort, indexPattern).reduce((acc: SortPairArr[], sortPair) => { + const entries = Object.entries(sortPair); + if (entries && entries[0]) { + acc.push(entries[0]); + } + return acc; + }, []); } diff --git a/src/plugins/discover/public/application/components/context_app/context_app_legacy.scss b/src/plugins/discover/public/application/components/context_app/context_app_legacy.scss deleted file mode 100644 index 87194d834827b..0000000000000 --- a/src/plugins/discover/public/application/components/context_app/context_app_legacy.scss +++ /dev/null @@ -1,5 +0,0 @@ -.dscCxtAppContent { - border: none; - background-color: transparent; - box-shadow: none; -} diff --git a/src/plugins/discover/public/application/components/context_app/context_app_legacy.tsx b/src/plugins/discover/public/application/components/context_app/context_app_legacy.tsx index b5387ec51db81..af99c995c60eb 100644 --- a/src/plugins/discover/public/application/components/context_app/context_app_legacy.tsx +++ b/src/plugins/discover/public/application/components/context_app/context_app_legacy.tsx @@ -16,10 +16,9 @@ * specific language governing permissions and limitations * under the License. */ -import './context_app_legacy.scss'; import React from 'react'; import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; -import { EuiPanel, EuiText, EuiPageContent, EuiPage } from '@elastic/eui'; +import { EuiHorizontalRule, EuiText, EuiPageContent, EuiPage } from '@elastic/eui'; import { ContextErrorMessage } from '../context_error_message'; import { DocTableLegacy, @@ -100,14 +99,9 @@ export function ContextAppLegacy(renderProps: ContextAppProps) { const loadingFeedback = () => { if (status === LOADING_STATUS.UNINITIALIZED || status === LOADING_STATUS.LOADING) { return ( - - - - - + + + ); } return null; @@ -122,13 +116,13 @@ export function ContextAppLegacy(renderProps: ContextAppProps) { {loadingFeedback()} + {isLoaded ? ( - -
- -
-
+
+ +
) : null} +
diff --git a/src/plugins/discover/public/application/components/discover.scss b/src/plugins/discover/public/application/components/discover.scss new file mode 100644 index 0000000000000..b17da97a45930 --- /dev/null +++ b/src/plugins/discover/public/application/components/discover.scss @@ -0,0 +1,91 @@ +discover-app { + flex-grow: 1; +} + +.dscPage { + @include euiBreakpoint('m', 'l', 'xl') { + height: calc(100vh - #{($euiHeaderHeightCompensation * 2)}); + } + + flex-direction: column; + overflow: hidden; + padding: 0; + + .dscPageBody { + overflow: hidden; + } +} + +.dscPageBody__inner { + overflow: hidden; + height: 100%; +} + +.dscPageBody__contents { + overflow: hidden; + padding-top: $euiSizeXS / 2; // A little breathing room for the index pattern button +} + +.dscPageContent__wrapper { + padding: 0 $euiSize $euiSize 0; + overflow: hidden; // Ensures horizontal scroll of table + + @include euiBreakpoint('xs', 's') { + padding: 0 $euiSize $euiSize; + } +} + +.dscPageContent, +.dscPageContent__inner { + height: 100%; +} + +.dscPageContent--centered { + height: auto; +} + +.dscResultCount { + padding: $euiSizeS; + + @include euiBreakpoint('xs', 's') { + .dscResultCount__toggle { + align-items: flex-end; + } + + .dscResuntCount__title, + .dscResultCount__actions { + margin-bottom: 0 !important; + } + } +} + +.dscTimechart { + display: block; + position: relative; + + // SASSTODO: the visualizing component should have an option or a modifier + .series > rect { + fill-opacity: 0.5; + stroke-width: 1; + } +} + +.dscHistogram { + display: flex; + height: $euiSize * 12.5; + padding: $euiSizeS; +} + +.dscTable { + // SASSTODO: add a monospace modifier to the doc-table component + .kbnDocTable__row { + font-family: $euiCodeFontFamily; + font-size: $euiFontSizeXS; + } +} + +.dscTable__footer { + background-color: $euiColorLightShade; + padding: $euiSizeXS $euiSizeS; + text-align: center; +} diff --git a/src/plugins/discover/public/application/components/discover_legacy.tsx b/src/plugins/discover/public/application/components/discover_legacy.tsx index e9de4c08a177b..56f8fa46a9f69 100644 --- a/src/plugins/discover/public/application/components/discover_legacy.tsx +++ b/src/plugins/discover/public/application/components/discover_legacy.tsx @@ -16,23 +16,32 @@ * 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 './discover.scss'; + +import React, { useState, useRef } from 'react'; +import { + EuiButtonEmpty, + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, + EuiHideFor, + EuiPage, + EuiPageBody, + EuiPageContent, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; import { IUiSettingsClient, MountPoint } from 'kibana/public'; +import classNames from 'classnames'; import { HitsCounter } from './hits_counter'; import { TimechartHeader } from './timechart_header'; -import { DiscoverSidebar } from './sidebar'; import { getServices, IndexPattern } from '../../kibana_services'; import { DiscoverUninitialized, DiscoverHistogram } from '../angular/directives'; import { DiscoverNoResults } from './no_results'; import { LoadingSpinner } from './loading_spinner/loading_spinner'; -import { DocTableLegacy } from '../angular/doc_table/create_doc_table_react'; +import { DocTableLegacy, DocTableLegacyProps } from '../angular/doc_table/create_doc_table_react'; import { SkipBottomButton } from './skip_bottom_button'; import { - IndexPatternField, search, ISearchSource, TimeRange, @@ -40,15 +49,20 @@ import { IndexPatternAttributes, DataPublicPluginStart, AggConfigs, + FilterManager, } 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 { TopNavMenuData } from '../../../../navigation/public'; +import { + DiscoverSidebarResponsive, + DiscoverSidebarResponsiveProps, +} from './sidebar/discover_sidebar_responsive'; +import { DocViewFilterFn, ElasticSearchHit } from '../doc_views/doc_views_types'; -export interface DiscoverLegacyProps { +export interface DiscoverProps { addColumn: (column: string) => void; fetch: () => void; fetchCounter: number; @@ -58,7 +72,7 @@ export interface DiscoverLegacyProps { hits: number; indexPattern: IndexPattern; minimumVisibleRows: number; - onAddFilter: (field: IndexPatternField | string, value: string, type: '+' | '-') => void; + onAddFilter: DocViewFilterFn; onChangeInterval: (interval: string) => void; onMoveColumn: (columns: string, newIdx: number) => void; onRemoveColumn: (column: string) => void; @@ -70,15 +84,17 @@ export interface DiscoverLegacyProps { config: IUiSettingsClient; data: DataPublicPluginStart; fixedScroll: (el: HTMLElement) => void; + filterManager: FilterManager; indexPatternList: Array>; sampleSize: number; savedSearch: SavedSearch; setHeaderActionMenu: (menuMount: MountPoint | undefined) => void; timefield: string; + setAppState: (state: Partial) => void; }; resetQuery: () => void; resultState: string; - rows: Array>; + rows: ElasticSearchHit[]; searchSource: ISearchSource; setIndexPattern: (id: string) => void; showSaveQuery: boolean; @@ -90,6 +106,13 @@ export interface DiscoverLegacyProps { updateSavedQueryId: (savedQueryId?: string) => void; } +export const DocTableLegacyMemoized = React.memo((props: DocTableLegacyProps) => ( + +)); +export const SidebarMemoized = React.memo((props: DiscoverSidebarResponsiveProps) => ( + +)); + export function DiscoverLegacy({ addColumn, fetch, @@ -119,43 +142,30 @@ export function DiscoverLegacy({ topNavMenu, updateQuery, updateSavedQueryId, -}: DiscoverLegacyProps) { +}: DiscoverProps) { + const scrollableDesktop = useRef(null); + const collapseIcon = useRef(null); + const isMobile = () => { + // collapse icon isn't displayed in mobile view, use it to detect which view is displayed + return collapseIcon && !collapseIcon.current; + }; + + const [toggleOn, toggleChart] = useState(true); const [isSidebarClosed, setIsSidebarClosed] = useState(false); - const { TopNavMenu } = getServices().navigation.ui; - const { trackUiMetric } = getServices(); + const services = getServices(); + const { TopNavMenu } = services.navigation.ui; + const { trackUiMetric } = services; const { savedSearch, indexPatternList } = opts; const bucketAggConfig = opts.chartAggConfigs?.aggs[1]; const bucketInterval = bucketAggConfig && search.aggs.isDateHistogramBucketAggConfig(bucketAggConfig) ? bucketAggConfig.buckets?.getInterval() : undefined; - const [fixedScrollEl, setFixedScrollEl] = useState(); - - 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, - }); + const contentCentered = resultState === 'uninitialized'; return ( -
-

{savedSearch.title}

+ -
-
-
- {!isSidebarClosed && ( -
- -
- )} - setIsSidebarClosed(!isSidebarClosed)} - data-test-subj="collapseSideBarButton" - aria-controls="discover-sidebar" - aria-expanded={isSidebarClosed ? 'false' : 'true'} - aria-label="Toggle sidebar" - className="dscCollapsibleSidebar__collapseButton" + +

+ {savedSearch.title} +

+ + + -
-
- {resultState === 'none' && ( - + + + setIsSidebarClosed(!isSidebarClosed)} + data-test-subj="collapseSideBarButton" + aria-controls="discover-sidebar" + aria-expanded={isSidebarClosed ? 'false' : 'true'} + aria-label="Toggle sidebar" + buttonRef={collapseIcon} /> - )} - {resultState === 'uninitialized' && } - {resultState === 'loading' && } - {resultState === 'ready' && ( -
- - 0 ? hits : 0} - showResetButton={!!(savedSearch && savedSearch.id)} - onResetQuery={resetQuery} + + + + + {resultState === 'none' && ( + - {opts.timefield && ( - - )} - - {opts.timefield && ( -
- {opts.chartAggConfigs && rows.length !== 0 && ( -
- -
- )} -
- )} - -
-
-

- -

- {rows && rows.length && ( -
- } + {resultState === 'loading' && } + {resultState === 'ready' && ( + + + + + 0 ? hits : 0} + showResetButton={!!(savedSearch && savedSearch.id)} + onResetQuery={resetQuery} /> - - ​ - - {rows.length === opts.sampleSize && ( -
- + {toggleOn && ( + + + + )} + + { + toggleChart(!toggleOn); + }} + > + {toggleOn + ? i18n.translate('discover.hideChart', { + defaultMessage: 'Hide chart', + }) + : i18n.translate('discover.showChart', { + defaultMessage: 'Show chart', + })} + + + + + + {toggleOn && opts.timefield && ( + +
+ {opts.chartAggConfigs && rows.length !== 0 && ( +
+ +
+ )} +
+
+ )} - window.scrollTo(0, 0)}> + +
+

+ +

+ {rows && rows.length && ( +
+ + {rows.length === opts.sampleSize ? ( +
- -
- )} -
- )} -
-
-
- )} -
-
-
-
+ + { + if (scrollableDesktop && scrollableDesktop.current) { + scrollableDesktop.current.focus(); + } + // Only the desktop one needs to target a specific container + if (!isMobile() && scrollableDesktop.current) { + scrollableDesktop.current.scrollTo(0, 0); + } else if (window) { + window.scrollTo(0, 0); + } + }} + > + + +
+ ) : ( + + ​ + + )} +
+ )} + + + + )} + + + + + ); } diff --git a/src/plugins/discover/public/application/components/doc/doc.tsx b/src/plugins/discover/public/application/components/doc/doc.tsx index 2623b5a270a31..d43a09bd51c6a 100644 --- a/src/plugins/discover/public/application/components/doc/doc.tsx +++ b/src/plugins/discover/public/application/components/doc/doc.tsx @@ -18,7 +18,7 @@ */ import React from 'react'; import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; -import { EuiCallOut, EuiLink, EuiLoadingSpinner, EuiPageContent } from '@elastic/eui'; +import { EuiCallOut, EuiLink, EuiLoadingSpinner, EuiPageContent, EuiPage } from '@elastic/eui'; import { IndexPatternsContract } from 'src/plugins/data/public'; import { ElasticRequestState, useEsDocSearch } from './use_es_doc_search'; import { getServices } from '../../../kibana_services'; @@ -49,84 +49,86 @@ export function Doc(props: DocProps) { return ( - - {reqState === ElasticRequestState.NotFoundIndexPattern && ( - - } - /> - )} - {reqState === ElasticRequestState.NotFound && ( - - } - > - + + {reqState === ElasticRequestState.NotFoundIndexPattern && ( + + } /> - - )} - - {reqState === ElasticRequestState.Error && ( - + } + > - } - > - {' '} - + )} + + {reqState === ElasticRequestState.Error && ( + + } > - - - )} + id="discover.doc.somethingWentWrongDescription" + defaultMessage="{indexName} is missing." + values={{ indexName: props.index }} + />{' '} + + + + + )} - {reqState === ElasticRequestState.Loading && ( - - {' '} - - - )} + {reqState === ElasticRequestState.Loading && ( + + {' '} + + + )} - {reqState === ElasticRequestState.Found && hit !== null && indexPattern && ( -
- -
- )} -
+ {reqState === ElasticRequestState.Found && hit !== null && indexPattern && ( +
+ +
+ )} + +
); } diff --git a/src/plugins/discover/public/application/components/doc_viewer/doc_viewer.scss b/src/plugins/discover/public/application/components/doc_viewer/doc_viewer.scss index ec2beca15a546..b6b7a244bd1f6 100644 --- a/src/plugins/discover/public/application/components/doc_viewer/doc_viewer.scss +++ b/src/plugins/discover/public/application/components/doc_viewer/doc_viewer.scss @@ -1,5 +1,8 @@ .kbnDocViewerTable { margin-top: $euiSizeS; + @include euiBreakpoint('xs', 's') { + table-layout: fixed; + } } .kbnDocViewer { @@ -11,10 +14,10 @@ white-space: pre-wrap; color: $euiColorFullShade; vertical-align: top; - padding-top: 2px; + padding-top: $euiSizeXS * 0.5; } .kbnDocViewer__field { - padding-top: 8px; + padding-top: $euiSizeS; } .dscFieldName { @@ -42,10 +45,9 @@ white-space: nowrap; } .kbnDocViewer__buttons { - width: 60px; + width: 96px; // Show all icons if one is focused, - // IE doesn't support, but the fallback is just the focused button becomes visible &:focus-within { .kbnDocViewer__actionButton { opacity: 1; @@ -54,11 +56,16 @@ } .kbnDocViewer__field { - width: 160px; + width: $euiSize * 10; + @include euiBreakpoint('xs', 's') { + width: $euiSize * 6; + } } .kbnDocViewer__actionButton { - opacity: 0; + @include euiBreakpoint('m', 'l', 'xl') { + opacity: 0; + } &:focus { opacity: 1; @@ -68,4 +75,3 @@ .kbnDocViewer__warning { margin-right: $euiSizeS; } - diff --git a/src/plugins/discover/public/application/components/field_name/field_name.tsx b/src/plugins/discover/public/application/components/field_name/field_name.tsx index b8f664d6cf38a..049557dbe1971 100644 --- a/src/plugins/discover/public/application/components/field_name/field_name.tsx +++ b/src/plugins/discover/public/application/components/field_name/field_name.tsx @@ -30,6 +30,7 @@ interface Props { fieldMapping?: FieldMapping; fieldIconProps?: Omit; scripted?: boolean; + className?: string; } export function FieldName({ @@ -37,6 +38,7 @@ export function FieldName({ fieldMapping, fieldType, fieldIconProps, + className, scripted = false, }: Props) { const typeName = getFieldTypeName(fieldType); @@ -45,7 +47,7 @@ export function FieldName({ const tooltip = displayName !== fieldName ? `${fieldName} (${displayName})` : fieldName; return ( - + diff --git a/src/plugins/discover/public/application/components/hits_counter/hits_counter.scss b/src/plugins/discover/public/application/components/hits_counter/hits_counter.scss new file mode 100644 index 0000000000000..5a3999f129bf4 --- /dev/null +++ b/src/plugins/discover/public/application/components/hits_counter/hits_counter.scss @@ -0,0 +1,3 @@ +.dscHitsCounter { + flex-grow: 0; +} diff --git a/src/plugins/discover/public/application/components/hits_counter/hits_counter.tsx b/src/plugins/discover/public/application/components/hits_counter/hits_counter.tsx index 1d2cd12877b1c..dfd155c3329e4 100644 --- a/src/plugins/discover/public/application/components/hits_counter/hits_counter.tsx +++ b/src/plugins/discover/public/application/components/hits_counter/hits_counter.tsx @@ -16,6 +16,8 @@ * specific language governing permissions and limitations * under the License. */ +import './hits_counter.scss'; + import React from 'react'; import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; @@ -41,8 +43,8 @@ export function HitsCounter({ hits, showResetButton, onResetQuery }: HitsCounter return ( +

diff --git a/src/plugins/discover/public/application/components/no_results/_no_results.scss b/src/plugins/discover/public/application/components/no_results/_no_results.scss index 7ea945e820bf9..6500593d57234 100644 --- a/src/plugins/discover/public/application/components/no_results/_no_results.scss +++ b/src/plugins/discover/public/application/components/no_results/_no_results.scss @@ -1,3 +1,3 @@ .dscNoResults { - max-width: 1000px; + padding: $euiSize; } diff --git a/src/plugins/discover/public/application/components/no_results/no_results.tsx b/src/plugins/discover/public/application/components/no_results/no_results.tsx index fcc2912d16dd5..df28b4795b4fb 100644 --- a/src/plugins/discover/public/application/components/no_results/no_results.tsx +++ b/src/plugins/discover/public/application/components/no_results/no_results.tsx @@ -19,7 +19,7 @@ import React, { Fragment } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiButton, EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; +import { EuiButton, EuiCallOut, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { getServices } from '../../../kibana_services'; import { DataPublicPluginStart } from '../../../../../data/public'; import { getLuceneQueryMessage, getTimeFieldMessage } from './no_results_helper'; @@ -85,7 +85,6 @@ export function DiscoverNoResults({ return ( - {callOut} ); diff --git a/src/plugins/discover/public/application/components/sidebar/change_indexpattern.tsx b/src/plugins/discover/public/application/components/sidebar/change_indexpattern.tsx index e44c05b3a88a9..b997bd961ea7d 100644 --- a/src/plugins/discover/public/application/components/sidebar/change_indexpattern.tsx +++ b/src/plugins/discover/public/application/components/sidebar/change_indexpattern.tsx @@ -20,16 +20,16 @@ import { i18n } from '@kbn/i18n'; import React, { useState } from 'react'; import { - EuiButtonEmpty, + EuiButton, EuiPopover, EuiPopoverTitle, EuiSelectable, - EuiButtonEmptyProps, + EuiButtonProps, } from '@elastic/eui'; import { EuiSelectableProps } from '@elastic/eui/src/components/selectable/selectable'; import { IndexPatternRef } from './types'; -export type ChangeIndexPatternTriggerProps = EuiButtonEmptyProps & { +export type ChangeIndexPatternTriggerProps = EuiButtonProps & { label: string; title?: string; }; @@ -54,9 +54,8 @@ export function ChangeIndexPattern({ const createTrigger = function () { const { label, title, ...rest } = trigger; return ( - setPopoverIsOpen(!isPopoverOpen)} {...rest} > - {label} - + {label} + ); }; @@ -74,8 +73,6 @@ export function ChangeIndexPattern({ button={createTrigger()} isOpen={isPopoverOpen} closePopover={() => setPopoverIsOpen(false)} - className="eui-textTruncate" - anchorClassName="eui-textTruncate" display="block" panelPaddingSize="s" ownFocus diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field.tsx index 35515a6a0e7a5..cc55eaee54893 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_field.tsx @@ -16,18 +16,24 @@ * specific language governing permissions and limitations * under the License. */ +import './discover_field.scss'; + import React, { useState } from 'react'; import { EuiPopover, EuiPopoverTitle, EuiButtonIcon, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { UiStatsMetricType } from '@kbn/analytics'; +import classNames from 'classnames'; import { DiscoverFieldDetails } from './discover_field_details'; import { FieldIcon, FieldButton } from '../../../../../kibana_react/public'; import { FieldDetails } from './types'; import { IndexPatternField, IndexPattern } from '../../../../../data/public'; import { getFieldTypeName } from './lib/get_field_type_name'; -import './discover_field.scss'; export interface DiscoverFieldProps { + /** + * Determines whether add/remove button is displayed not only when focused + */ + alwaysShowActionButton?: boolean; /** * The displayed field */ @@ -66,6 +72,7 @@ export interface DiscoverFieldProps { } export function DiscoverField({ + alwaysShowActionButton = false, field, indexPattern, onAddField, @@ -120,7 +127,9 @@ export function DiscoverField({ {wrapOnDot(field.displayName)} ); - + const actionBtnClassName = classNames('dscSidebarItem__action', { + ['dscSidebarItem__mobile']: alwaysShowActionButton, + }); let actionButton; if (field.name !== '_source' && !selected) { actionButton = ( @@ -132,7 +141,7 @@ export function DiscoverField({ > ) => { if (ev.type === 'click') { ev.currentTarget.focus(); @@ -157,7 +166,7 @@ export function DiscoverField({ ) => { if (ev.type === 'click') { ev.currentTarget.focus(); diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field_details.scss b/src/plugins/discover/public/application/components/sidebar/discover_field_details.scss index f4b3eed741f9f..ca48d67f75dec 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field_details.scss +++ b/src/plugins/discover/public/application/components/sidebar/discover_field_details.scss @@ -1,3 +1,8 @@ +.dscFieldDetails { + color: $euiTextColor; + margin-bottom: $euiSizeS; +} + .dscFieldDetails__visualizeBtn { @include euiFontSizeXS; height: $euiSizeL !important; diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field_search.scss b/src/plugins/discover/public/application/components/sidebar/discover_field_search.scss new file mode 100644 index 0000000000000..4b620f2073771 --- /dev/null +++ b/src/plugins/discover/public/application/components/sidebar/discover_field_search.scss @@ -0,0 +1,7 @@ +.dscFieldSearch__formWrapper { + padding: $euiSizeM; +} + +.dscFieldSearch__filterWrapper { + width: 100%; +} diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field_search.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field_search.test.tsx index 527be8cff9f0c..31928fd367951 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field_search.test.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_field_search.test.tsx @@ -50,17 +50,18 @@ describe('DiscoverFieldSearch', () => { test('change in active filters should change facet selection and call onChange', () => { const onChange = jest.fn(); const component = mountComponent({ ...defaultProps, ...{ onChange } }); - let btn = findTestSubject(component, 'toggleFieldFilterButton'); - expect(btn.hasClass('euiFacetButton--isSelected')).toBeFalsy(); + const btn = findTestSubject(component, 'toggleFieldFilterButton'); + const badge = btn.find('.euiNotificationBadge'); + expect(badge.text()).toEqual('0'); btn.simulate('click'); const aggregatableButtonGroup = findButtonGroup(component, 'aggregatable'); + act(() => { // @ts-ignore (aggregatableButtonGroup.props() as EuiButtonGroupProps).onChange('aggregatable-true', null); }); component.update(); - btn = findTestSubject(component, 'toggleFieldFilterButton'); - expect(btn.hasClass('euiFacetButton--isSelected')).toBe(true); + expect(badge.text()).toEqual('1'); expect(onChange).toBeCalledWith('aggregatable', true); }); diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field_search.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field_search.tsx index a42e2412ae928..60eccefd35006 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field_search.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_field_search.tsx @@ -16,14 +16,15 @@ * specific language governing permissions and limitations * under the License. */ +import './discover_field_search.scss'; + import React, { OptionHTMLAttributes, ReactNode, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { - EuiFacetButton, EuiFieldSearch, + EuiFilterGroup, EuiFlexGroup, EuiFlexItem, - EuiIcon, EuiPopover, EuiPopoverFooter, EuiPopoverTitle, @@ -34,6 +35,8 @@ import { EuiFormRow, EuiButtonGroup, EuiOutsideClickDetector, + EuiFilterButton, + EuiSpacer, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -108,7 +111,7 @@ export function DiscoverFieldSearch({ onChange, value, types }: Props) { defaultMessage: 'Show field filter settings', }); - const handleFacetButtonClicked = () => { + const handleFilterButtonClicked = () => { setPopoverOpen(!isPopoverOpen); }; @@ -162,20 +165,21 @@ export function DiscoverFieldSearch({ onChange, value, types }: Props) { }; const buttonContent = ( - } + iconType="arrowDown" isSelected={activeFiltersCount > 0} - quantity={activeFiltersCount} - onClick={handleFacetButtonClicked} + numFilters={0} + hasActiveFilters={activeFiltersCount > 0} + numActiveFilters={activeFiltersCount} + onClick={handleFilterButtonClicked} > - + ); const select = ( @@ -255,7 +259,6 @@ export function DiscoverFieldSearch({ onChange, value, types }: Props) { onChange('name', event.currentTarget.value)} placeholder={searchPlaceholder} @@ -263,13 +266,14 @@ export function DiscoverFieldSearch({ onChange, value, types }: Props) { /> -
- {}} isDisabled={!isPopoverOpen}> + + {}} isDisabled={!isPopoverOpen}> + { @@ -294,8 +298,8 @@ export function DiscoverFieldSearch({ onChange, value, types }: Props) { /> - -
+ + ); } diff --git a/src/plugins/discover/public/application/components/sidebar/discover_index_pattern.tsx b/src/plugins/discover/public/application/components/sidebar/discover_index_pattern.tsx index 3acdcb1e92091..0bb03492cfc75 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_index_pattern.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_index_pattern.tsx @@ -65,26 +65,23 @@ export function DiscoverIndexPattern({ } return ( -
- - { - const indexPattern = options.find((pattern) => pattern.id === id); - if (indexPattern) { - setIndexPattern(id); - setSelected(indexPattern); - } - }} - /> - -
+ + { + const indexPattern = options.find((pattern) => pattern.id === id); + if (indexPattern) { + setIndexPattern(id); + setSelected(indexPattern); + } + }} + /> + ); } diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.scss b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.scss index f130b0399f467..aaf1743653d7d 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.scss +++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.scss @@ -1,26 +1,37 @@ -.dscSidebar__container { - padding-left: 0 !important; - padding-right: 0 !important; - background-color: transparent; - border-right-color: transparent; - border-bottom-color: transparent; +.dscSidebar { + margin: 0; + flex-grow: 1; + padding-left: $euiSize; + width: $euiSize * 19; + height: 100%; + + @include euiBreakpoint('xs', 's') { + width: 100%; + padding: $euiSize $euiSize 0 $euiSize; + background-color: $euiPageBackgroundColor; + } } -.dscIndexPattern__container { - display: flex; - align-items: center; - height: $euiSize * 3; - margin-top: -$euiSizeS; +.dscSidebar__group { + height: 100%; +} + +.dscSidebar__mobile { + width: 100%; + padding: $euiSize $euiSize 0; + + .dscSidebar__mobileBadge { + margin-left: $euiSizeS; + vertical-align: text-bottom; + } } -.dscIndexPattern__triggerButton { - @include euiTitle('xs'); - line-height: $euiSizeXXL; +.dscSidebar__flyoutHeader { + align-items: center; } .dscFieldList { - list-style: none; - margin-bottom: 0; + padding: 0 $euiSizeXS $euiSizeXS; } .dscFieldListHeader { @@ -29,18 +40,10 @@ } .dscFieldList--popular { + padding-bottom: $euiSizeS; background-color: lightOrDarkTheme(tint($euiColorPrimary, 90%), $euiColorLightShade); } -.dscFieldChooser { - padding-left: $euiSize; -} - -.dscFieldChooser__toggle { - color: $euiColorMediumShade; - margin-left: $euiSizeS !important; -} - .dscSidebarItem { &:hover, &:focus-within, @@ -57,40 +60,12 @@ */ .dscSidebarItem__action { opacity: 0; /* 1 */ - transition: none; + + &.dscSidebarItem__mobile { + opacity: 1; + } &:focus { opacity: 1; /* 2 */ } - font-size: $euiFontSizeXS; - padding: 2px 6px !important; - height: 22px !important; - min-width: auto !important; - .euiButton__content { - padding: 0 4px; - } -} - -.dscFieldSearch { - padding: $euiSizeS; -} - -.dscFieldSearch__toggleButton { - width: calc(100% - #{$euiSizeS}); - color: $euiColorPrimary; - padding-left: $euiSizeXS; - margin-left: $euiSizeXS; -} - -.dscFieldSearch__filterWrapper { - flex-grow: 0; -} - -.dscFieldSearch__formWrapper { - padding: $euiSizeM; -} - -.dscFieldDetails { - color: $euiTextColor; - margin-bottom: $euiSizeS; } diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx index 23d2fa0a39f34..74921a70e7f2f 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx @@ -17,7 +17,7 @@ * under the License. */ -import _ from 'lodash'; +import { each, cloneDeep } from 'lodash'; import { ReactWrapper } from 'enzyme'; import { findTestSubject } from '@elastic/eui/lib/test'; // @ts-ignore @@ -26,35 +26,41 @@ import realHits from 'fixtures/real_hits.js'; import stubbedLogstashFields from 'fixtures/logstash_fields'; import { mountWithIntl } from '@kbn/test/jest'; import React from 'react'; -import { DiscoverSidebar, DiscoverSidebarProps } from './discover_sidebar'; +import { DiscoverSidebarProps } from './discover_sidebar'; import { coreMock } from '../../../../../../core/public/mocks'; import { IndexPatternAttributes } from '../../../../../data/common'; import { getStubIndexPattern } from '../../../../../data/public/test_utils'; import { SavedObject } from '../../../../../../core/types'; +import { getDefaultFieldFilter } from './lib/field_filter'; +import { DiscoverSidebar } from './discover_sidebar'; +import { DiscoverServices } from '../../../build_services'; +import { ElasticSearchHit } from '../../doc_views/doc_views_types'; -jest.mock('../../../kibana_services', () => ({ - getServices: () => ({ - history: () => ({ - location: { - search: '', - }, - }), - capabilities: { - visualize: { - show: true, - }, - discover: { - save: false, - }, - }, - uiSettings: { - get: (key: string) => { - if (key === 'fields:popularLimit') { - return 5; - } - }, +const mockServices = ({ + history: () => ({ + location: { + search: '', }, }), + capabilities: { + visualize: { + show: true, + }, + discover: { + save: false, + }, + }, + uiSettings: { + get: (key: string) => { + if (key === 'fields:popularLimit') { + return 5; + } + }, + }, +} as unknown) as DiscoverServices; + +jest.mock('../../../kibana_services', () => ({ + getServices: () => mockServices, })); jest.mock('./lib/get_index_pattern_field_list', () => ({ @@ -71,9 +77,9 @@ function getCompProps() { ); // @ts-expect-error _.each() is passing additional args to flattenHit - const hits = _.each(_.cloneDeep(realHits), indexPattern.flattenHit) as Array< + const hits = (each(cloneDeep(realHits), indexPattern.flattenHit) as Array< Record - >; + >) as ElasticSearchHit[]; const indexPatternList = [ { id: '0', attributes: { title: 'b' } } as SavedObject, @@ -97,9 +103,12 @@ function getCompProps() { onAddField: jest.fn(), onRemoveField: jest.fn(), selectedIndexPattern: indexPattern, + services: mockServices, setIndexPattern: jest.fn(), state: {}, trackUiMetric: jest.fn(), + fieldFilter: getDefaultFieldFilter(), + setFieldFilter: jest.fn(), }; } @@ -128,9 +137,4 @@ describe('discover sidebar', function () { findTestSubject(comp, 'fieldToggle-extension').simulate('click'); expect(props.onRemoveField).toHaveBeenCalledWith('extension'); }); - it('should allow adding filters', function () { - findTestSubject(comp, 'field-extension-showDetails').simulate('click'); - findTestSubject(comp, 'plus-extension-gif').simulate('click'); - expect(props.onAddFilter).toHaveBeenCalled(); - }); }); diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx index b8e09ce4d17e8..3283551488d68 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx @@ -19,10 +19,19 @@ import './discover_sidebar.scss'; import React, { useCallback, useEffect, useState, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiButtonIcon, EuiTitle, EuiSpacer } from '@elastic/eui'; -import { sortBy } from 'lodash'; -import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; import { UiStatsMetricType } from '@kbn/analytics'; +import { + EuiAccordion, + EuiFlexItem, + EuiFlexGroup, + EuiText, + EuiTitle, + EuiSpacer, + EuiNotificationBadge, + EuiPageSideBar, +} from '@elastic/eui'; +import { isEqual, sortBy } from 'lodash'; +import { FormattedMessage } from '@kbn/i18n/react'; import { DiscoverField } from './discover_field'; import { DiscoverIndexPattern } from './discover_index_pattern'; import { DiscoverFieldSearch } from './discover_field_search'; @@ -32,11 +41,16 @@ import { FIELDS_LIMIT_SETTING } from '../../../../common'; import { groupFields } from './lib/group_fields'; import { IndexPatternField, IndexPattern } from '../../../../../data/public'; import { getDetails } from './lib/get_details'; -import { getDefaultFieldFilter, setFieldFilterProp } from './lib/field_filter'; +import { FieldFilterState, getDefaultFieldFilter, setFieldFilterProp } from './lib/field_filter'; import { getIndexPatternFieldList } from './lib/get_index_pattern_field_list'; -import { getServices } from '../../../kibana_services'; +import { DiscoverServices } from '../../../build_services'; +import { ElasticSearchHit } from '../../doc_views/doc_views_types'; export interface DiscoverSidebarProps { + /** + * Determines whether add/remove buttons are displayed not only when focused + */ + alwaysShowActionButtons?: boolean; /** * the selected columns displayed in the doc table in discover */ @@ -45,10 +59,14 @@ export interface DiscoverSidebarProps { * a statistics of the distribution of fields in the given hits */ fieldCounts: Record; + /** + * Current state of the field filter, filtering fields by name, type, ... + */ + fieldFilter: FieldFilterState; /** * hits fetched from ES, displayed in the doc table */ - hits: Array>; + hits: ElasticSearchHit[]; /** * List of available index patterns */ @@ -70,6 +88,14 @@ export interface DiscoverSidebarProps { * Currently selected index pattern */ selectedIndexPattern?: IndexPattern; + /** + * Discover plugin services; + */ + services: DiscoverServices; + /** + * Change current state of fieldFilter + */ + setFieldFilter: (next: FieldFilterState) => void; /** * Callback function to select another index pattern */ @@ -80,35 +106,41 @@ export interface DiscoverSidebarProps { * @param eventName */ trackUiMetric?: (metricType: UiStatsMetricType, eventName: string | string[]) => void; + /** + * Shows index pattern and a button that displays the sidebar in a flyout + */ + useFlyout?: boolean; } export function DiscoverSidebar({ + alwaysShowActionButtons = false, columns, fieldCounts, + fieldFilter, hits, indexPatternList, onAddField, onAddFilter, onRemoveField, selectedIndexPattern, + services, + setFieldFilter, setIndexPattern, trackUiMetric, + useFlyout = false, }: DiscoverSidebarProps) { - const [showFields, setShowFields] = useState(false); const [fields, setFields] = useState(null); - const [fieldFilterState, setFieldFilterState] = useState(getDefaultFieldFilter()); - const services = useMemo(() => getServices(), []); useEffect(() => { const newFields = getIndexPatternFieldList(selectedIndexPattern, fieldCounts); setFields(newFields); - }, [selectedIndexPattern, fieldCounts, hits, services]); + }, [selectedIndexPattern, fieldCounts, hits]); const onChangeFieldSearch = useCallback( (field: string, value: string | boolean | undefined) => { - const newState = setFieldFilterProp(fieldFilterState, field, value); - setFieldFilterState(newState); + const newState = setFieldFilterProp(fieldFilter, field, value); + setFieldFilter(newState); }, - [fieldFilterState] + [fieldFilter, setFieldFilter] ); const getDetailsByField = useCallback( @@ -122,12 +154,12 @@ export function DiscoverSidebar({ selected: selectedFields, popular: popularFields, unpopular: unpopularFields, - } = useMemo(() => groupFields(fields, columns, popularLimit, fieldCounts, fieldFilterState), [ + } = useMemo(() => groupFields(fields, columns, popularLimit, fieldCounts, fieldFilter), [ fields, columns, popularLimit, fieldCounts, - fieldFilterState, + fieldFilter, ]); const fieldTypes = useMemo(() => { @@ -146,10 +178,11 @@ export function DiscoverSidebar({ return null; } - return ( - + const filterChanged = isEqual(fieldFilter, getDefaultFieldFilter()); + + if (useFlyout) { + return (
o.attributes.title)} /> -
+
+ ); + } + + return ( + + + + o.attributes.title)} + /> + +
-

-
- {fields.length > 0 && ( - <> - -

- -

-
- -
    - {selectedFields.map((field: IndexPatternField) => { - return ( -
  • - -
  • - ); - })} -
-
- -

- -

-
-
- setShowFields(!showFields)} - aria-label={ - showFields - ? i18n.translate( - 'discover.fieldChooser.filter.indexAndFieldsSectionHideAriaLabel', - { - defaultMessage: 'Hide fields', - } - ) - : i18n.translate( - 'discover.fieldChooser.filter.indexAndFieldsSectionShowAriaLabel', - { - defaultMessage: 'Show fields', - } - ) - } - /> -
-
- - )} - {popularFields.length > 0 && ( -
- - - -
    - {popularFields.map((field: IndexPatternField) => { - return ( -
  • + +
    + {fields.length > 0 && ( + <> + {selectedFields && + selectedFields.length > 0 && + selectedFields[0].displayName !== '_source' ? ( + <> + + + + + + } + extraAction={ + + {selectedFields.length} + + } > - -
  • - ); - })} -
-
- )} - -
    - {unpopularFields.map((field: IndexPatternField) => { - return ( -
  • +
      + {selectedFields.map((field: IndexPatternField) => { + return ( +
    • + +
    • + ); + })} +
    + + {' '} + + ) : null} + + + + + + } + extraAction={ + + {popularFields.length + unpopularFields.length} + + } > - -
  • - ); - })} -
-
- -
+ + {popularFields.length > 0 && ( + <> + + + +
    + {popularFields.map((field: IndexPatternField) => { + return ( +
  • + +
  • + ); + })} +
+ + )} +
    + {unpopularFields.map((field: IndexPatternField) => { + return ( +
  • + +
  • + ); + })} +
+ + + )} +
+
+ + ); } diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.test.tsx new file mode 100644 index 0000000000000..906de04df3a1d --- /dev/null +++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.test.tsx @@ -0,0 +1,145 @@ +/* + * 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 { each, cloneDeep } from 'lodash'; +import { ReactWrapper } from 'enzyme'; +import { findTestSubject } from '@elastic/eui/lib/test'; +// @ts-ignore +import realHits from 'fixtures/real_hits.js'; +// @ts-ignore +import stubbedLogstashFields from 'fixtures/logstash_fields'; +import { mountWithIntl } from '@kbn/test/jest'; +import React from 'react'; +import { DiscoverSidebarProps } from './discover_sidebar'; +import { coreMock } from '../../../../../../core/public/mocks'; +import { IndexPatternAttributes } from '../../../../../data/common'; +import { getStubIndexPattern } from '../../../../../data/public/test_utils'; +import { SavedObject } from '../../../../../../core/types'; +import { FieldFilterState } from './lib/field_filter'; +import { DiscoverSidebarResponsive } from './discover_sidebar_responsive'; +import { DiscoverServices } from '../../../build_services'; +import { ElasticSearchHit } from '../../doc_views/doc_views_types'; + +const mockServices = ({ + history: () => ({ + location: { + search: '', + }, + }), + capabilities: { + visualize: { + show: true, + }, + discover: { + save: false, + }, + }, + uiSettings: { + get: (key: string) => { + if (key === 'fields:popularLimit') { + return 5; + } + }, + }, +} as unknown) as DiscoverServices; + +jest.mock('../../../kibana_services', () => ({ + getServices: () => mockServices, +})); + +jest.mock('./lib/get_index_pattern_field_list', () => ({ + getIndexPatternFieldList: jest.fn((indexPattern) => indexPattern.fields), +})); + +function getCompProps() { + const indexPattern = getStubIndexPattern( + 'logstash-*', + (cfg: any) => cfg, + 'time', + stubbedLogstashFields(), + coreMock.createSetup() + ); + + // @ts-expect-error _.each() is passing additional args to flattenHit + const hits = (each(cloneDeep(realHits), indexPattern.flattenHit) as Array< + Record + >) as ElasticSearchHit[]; + + const indexPatternList = [ + { id: '0', attributes: { title: 'b' } } as SavedObject, + { id: '1', attributes: { title: 'a' } } as SavedObject, + { id: '2', attributes: { title: 'c' } } as SavedObject, + ]; + + const fieldCounts: Record = {}; + + for (const hit of hits) { + for (const key of Object.keys(indexPattern.flattenHit(hit))) { + fieldCounts[key] = (fieldCounts[key] || 0) + 1; + } + } + return { + columns: ['extension'], + fieldCounts, + hits, + indexPatternList, + onAddFilter: jest.fn(), + onAddField: jest.fn(), + onRemoveField: jest.fn(), + selectedIndexPattern: indexPattern, + services: mockServices, + setIndexPattern: jest.fn(), + state: {}, + trackUiMetric: jest.fn(), + fieldFilter: {} as FieldFilterState, + setFieldFilter: jest.fn(), + }; +} + +describe('discover responsive sidebar', function () { + let props: DiscoverSidebarProps; + let comp: ReactWrapper; + + beforeAll(() => { + props = getCompProps(); + comp = mountWithIntl(); + }); + + it('should have Selected Fields and Available Fields with Popular Fields sections', function () { + const popular = findTestSubject(comp, 'fieldList-popular'); + const selected = findTestSubject(comp, 'fieldList-selected'); + const unpopular = findTestSubject(comp, 'fieldList-unpopular'); + expect(popular.children().length).toBe(1); + expect(unpopular.children().length).toBe(7); + expect(selected.children().length).toBe(1); + }); + it('should allow selecting fields', function () { + findTestSubject(comp, 'fieldToggle-bytes').simulate('click'); + expect(props.onAddField).toHaveBeenCalledWith('bytes'); + }); + it('should allow deselecting fields', function () { + findTestSubject(comp, 'fieldToggle-extension').simulate('click'); + expect(props.onRemoveField).toHaveBeenCalledWith('extension'); + }); + it('should allow adding filters', function () { + findTestSubject(comp, 'field-extension-showDetails').simulate('click'); + findTestSubject(comp, 'plus-extension-gif').simulate('click'); + expect(props.onAddFilter).toHaveBeenCalled(); + }); +}); diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.tsx b/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.tsx new file mode 100644 index 0000000000000..369ebbde5743b --- /dev/null +++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.tsx @@ -0,0 +1,205 @@ +/* + * 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 } from 'react'; +import { sortBy } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { UiStatsMetricType } from '@kbn/analytics'; +import { + EuiTitle, + EuiHideFor, + EuiShowFor, + EuiButton, + EuiBadge, + EuiFlyoutHeader, + EuiFlyout, + EuiSpacer, + EuiIcon, + EuiLink, + EuiPortal, +} from '@elastic/eui'; +import { DiscoverIndexPattern } from './discover_index_pattern'; +import { IndexPatternAttributes } from '../../../../../data/common'; +import { SavedObject } from '../../../../../../core/types'; +import { IndexPatternField, IndexPattern } from '../../../../../data/public'; +import { getDefaultFieldFilter } from './lib/field_filter'; +import { DiscoverSidebar } from './discover_sidebar'; +import { DiscoverServices } from '../../../build_services'; +import { ElasticSearchHit } from '../../doc_views/doc_views_types'; + +export interface DiscoverSidebarResponsiveProps { + /** + * Determines whether add/remove buttons are displayed non only when focused + */ + alwaysShowActionButtons?: boolean; + /** + * the selected columns displayed in the doc table in discover + */ + columns: string[]; + /** + * a statistics of the distribution of fields in the given hits + */ + fieldCounts: Record; + /** + * hits fetched from ES, displayed in the doc table + */ + hits: ElasticSearchHit[]; + /** + * List of available index patterns + */ + indexPatternList: Array>; + /** + * Has been toggled closed + */ + isClosed?: boolean; + /** + * Callback function when selecting a field + */ + onAddField: (fieldName: string) => void; + /** + * Callback function when adding a filter from sidebar + */ + onAddFilter: (field: IndexPatternField | string, value: string, type: '+' | '-') => void; + /** + * Callback function when removing a field + * @param fieldName + */ + onRemoveField: (fieldName: string) => void; + /** + * Currently selected index pattern + */ + selectedIndexPattern?: IndexPattern; + /** + * Discover plugin services; + */ + services: DiscoverServices; + /** + * Callback function to select another index pattern + */ + setIndexPattern: (id: string) => void; + /** + * Metric tracking function + * @param metricType + * @param eventName + */ + trackUiMetric?: (metricType: UiStatsMetricType, eventName: string | string[]) => void; + /** + * Shows index pattern and a button that displays the sidebar in a flyout + */ + useFlyout?: boolean; +} + +/** + * Component providing 2 different renderings for the sidebar depending on available screen space + * Desktop: Sidebar view, all elements are visible + * Mobile: Index pattern selector is visible and a button to trigger a flyout with all elements + */ +export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps) { + const [fieldFilter, setFieldFilter] = useState(getDefaultFieldFilter()); + const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); + + if (!props.selectedIndexPattern) { + return null; + } + + return ( + <> + {props.isClosed ? null : ( + + + + )} + +
+
+ o.attributes.title)} + /> +
+ + setIsFlyoutVisible(true)} + > + + + {props.columns[0] === '_source' ? 0 : props.columns.length} + + +
+ {isFlyoutVisible && ( + + setIsFlyoutVisible(false)} + aria-labelledby="flyoutTitle" + ownFocus + > + + +

+ setIsFlyoutVisible(false)}> + {' '} + + {i18n.translate('discover.fieldList.flyoutHeading', { + defaultMessage: 'Field list', + })} + + +

+
+
+ {/* Using only the direct flyout body class because we maintain scroll in a lower sidebar component. Needs a fix on the EUI side */} +
+ +
+
+
+ )} +
+ + ); +} diff --git a/src/plugins/discover/public/application/components/sidebar/index.ts b/src/plugins/discover/public/application/components/sidebar/index.ts index aec8dfc86e817..7575b5691a95a 100644 --- a/src/plugins/discover/public/application/components/sidebar/index.ts +++ b/src/plugins/discover/public/application/components/sidebar/index.ts @@ -18,3 +18,4 @@ */ export { DiscoverSidebar } from './discover_sidebar'; +export { DiscoverSidebarResponsive } from './discover_sidebar_responsive'; diff --git a/src/plugins/discover/public/application/components/sidebar/lib/get_details.ts b/src/plugins/discover/public/application/components/sidebar/lib/get_details.ts index 22a6e7a628555..e979131a7a85f 100644 --- a/src/plugins/discover/public/application/components/sidebar/lib/get_details.ts +++ b/src/plugins/discover/public/application/components/sidebar/lib/get_details.ts @@ -20,10 +20,11 @@ // @ts-ignore import { fieldCalculator } from './field_calculator'; import { IndexPattern, IndexPatternField } from '../../../../../../data/public'; +import { ElasticSearchHit } from '../../../doc_views/doc_views_types'; export function getDetails( field: IndexPatternField, - hits: Array>, + hits: ElasticSearchHit[], columns: string[], indexPattern?: IndexPattern ) { diff --git a/src/plugins/discover/public/application/components/skip_bottom_button/skip_bottom_button.tsx b/src/plugins/discover/public/application/components/skip_bottom_button/skip_bottom_button.tsx index d5bc5bb64f59b..e2b8e0ffcf518 100644 --- a/src/plugins/discover/public/application/components/skip_bottom_button/skip_bottom_button.tsx +++ b/src/plugins/discover/public/application/components/skip_bottom_button/skip_bottom_button.tsx @@ -18,7 +18,7 @@ */ import React from 'react'; import { EuiSkipLink } from '@elastic/eui'; -import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; +import { FormattedMessage } from '@kbn/i18n/react'; export interface SkipBottomButtonProps { /** @@ -29,26 +29,22 @@ export interface SkipBottomButtonProps { export function SkipBottomButton({ onClick }: SkipBottomButtonProps) { return ( - - { - // prevent the anchor to reload the page on click - event.preventDefault(); - // The destinationId prop cannot be leveraged here as the table needs - // to be updated first (angular logic) - onClick(); - }} - className="dscSkipButton" - destinationId="" - data-test-subj="discoverSkipTableButton" - > - - - + ) => { + // prevent the anchor to reload the page on click + event.preventDefault(); + // The destinationId prop cannot be leveraged here as the table needs + // to be updated first (angular logic) + onClick(); + }} + className="dscSkipButton" + id="dscSkipButton" + destinationId="" + data-test-subj="discoverSkipTableButton" + position="absolute" + > + + ); } diff --git a/src/plugins/discover/public/application/components/table/table.tsx b/src/plugins/discover/public/application/components/table/table.tsx index 5d37f598b38f6..d57447eab9e26 100644 --- a/src/plugins/discover/public/application/components/table/table.tsx +++ b/src/plugins/discover/public/application/components/table/table.tsx @@ -32,13 +32,16 @@ export function DocViewTable({ onAddColumn, onRemoveColumn, }: DocViewRenderProps) { + const [fieldRowOpen, setFieldRowOpen] = useState({} as Record); + if (!indexPattern) { + return null; + } const mapping = indexPattern.fields.getByName; const flattened = indexPattern.flattenHit(hit); const formatted = indexPattern.formatHit(hit, 'html'); - const [fieldRowOpen, setFieldRowOpen] = useState({} as Record); function toggleValueCollapse(field: string) { - fieldRowOpen[field] = fieldRowOpen[field] !== true; + fieldRowOpen[field] = !fieldRowOpen[field]; setFieldRowOpen({ ...fieldRowOpen }); } diff --git a/src/plugins/discover/public/application/components/table/table_row.tsx b/src/plugins/discover/public/application/components/table/table_row.tsx index 3d75e175951d5..3ebf3c435916b 100644 --- a/src/plugins/discover/public/application/components/table/table_row.tsx +++ b/src/plugins/discover/public/application/components/table/table_row.tsx @@ -67,32 +67,11 @@ export function DocViewTableRow({ return ( - {typeof onFilter === 'function' && ( - - onFilter(fieldMapping, valueRaw, '+')} - /> - onFilter(fieldMapping, valueRaw, '-')} - /> - {typeof onToggleColumn === 'function' && ( - - )} - onFilter('_exists_', field, '+')} - scripted={fieldMapping && fieldMapping.scripted} - /> - - )} @@ -113,6 +92,26 @@ export function DocViewTableRow({ dangerouslySetInnerHTML={{ __html: value as string }} /> + {typeof onFilter === 'function' && ( + + onFilter(fieldMapping, valueRaw, '+')} + /> + onFilter(fieldMapping, valueRaw, '-')} + /> + {typeof onToggleColumn === 'function' && ( + + )} + onFilter('_exists_', field, '+')} + scripted={fieldMapping && fieldMapping.scripted} + /> + + )} ); } diff --git a/src/plugins/discover/public/application/components/table/table_row_btn_filter_add.tsx b/src/plugins/discover/public/application/components/table/table_row_btn_filter_add.tsx index bd842eb5c6f72..142761768b472 100644 --- a/src/plugins/discover/public/application/components/table/table_row_btn_filter_add.tsx +++ b/src/plugins/discover/public/application/components/table/table_row_btn_filter_add.tsx @@ -49,7 +49,7 @@ export function DocViewTableRowBtnFilterAdd({ onClick, disabled = false }: Props data-test-subj="addInclusiveFilterButton" disabled={disabled} onClick={onClick} - iconType={'magnifyWithPlus'} + iconType={'plusInCircle'} iconSize={'s'} /> diff --git a/src/plugins/discover/public/application/components/table/table_row_btn_filter_exists.tsx b/src/plugins/discover/public/application/components/table/table_row_btn_filter_exists.tsx index dab22c103bc48..43a711fc72da5 100644 --- a/src/plugins/discover/public/application/components/table/table_row_btn_filter_exists.tsx +++ b/src/plugins/discover/public/application/components/table/table_row_btn_filter_exists.tsx @@ -61,7 +61,7 @@ export function DocViewTableRowBtnFilterExists({ className="kbnDocViewer__actionButton" data-test-subj="addExistsFilterButton" disabled={disabled} - iconType={'indexOpen'} + iconType={'filter'} iconSize={'s'} /> diff --git a/src/plugins/discover/public/application/components/table/table_row_btn_filter_remove.tsx b/src/plugins/discover/public/application/components/table/table_row_btn_filter_remove.tsx index bbef54cb4ecc7..878088ae0a6d8 100644 --- a/src/plugins/discover/public/application/components/table/table_row_btn_filter_remove.tsx +++ b/src/plugins/discover/public/application/components/table/table_row_btn_filter_remove.tsx @@ -49,7 +49,7 @@ export function DocViewTableRowBtnFilterRemove({ onClick, disabled = false }: Pr data-test-subj="removeInclusiveFilterButton" disabled={disabled} onClick={onClick} - iconType={'magnifyWithMinus'} + iconType={'minusInCircle'} iconSize={'s'} /> diff --git a/src/plugins/discover/public/application/components/table/table_row_btn_toggle_column.tsx b/src/plugins/discover/public/application/components/table/table_row_btn_toggle_column.tsx index 3e5a057929701..1a32ba3be1712 100644 --- a/src/plugins/discover/public/application/components/table/table_row_btn_toggle_column.tsx +++ b/src/plugins/discover/public/application/components/table/table_row_btn_toggle_column.tsx @@ -37,7 +37,7 @@ export function DocViewTableRowBtnToggleColumn({ onClick, active, disabled = fal className="kbnDocViewer__actionButton" data-test-subj="toggleColumnButton" disabled - iconType={'tableOfContents'} + iconType={'listAdd'} iconSize={'s'} /> ); @@ -59,7 +59,7 @@ export function DocViewTableRowBtnToggleColumn({ onClick, active, disabled = fal onClick={onClick} className="kbnDocViewer__actionButton" data-test-subj="toggleColumnButton" - iconType={'tableOfContents'} + iconType={'listAdd'} iconSize={'s'} /> diff --git a/src/plugins/discover/public/application/components/timechart_header/timechart_header.scss b/src/plugins/discover/public/application/components/timechart_header/timechart_header.scss new file mode 100644 index 0000000000000..506dc26d9bee3 --- /dev/null +++ b/src/plugins/discover/public/application/components/timechart_header/timechart_header.scss @@ -0,0 +1,7 @@ +.dscTimeIntervalSelect { + align-items: center; +} + +.dscTimeChartHeader { + flex-grow: 0; +} diff --git a/src/plugins/discover/public/application/components/timechart_header/timechart_header.tsx b/src/plugins/discover/public/application/components/timechart_header/timechart_header.tsx index 1451106827ee0..544de61b5825b 100644 --- a/src/plugins/discover/public/application/components/timechart_header/timechart_header.tsx +++ b/src/plugins/discover/public/application/components/timechart_header/timechart_header.tsx @@ -25,8 +25,8 @@ import { EuiSelect, EuiIconTip, } from '@elastic/eui'; -import { I18nProvider } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; +import './timechart_header.scss'; import moment from 'moment'; export interface TimechartHeaderProps { @@ -99,73 +99,78 @@ export function TimechartHeader({ } return ( - - - - + + + + {`${toMoment(timeRange.from)} - ${toMoment(timeRange.to)} ${ + interval !== 'auto' + ? i18n.translate('discover.timechartHeader.timeIntervalSelect.per', { + defaultMessage: 'per', + }) + : '' + }`} + + + + + val !== 'custom') + .map(({ display, val }) => { + return { + text: display, + value: val, + label: display, + }; })} - delay="long" - > - - {`${toMoment(timeRange.from)} - ${toMoment(timeRange.to)} ${ - interval !== 'auto' - ? i18n.translate('discover.timechartHeader.timeIntervalSelect.per', { - defaultMessage: 'per', - }) - : '' - }`} - - - - - val !== 'custom') - .map(({ display, val }) => { - return { - text: display, - value: val, - label: display, - }; - })} - value={interval} - onChange={handleIntervalChange} - append={ - bucketInterval.scaled ? ( - 1 - ? i18n.translate('discover.bucketIntervalTooltip.tooLargeBucketsText', { - defaultMessage: 'buckets that are too large', - }) - : i18n.translate('discover.bucketIntervalTooltip.tooManyBucketsText', { - defaultMessage: 'too many buckets', - }), - bucketIntervalDescription: bucketInterval.description, - }, - })} - color="warning" - size="s" - type="alert" - /> - ) : undefined - } - /> - - - + value={interval} + onChange={handleIntervalChange} + append={ + bucketInterval.scaled ? ( + 1 + ? i18n.translate('discover.bucketIntervalTooltip.tooLargeBucketsText', { + defaultMessage: 'buckets that are too large', + }) + : i18n.translate('discover.bucketIntervalTooltip.tooManyBucketsText', { + defaultMessage: 'too many buckets', + }), + bucketIntervalDescription: bucketInterval.description, + }, + })} + color="warning" + size="s" + type="alert" + /> + ) : undefined + } + /> + + ); } diff --git a/src/plugins/discover/public/application/doc_views/doc_views_types.ts b/src/plugins/discover/public/application/doc_views/doc_views_types.ts index 01145402e0f29..dcfc25fd4099d 100644 --- a/src/plugins/discover/public/application/doc_views/doc_views_types.ts +++ b/src/plugins/discover/public/application/doc_views/doc_views_types.ts @@ -49,7 +49,7 @@ export interface DocViewRenderProps { columns?: string[]; filter?: DocViewFilterFn; hit: ElasticSearchHit; - indexPattern: IndexPattern; + indexPattern?: IndexPattern; onAddColumn?: (columnName: string) => void; onRemoveColumn?: (columnName: string) => void; } diff --git a/src/plugins/discover/public/application/index.scss b/src/plugins/discover/public/application/index.scss index 5aa353828274c..3c24d4f51de2e 100644 --- a/src/plugins/discover/public/application/index.scss +++ b/src/plugins/discover/public/application/index.scss @@ -1,2 +1 @@ @import 'angular/index'; -@import 'discover'; diff --git a/test/functional/apps/discover/_discover.js b/test/functional/apps/discover/_discover.js index 2270f3c815aaa..78197cd8d66ff 100644 --- a/test/functional/apps/discover/_discover.js +++ b/test/functional/apps/discover/_discover.js @@ -114,7 +114,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.discover.waitUntilSearchingHasFinished(); const newDurationHours = await PageObjects.timePicker.getTimeDurationInHours(); - expect(Math.round(newDurationHours)).to.be(24); + expect(Math.round(newDurationHours)).to.be(26); await retry.waitFor('doc table to contain the right search result', async () => { const rowData = await PageObjects.discover.getDocTableField(1); diff --git a/test/functional/apps/discover/_sidebar.js b/test/functional/apps/discover/_sidebar.ts similarity index 65% rename from test/functional/apps/discover/_sidebar.js rename to test/functional/apps/discover/_sidebar.ts index ce7ebff9cce74..c91c9020b373b 100644 --- a/test/functional/apps/discover/_sidebar.js +++ b/test/functional/apps/discover/_sidebar.ts @@ -17,31 +17,23 @@ * under the License. */ -import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; -export default function ({ getService, getPageObjects }) { - const log = getService('log'); +export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); const PageObjects = getPageObjects(['common', 'discover', 'timePicker']); + const testSubjects = getService('testSubjects'); describe('discover sidebar', function describeIndexTests() { before(async function () { - // delete .kibana index and update configDoc + await esArchiver.loadIfNeeded('logstash_functional'); + await esArchiver.loadIfNeeded('discover'); await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash-*', }); - - log.debug('load kibana index with default index pattern'); - await esArchiver.load('discover'); - - // and load a set of makelogs data - await esArchiver.loadIfNeeded('logstash_functional'); - - log.debug('discover'); + await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings(); await PageObjects.common.navigateToApp('discover'); - - await PageObjects.timePicker.setDefaultAbsoluteRange(); }); describe('field filtering', function () { @@ -53,26 +45,17 @@ export default function ({ getService, getPageObjects }) { describe('collapse expand', function () { it('should initially be expanded', async function () { - const width = await PageObjects.discover.getSidebarWidth(); - log.debug('expanded sidebar width = ' + width); - expect(width > 20).to.be(true); + await testSubjects.existOrFail('discover-sidebar'); }); it('should collapse when clicked', async function () { await PageObjects.discover.toggleSidebarCollapse(); - log.debug('PageObjects.discover.getSidebarWidth()'); - const width = await PageObjects.discover.getSidebarWidth(); - log.debug('collapsed sidebar width = ' + width); - expect(width < 20).to.be(true); + await testSubjects.missingOrFail('discover-sidebar'); }); it('should expand when clicked', async function () { await PageObjects.discover.toggleSidebarCollapse(); - - log.debug('PageObjects.discover.getSidebarWidth()'); - const width = await PageObjects.discover.getSidebarWidth(); - log.debug('expanded sidebar width = ' + width); - expect(width > 20).to.be(true); + await testSubjects.existOrFail('discover-sidebar'); }); }); }); diff --git a/test/functional/page_objects/discover_page.ts b/test/functional/page_objects/discover_page.ts index 9c5bedf7c242d..494141355806f 100644 --- a/test/functional/page_objects/discover_page.ts +++ b/test/functional/page_objects/discover_page.ts @@ -251,11 +251,6 @@ export function DiscoverPageProvider({ getService, getPageObjects }: FtrProvider .map((field) => $(field).text()); } - public async getSidebarWidth() { - const sidebar = await testSubjects.find('discover-sidebar'); - return await sidebar.getAttribute('clientWidth'); - } - public async hasNoResults() { return await testSubjects.exists('discoverNoResults'); } @@ -284,6 +279,9 @@ export function DiscoverPageProvider({ getService, getPageObjects }: FtrProvider } public async clickFieldListItemRemove(field: string) { + if (!(await testSubjects.exists('fieldList-selected'))) { + return; + } const selectedList = await testSubjects.find('fieldList-selected'); if (await testSubjects.descendantExists(`field-${field}`, selectedList)) { await this.clickFieldListItemToggle(field); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 7b84c62264c83..48dc3918630bd 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -1486,15 +1486,12 @@ "discover.fieldChooser.fieldCalculator.analysisIsNotAvailableForGeoFieldsErrorMessage": "ジオフィールドは分析できません。", "discover.fieldChooser.fieldCalculator.analysisIsNotAvailableForObjectFieldsErrorMessage": "オブジェクトフィールドは分析できません。", "discover.fieldChooser.fieldCalculator.fieldIsNotPresentInDocumentsErrorMessage": "このフィールドはElasticsearchマッピングに表示されますが、ドキュメントテーブルの{hitsLength}件のドキュメントには含まれません。可視化や検索は可能な場合があります。", - "discover.fieldChooser.fieldFilterFacetButtonLabel": "タイプでフィルタリング", "discover.fieldChooser.filter.aggregatableLabel": "集約可能", "discover.fieldChooser.filter.availableFieldsTitle": "利用可能なフィールド", "discover.fieldChooser.filter.fieldSelectorLabel": "{id}フィルターオプションの選択", "discover.fieldChooser.filter.filterByTypeLabel": "タイプでフィルタリング", "discover.fieldChooser.filter.hideMissingFieldsLabel": "未入力のフィールドを非表示", "discover.fieldChooser.filter.indexAndFieldsSectionAriaLabel": "インデックスとフィールド", - "discover.fieldChooser.filter.indexAndFieldsSectionHideAriaLabel": "フィールドを非表示", - "discover.fieldChooser.filter.indexAndFieldsSectionShowAriaLabel": "フィールドを表示", "discover.fieldChooser.filter.popularTitle": "人気", "discover.fieldChooser.filter.searchableLabel": "検索可能", "discover.fieldChooser.filter.selectedFieldsTitle": "スクリプトフィールド", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 55071303a1b36..0505e4762c332 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -1487,15 +1487,12 @@ "discover.fieldChooser.fieldCalculator.analysisIsNotAvailableForGeoFieldsErrorMessage": "分析不适用于地理字段。", "discover.fieldChooser.fieldCalculator.analysisIsNotAvailableForObjectFieldsErrorMessage": "分析不适用于对象字段。", "discover.fieldChooser.fieldCalculator.fieldIsNotPresentInDocumentsErrorMessage": "此字段在您的 Elasticsearch 映射中,但不在文档表中显示的 {hitsLength} 个文档中。您可能仍能够基于它可视化或搜索。", - "discover.fieldChooser.fieldFilterFacetButtonLabel": "按类型筛选", "discover.fieldChooser.filter.aggregatableLabel": "可聚合", "discover.fieldChooser.filter.availableFieldsTitle": "可用字段", "discover.fieldChooser.filter.fieldSelectorLabel": "{id} 筛选选项的选择", "discover.fieldChooser.filter.filterByTypeLabel": "按类型筛选", "discover.fieldChooser.filter.hideMissingFieldsLabel": "隐藏缺失字段", "discover.fieldChooser.filter.indexAndFieldsSectionAriaLabel": "索引和字段", - "discover.fieldChooser.filter.indexAndFieldsSectionHideAriaLabel": "隐藏字段", - "discover.fieldChooser.filter.indexAndFieldsSectionShowAriaLabel": "显示字段", "discover.fieldChooser.filter.popularTitle": "常见", "discover.fieldChooser.filter.searchableLabel": "可搜索", "discover.fieldChooser.filter.selectedFieldsTitle": "选定字段",