[discover] De-angularize index pattern selection (#46347)

* Implementation of DiscoverIndexPattern React component

* Implement usage of the DiscoverIndexPattern

* Adapt tests
This commit is contained in:
Matthias Wilhelm 2019-09-30 11:19:28 +02:00 committed by GitHub
parent bd1ac0dad4
commit f9cc456de0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 354 additions and 77 deletions

View file

@ -127,13 +127,6 @@ describe('discover field chooser directives', function () {
};
};
describe('Index list', function () {
it('should be in alphabetical order', function () {
$elem.find('.ui-select-toggle').click();
expect($elem.find('[role=option]').text().replace(/\W+/g, '')).to.be('abc');
});
});
describe('Field listing', function () {
it('should have Selected Fields, Fields and Popular Fields sections', function () {
const headers = $elem.find('.sidebar-list-header');

View file

@ -0,0 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`DiscoverIndexPattern A single index pattern is just displayed 1`] = `
<DiscoverIndexPatternTitle
isChangeable={false}
onChange={[Function]}
title="test1"
/>
`;
exports[`DiscoverIndexPattern Invalid props dont cause an exception: "" 1`] = `""`;

View file

@ -0,0 +1,75 @@
/*
* 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 from 'react';
import { shallowWithIntl, mountWithIntl } from 'test_utils/enzyme_helpers';
// @ts-ignore
import { findTestSubject } from '@elastic/eui/lib/test';
import { SavedObject } from 'kibana/server';
import { DiscoverIndexPattern, DiscoverIndexPatternProps } from './discover_index_pattern';
import { comboBoxKeyCodes } from '@elastic/eui';
const indexPattern1 = {
id: 'test1',
attributes: {
title: 'test1',
},
} as SavedObject;
const indexPattern2 = {
id: 'test2',
attributes: {
title: 'test2',
},
} as SavedObject;
describe('DiscoverIndexPattern', () => {
test('Invalid props dont cause an exception', () => {
const props = {
indexPatternList: null,
selectedIndexPattern: null,
setIndexPattern: jest.fn(),
} as any;
expect(shallowWithIntl(<DiscoverIndexPattern {...props} />)).toMatchSnapshot(`""`);
});
test('A single index pattern is just displayed', () => {
const props = {
indexPatternList: [indexPattern1],
selectedIndexPattern: indexPattern1,
setIndexPattern: jest.fn(),
} as DiscoverIndexPatternProps;
expect(shallowWithIntl(<DiscoverIndexPattern {...props} />)).toMatchSnapshot();
});
test('Multiple index patterns are selectable', () => {
const props = {
indexPatternList: [indexPattern1, indexPattern2],
selectedIndexPattern: indexPattern2,
setIndexPattern: jest.fn(),
} as DiscoverIndexPatternProps;
const component = mountWithIntl(<DiscoverIndexPattern {...props} />);
findTestSubject(component, 'indexPattern-switch-link').simulate('click');
const searchInput = findTestSubject(component, 'comboBoxSearchInput');
searchInput.simulate('change', { target: { value: 'test1' } });
searchInput.simulate('keyDown', { keyCode: comboBoxKeyCodes.ENTER });
expect(props.setIndexPattern).toBeCalledWith('test1');
});
});

View file

@ -0,0 +1,107 @@
/*
* 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 { EuiComboBox } from '@elastic/eui';
import { SavedObject } from 'kibana/server';
import { DiscoverIndexPatternTitle } from './discover_index_pattern_title';
export interface DiscoverIndexPatternProps {
/**
* list of available index patterns, if length > 1, component offers a "change" link
*/
indexPatternList: SavedObject[];
/**
* currently selected index pattern, due to angular issues it's undefined at first rendering
*/
selectedIndexPattern: SavedObject;
/**
* triggered when user selects a new index pattern
*/
setIndexPattern: (id: string) => void;
}
/**
* Component allows you to select an index pattern in discovers side bar
*/
export function DiscoverIndexPattern({
indexPatternList,
selectedIndexPattern,
setIndexPattern,
}: DiscoverIndexPatternProps) {
if (!indexPatternList || indexPatternList.length === 0 || !selectedIndexPattern) {
// just in case, shouldn't happen
return null;
}
const [selected, setSelected] = useState(selectedIndexPattern);
const [showCombo, setShowCombo] = useState(false);
const options = indexPatternList.map(entity => ({
value: entity.id,
label: entity.attributes!.title,
}));
const selectedOptions = selected
? [{ value: selected.id, label: selected.attributes.title }]
: [];
const findIndexPattern = (id?: string) => indexPatternList.find(entity => entity.id === id);
if (!showCombo) {
return (
<DiscoverIndexPatternTitle
isChangeable={indexPatternList.length > 1}
onChange={() => setShowCombo(true)}
title={selected.attributes ? selected.attributes.title : ''}
/>
);
}
/**
* catches a EuiComboBox related 'Can't perform a React state update on an unmounted component'
* warning in console by delaying the hiding/removal of the EuiComboBox a bit
*/
function hideCombo() {
setTimeout(() => setShowCombo(false), 50);
}
return (
<EuiComboBox
className="index-pattern-selection"
data-test-subj="index-pattern-selection"
fullWidth={true}
isClearable={false}
onBlur={() => hideCombo()}
onChange={choices => {
const newSelected = choices[0] && findIndexPattern(choices[0].value);
if (newSelected) {
setSelected(newSelected);
setIndexPattern(newSelected.id);
}
hideCombo();
}}
inputRef={el => {
// auto focus input element when combo box is displayed
if (el) {
el.focus();
}
}}
options={options}
selectedOptions={selectedOptions}
singleSelection={{ asPlainText: true }}
/>
);
}

View file

@ -0,0 +1,32 @@
/*
* 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.
*/
// @ts-ignore
import { uiModules } from 'ui/modules';
import { wrapInI18nContext } from 'ui/i18n';
import { DiscoverIndexPattern } from './discover_index_pattern';
const app = uiModules.get('apps/discover');
app.directive('discoverIndexPatternSelect', function(reactDirective: any) {
return reactDirective(wrapInI18nContext(DiscoverIndexPattern), [
['indexPatternList', { watchDepth: 'reference' }],
['selectedIndexPattern', { watchDepth: 'reference' }],
['setIndexPattern', { watchDepth: 'reference' }],
]);
});

View file

@ -0,0 +1,85 @@
/*
* 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 from 'react';
import { EuiToolTip, EuiFlexItem, EuiFlexGroup, EuiTitle, EuiButtonEmpty } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
export interface DiscoverIndexPatternTitleProps {
/**
* determines whether the change link is displayed
*/
isChangeable: boolean;
/**
* function triggered when the change link is clicked
*/
onChange: () => void;
/**
* title of the current index pattern
*/
title: string;
}
/**
* Component displaying the title of the current selected index pattern
* and if changeable is true, a link is provided to change the index pattern
*/
export function DiscoverIndexPatternTitle({
isChangeable,
onChange,
title,
}: DiscoverIndexPatternTitleProps) {
return (
<EuiFlexGroup gutterSize="none" responsive={false} className="index-pattern-selection">
<EuiFlexItem className="eui-textTruncate">
<EuiToolTip content={title}>
<EuiTitle size="xxs" className="eui-textTruncate">
<h2 title={title}>{title}</h2>
</EuiTitle>
</EuiToolTip>
</EuiFlexItem>
{isChangeable && (
<EuiFlexItem grow={false}>
<EuiToolTip
content={
<FormattedMessage
id="kbn.discover.fieldChooser.indexPattern.changeLinkTooltip"
defaultMessage="Change current index pattern"
/>
}
>
<EuiButtonEmpty
data-test-subj="indexPattern-switch-link"
size="xs"
onClick={() => onChange()}
>
(
<FormattedMessage
id="kbn.discover.fieldChooser.indexPattern.changeLinkLabel"
defaultMessage="change"
description="should be a short word since lack of space"
/>
)
</EuiButtonEmpty>
</EuiToolTip>
</EuiFlexItem>
)}
</EuiFlexGroup>
);
}

View file

@ -1,39 +1,14 @@
<section class="sidebar-list" aria-label="{{::'kbn.discover.fieldChooser.filter.indexAndFieldsSectionAriaLabel' | i18n: {defaultMessage: 'Index and fields'} }}">
<div ng-show="indexPatternList.length > 1">
<ui-select
class="index-pattern-selection"
ng-model="selectedIndexPattern"
on-select="setIndexPattern($item)"
>
<ui-select-match>
<span
class="uiSelectMatch--ellipsis"
tooltip="{{$select.selected.get('title')}}"
tooltip-append-to-body="true"
>
{{$select.selected.get('title')}}
</span>
</ui-select-match>
<ui-select-choices repeat="pattern in indexPatternList | filter:$select.search">
<div ng-bind-html="pattern.get('title') | highlight: $select.search"></div>
</ui-select-choices>
</ui-select>
</div>
<div ng-hide="indexPatternList.length > 1">
<div class="index-pattern">
<h2
class="index-pattern-label"
id="index_pattern_id"
tabindex="0"
css-truncate>
{{ indexPattern.title }}</h2>
</div>
</div>
<discover-index-pattern-select
selected-index-pattern="selectedIndexPattern"
set-index-pattern="setIndexPattern"
index-pattern-list="indexPatternList"
>
</discover-index-pattern-select>
<div class="dscSidebar__listHeader sidebar-list-header" ng-if="fields.length">
<h3
class="euiFlexItem euiTitle euiTitle--xxsmall"
class="euiFlexItem euiTitle euiTitle--xxxsmall sidebar-list-header-heading"
id="selected_fields"
tabindex="0"
i18n-id="kbn.discover.fieldChooser.filter.selectedFieldsTitle"
@ -54,7 +29,7 @@
<div class="sidebar-list-header sidebar-item euiFlexGroup euiFlexGroup--gutterMedium" ng-if="fields.length">
<h3
class="euiFlexItem euiTitle euiTitle--xxsmall"
class="euiFlexItem euiTitle euiTitle--xxxsmall sidebar-list-header-heading"
id="available_fields"
tabindex="0"
i18n-id="kbn.discover.fieldChooser.filter.availableFieldsTitle"

View file

@ -21,7 +21,7 @@ import 'ui/directives/css_truncate';
import { i18n } from '@kbn/i18n';
import 'ui/directives/field_name';
import './discover_field';
import 'ui/angular_ui_select';
import './discover_index_pattern_directive';
import _ from 'lodash';
import $ from 'jquery';
import rison from 'rison-node';
@ -61,8 +61,8 @@ app.directive('discFieldChooser', function ($location, config, $route) {
(pattern) => pattern.id === $scope.indexPattern.id
);
$scope.indexPatternList = _.sortBy($scope.indexPatternList, o => o.get('title'));
$scope.setIndexPattern = function (pattern) {
$scope.state.index = pattern.id;
$scope.setIndexPattern = function (id) {
$scope.state.index = id;
$scope.state.save();
};

View file

@ -1,8 +1,8 @@
// ONLY USED IN DISCOVER
.sidebar-container {
padding-left: 0px !important;
padding-right: 0px !important;
padding-left: $euiSizeS !important;
padding-right: $euiSizeS !important;
background-color: $euiColorLightestShade;
border-right-color: transparent;
border-bottom-color: transparent;
@ -52,40 +52,43 @@
}
}
.sidebar-item-title,
.sidebar-item-text {
margin: 0;
padding: $euiSizeXS $euiSizeS;
text-align: center;
width: 100%;
border: none;
border-radius: 0;
}
.sidebar-item-title,
.sidebar-item-text {
margin: 0;
padding: $euiSizeXS 0;
text-align: center;
width: 100%;
border: none;
border-radius: 0;
}
.sidebar-item-title {
@include euiTextTruncate;
text-align: left;
.sidebar-item-title {
@include euiTextTruncate;
text-align: left;
&.full-title {
white-space: normal;
}
&.full-title {
white-space: normal;
}
}
.sidebar-item-text {
background: $euiColorEmptyShade;
}
.sidebar-item-text {
background: $euiColorEmptyShade;
}
}
.sidebar-list-header {
padding-left: $euiSizeS;
padding-right: $euiSizeS;
color: $euiColorFullShade;
border: 1px solid transparent;
.sidebar-list-header-heading {
color: $euiColorDarkestShade;
border: 1px solid transparent;
}
.sidebar-list-header-label {
font-size: $euiFontSizeS;
padding-left: $euiSizeS;
font-size: $euiFontSizeXS;
line-height: $euiLineHeight;
font-weight: $euiFontWeightBold;
color: $euiColorDarkShade;
border-bottom: 1px solid $euiColorLightShade;
}
}
@ -110,12 +113,7 @@
}
}
.index-pattern-selection {
padding: 0 $euiSizeS;
}
.index-pattern-selection .ui-select-choices {
width: auto;
max-height: 300px;
.index-pattern-selection:not(.euiComboBox) {
padding: $euiSizeS 0;
}
}

View file

@ -30,6 +30,7 @@ export function DiscoverPageProvider({ getService, getPageObjects }) {
const globalNav = getService('globalNav');
const config = getService('config');
const defaultFindTimeout = config.get('timeouts.find');
const comboBox = getService('comboBox');
class DiscoverPage {
async getQueryField() {
@ -271,8 +272,8 @@ export function DiscoverPageProvider({ getService, getPageObjects }) {
}
async selectIndexPattern(indexPattern) {
await find.clickByCssSelector('.index-pattern-selection');
await find.setValue('.ui-select-search', indexPattern + '\n');
await testSubjects.click('indexPattern-switch-link');
await comboBox.set('index-pattern-selection', indexPattern);
await PageObjects.header.waitUntilLoadingHasFinished();
}