[Discover] Deangularize and euificate sidebar (#47559) (#61987)

* Split angular templates into React components

* Add tooltip for field label

* Adapt SCSS

* Cleanup angular directives

* Extract helper functions

* Improve tests + docs

* Move css to _sidebar.scss

* Exclude _id field from displaying the Visualize button to prevent an ES error

* A11y improvements
This commit is contained in:
Matthias Wilhelm 2020-03-31 19:13:26 +02:00 committed by GitHub
parent 1f1d5195aa
commit 886979bca5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
64 changed files with 2005 additions and 1774 deletions

View file

@ -1,81 +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 expect from '@kbn/expect';
import ngMock from 'ng_mock';
import { pluginInstance } from 'plugins/kibana/discover/legacy';
let $parentScope;
let $scope;
let $elem;
const init = function(expandable) {
// Load the application
pluginInstance.initializeServices();
pluginInstance.initializeInnerAngular();
ngMock.module('app/discover');
// Create the scope
ngMock.inject(function($rootScope, $compile) {
// Give us a scope
$parentScope = $rootScope;
// Create the element
$elem = angular.element(
'<span css-truncate ' +
(expandable ? 'css-truncate-expandable' : '') +
'>this isnt important</span>'
);
// And compile it
$compile($elem)($parentScope);
// Fire a digest cycle
$elem.scope().$digest();
// Grab the isolate scope so we can test it
$scope = $elem.isolateScope();
});
};
describe('cssTruncate directive', function() {
describe('expandable', function() {
beforeEach(function() {
init(true);
});
it('should set text-overflow to ellipsis and whitespace to nowrap', function(done) {
expect($elem.css('text-overflow')).to.be('ellipsis');
expect($elem.css('white-space')).to.be('nowrap');
done();
});
it('should set white-space to normal when clicked, and back to nowrap when clicked again', function(done) {
$scope.toggle();
expect($elem.css('white-space')).to.be('normal');
$scope.toggle();
expect($elem.css('white-space')).to.be('nowrap');
done();
});
});
});

View file

@ -1,97 +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 _ from 'lodash';
import sinon from 'sinon';
import ngMock from 'ng_mock';
import expect from '@kbn/expect';
import { pluginInstance } from 'plugins/kibana/discover/legacy';
import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern';
// Load the kibana app dependencies.
describe('discoverField', function() {
let $scope;
let indexPattern;
let $elem;
beforeEach(() => pluginInstance.initializeServices());
beforeEach(() => pluginInstance.initializeInnerAngular());
beforeEach(ngMock.module('app/discover'));
beforeEach(
ngMock.inject(function(Private, $rootScope, $compile) {
$elem = angular.element(`
<discover-field
field="field"
on-add-field="addField"
on-remove-field="removeField"
on-show-details="showDetails"
></discover-field>
`);
indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider);
_.assign($rootScope, {
field: indexPattern.fields.getByName('extension'),
addField: sinon.spy(() => ($rootScope.field.display = true)),
removeField: sinon.spy(() => ($rootScope.field.display = false)),
showDetails: sinon.spy(() => ($rootScope.field.details = { exists: true })),
});
$compile($elem)($rootScope);
$scope = $elem.isolateScope();
$scope.$digest();
sinon.spy($scope, 'toggleDetails');
})
);
afterEach(function() {
$scope.toggleDetails.restore();
$scope.$destroy();
});
describe('toggleDisplay', function() {
it('should exist', function() {
expect($scope.toggleDisplay).to.be.a(Function);
});
it('should call onAddField or onRemoveField depending on the display state', function() {
$scope.toggleDisplay($scope.field);
expect($scope.onAddField.callCount).to.be(1);
expect($scope.onAddField.firstCall.args).to.eql([$scope.field.name]);
$scope.toggleDisplay($scope.field);
expect($scope.onRemoveField.callCount).to.be(1);
expect($scope.onRemoveField.firstCall.args).to.eql([$scope.field.name]);
});
it('should call toggleDetails when currently showing the details', function() {
$scope.toggleDetails($scope.field);
$scope.toggleDisplay($scope.field);
expect($scope.toggleDetails.callCount).to.be(2);
});
});
describe('toggleDetails', function() {
it('should notify the parent when showing the details', function() {
$scope.toggleDetails($scope.field);
expect($scope.onShowDetails.callCount).to.be(1);
});
});
});

View file

@ -1,257 +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 ngMock from 'ng_mock';
import _ from 'lodash';
import sinon from 'sinon';
import expect from '@kbn/expect';
import $ from 'jquery';
import { pluginInstance } from 'plugins/kibana/discover/legacy';
import FixturesHitsProvider from 'fixtures/hits';
import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern';
import { SimpleSavedObject } from '../../../../../../../core/public';
// Load the kibana app dependencies.
let $parentScope;
let $scope;
let hits;
let indexPattern;
let indexPatternList;
// Sets up the directive, take an element, and a list of properties to attach to the parent scope.
const init = function($elem, props) {
ngMock.inject(function($rootScope, $compile, $timeout) {
$parentScope = $rootScope;
_.assign($parentScope, props);
$compile($elem)($parentScope);
// Required for test to run solo. Sigh
$timeout(() => $elem.scope().$digest(), 0);
$scope = $elem.isolateScope();
});
};
const destroy = function() {
$scope.$destroy();
$parentScope.$destroy();
};
describe('discover field chooser directives', function() {
const $elem = angular.element(`
<disc-field-chooser
columns="columns"
toggle="toggle"
hits="hits"
field-counts="fieldCounts"
index-pattern="indexPattern"
index-pattern-list="indexPatternList"
state="state"
on-add-field="addField"
on-add-filter="addFilter"
on-remove-field="removeField"
></disc-field-chooser>
`);
beforeEach(() => pluginInstance.initializeServices());
beforeEach(() => pluginInstance.initializeInnerAngular());
beforeEach(ngMock.module('app/discover'));
beforeEach(
ngMock.inject(function(Private) {
hits = Private(FixturesHitsProvider);
indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider);
indexPatternList = [
new SimpleSavedObject(undefined, { id: '0', attributes: { title: 'b' } }),
new SimpleSavedObject(undefined, { id: '1', attributes: { title: 'a' } }),
new SimpleSavedObject(undefined, { id: '2', attributes: { title: 'c' } }),
];
const fieldCounts = _.transform(
hits,
function(counts, hit) {
_.keys(indexPattern.flattenHit(hit)).forEach(function(key) {
counts[key] = (counts[key] || 0) + 1;
});
},
{}
);
init($elem, {
columns: [],
toggle: sinon.spy(),
hits: hits,
fieldCounts: fieldCounts,
addField: sinon.spy(),
addFilter: sinon.spy(),
indexPattern: indexPattern,
indexPatternList: indexPatternList,
removeField: sinon.spy(),
});
$scope.$digest();
})
);
afterEach(() => destroy());
const getSections = function(ctx) {
return {
selected: $('.dscFieldList--selected', ctx),
popular: $('.dscFieldList--popular', ctx),
unpopular: $('.dscFieldList--unpopular', ctx),
};
};
describe('Field listing', function() {
it('should have Selected Fields, Fields and Popular Fields sections', function() {
const headers = $elem.find('.sidebar-list-header');
expect(headers.length).to.be(3);
});
it('should have 2 popular fields, 1 unpopular field and no selected fields', function() {
const section = getSections($elem);
const popular = find('popular');
const unpopular = find('unpopular');
expect(section.selected.find('li').length).to.be(0);
expect(popular).to.contain('ssl');
expect(popular).to.contain('@timestamp');
expect(popular).to.not.contain('ip\n');
expect(unpopular).to.contain('extension');
expect(unpopular).to.contain('machine.os');
expect(unpopular).to.not.contain('ssl');
function find(popularity) {
return section[popularity]
.find('.dscFieldName')
.map((i, el) => $(el).text())
.toArray();
}
});
it('should show the popular fields header if there are popular fields', function() {
const section = getSections($elem);
expect(section.popular.hasClass('ng-hide')).to.be(false);
expect(section.popular.find('li:not(.sidebar-list-header)').length).to.be.above(0);
});
it('should not show the popular fields if there are not any', function() {
// Re-init
destroy();
_.each(indexPattern.fields, function(field) {
field.$$spec.count = 0;
}); // Reset the popular fields
init($elem, {
columns: [],
toggle: sinon.spy(),
hits: require('fixtures/hits'),
filter: sinon.spy(),
indexPattern: indexPattern,
});
const section = getSections($elem);
$scope.$digest();
expect(section.popular.hasClass('ng-hide')).to.be(true);
expect(section.popular.find('li:not(.sidebar-list-header)').length).to.be(0);
});
it('should move the field into selected when it is added to the columns array', function() {
const section = getSections($elem);
$scope.columns.push('bytes');
$scope.$digest();
expect(section.selected.text()).to.contain('bytes');
expect(section.popular.text()).to.not.contain('bytes');
$scope.columns.push('ip');
$scope.$digest();
expect(section.selected.text()).to.contain('ip\n');
expect(section.unpopular.text()).to.not.contain('ip\n');
expect(section.popular.text()).to.contain('ssl');
});
});
describe('details processing', function() {
let field;
function getField() {
return _.find($scope.fields, { name: 'bytes' });
}
beforeEach(function() {
field = getField();
});
it('should have a computeDetails function', function() {
expect($scope.computeDetails).to.be.a(Function);
});
it('should increase the field popularity when called', function() {
indexPattern.popularizeField = sinon.spy();
$scope.computeDetails(field);
expect(indexPattern.popularizeField.called).to.be(true);
});
it('should append a details object to the field', function() {
$scope.computeDetails(field);
expect(field.details).to.not.be(undefined);
});
it('should delete the field details if they already exist', function() {
$scope.computeDetails(field);
expect(field.details).to.not.be(undefined);
$scope.computeDetails(field);
expect(field.details).to.be(undefined);
});
it('... unless recompute is true', function() {
$scope.computeDetails(field);
expect(field.details).to.not.be(undefined);
$scope.computeDetails(field, true);
expect(field.details).to.not.be(undefined);
});
it('should create buckets with formatted and raw values', function() {
$scope.computeDetails(field);
expect(field.details.buckets).to.not.be(undefined);
expect(field.details.buckets[0].value).to.be(40.141592);
});
it('should recalculate the details on open fields if the hits change', function() {
$scope.hits = [{ _source: { bytes: 1024 } }];
$scope.$apply();
field = getField();
$scope.computeDetails(field);
expect(getField().details.total).to.be(1);
$scope.hits = [{ _source: { notbytes: 1024 } }];
$scope.$apply();
field = getField();
expect(field.details).to.not.have.property('total');
});
});
});

View file

@ -61,6 +61,7 @@ const destroy = function() {
describe('docTable', function() {
let $elem;
beforeEach(() => pluginInstance.initializeInnerAngular());
beforeEach(() => pluginInstance.initializeServices());
beforeEach(ngMock.module('app/discover'));
beforeEach(function() {
$elem = angular.element(`

View file

@ -27,7 +27,7 @@ import { CoreStart, LegacyCoreStart } from 'kibana/public';
import { DataPublicPluginStart } from '../../../../../plugins/data/public';
import { Storage } from '../../../../../plugins/kibana_utils/public';
import { NavigationPublicPluginStart as NavigationStart } from '../../../../../plugins/navigation/public';
import { createDocTableDirective } from './np_ready/angular/doc_table/doc_table';
import { createDocTableDirective } from './np_ready/angular/doc_table';
import { createTableHeaderDirective } from './np_ready/angular/doc_table/components/table_header';
import {
createToolBarPagerButtonsDirective,
@ -37,18 +37,8 @@ import { createTableRowDirective } from './np_ready/angular/doc_table/components
import { createPagerFactory } from './np_ready/angular/doc_table/lib/pager/pager_factory';
import { createInfiniteScrollDirective } from './np_ready/angular/doc_table/infinite_scroll';
import { createDocViewerDirective } from './np_ready/angular/doc_viewer';
import { createFieldSearchDirective } from './np_ready/components/field_chooser/discover_field_search_directive';
import { createIndexPatternSelectDirective } from './np_ready/components/field_chooser/discover_index_pattern_directive';
import { createStringFieldProgressBarDirective } from './np_ready/components/field_chooser/string_progress_bar';
// @ts-ignore
import { FieldNameDirectiveProvider } from './np_ready/angular/directives/field_name';
// @ts-ignore
import { createFieldChooserDirective } from './np_ready/components/field_chooser/field_chooser';
// @ts-ignore
import { createDiscoverFieldDirective } from './np_ready/components/field_chooser/discover_field';
import { CollapsibleSidebarProvider } from './np_ready/angular/directives/collapsible_sidebar/collapsible_sidebar';
import { DiscoverStartPlugins } from './plugin';
import { createCssTruncateDirective } from './np_ready/angular/directives/css_truncate';
// @ts-ignore
import { FixedScrollProvider } from './np_ready/angular/directives/fixed_scroll';
// @ts-ignore
@ -65,6 +55,7 @@ import {
createTopNavDirective,
createTopNavHelper,
} from '../../../../../plugins/kibana_legacy/public';
import { createDiscoverSidebarDirective } from './np_ready/components/sidebar';
/**
* returns the main inner angular module, it contains all the parts of Angular Discover
@ -125,7 +116,6 @@ export function initializeInnerAngularModule(
])
.config(watchMultiDecorator)
.directive('icon', reactDirective => reactDirective(EuiIcon))
.directive('fieldName', FieldNameDirectiveProvider)
.directive('renderComplete', createRenderCompleteDirective)
.service('debounce', ['$timeout', DebounceProviderTimeout]);
}
@ -149,16 +139,10 @@ export function initializeInnerAngularModule(
.run(registerListenEventListener)
.directive('icon', reactDirective => reactDirective(EuiIcon))
.directive('kbnAccessibleClick', KbnAccessibleClickProvider)
.directive('fieldName', FieldNameDirectiveProvider)
.directive('collapsibleSidebar', CollapsibleSidebarProvider)
.directive('cssTruncate', createCssTruncateDirective)
.directive('fixedScroll', FixedScrollProvider)
.directive('renderComplete', createRenderCompleteDirective)
.directive('discoverFieldSearch', createFieldSearchDirective)
.directive('discoverIndexPatternSelect', createIndexPatternSelectDirective)
.directive('stringFieldProgressBar', createStringFieldProgressBarDirective)
.directive('discoverField', createDiscoverFieldDirective)
.directive('discFieldChooser', createFieldChooserDirective)
.directive('discoverSidebar', createDiscoverSidebarDirective)
.service('debounce', ['$timeout', DebounceProviderTimeout]);
}

View file

@ -16,7 +16,6 @@
* specific language governing permissions and limitations
* under the License.
*/
import angular from 'angular'; // just used in embeddables and discover controller
import { DiscoverServices } from './build_services';
let angularModule: any = null;
@ -52,7 +51,6 @@ export const [getUrlTracker, setUrlTracker] = createGetterSetter<{
}>('urlTracker');
// EXPORT legacy static dependencies, should be migrated when available in a new version;
export { angular };
export { wrapInI18nContext } from 'ui/i18n';
import { search } from '../../../../../plugins/data/public';
import { createGetterSetter } from '../../../../../plugins/kibana_utils/common';

View file

@ -1,9 +1,5 @@
discover-app {
flex-grow: 1;
.sidebar-container {
background-color: transparent;
}
}
.dscHistogram {
@ -12,22 +8,6 @@ discover-app {
padding: $euiSizeS;
}
// SASSTODO: replace the margin-top value with a variable
.dscSidebar__listHeader {
margin-top: 5px;
}
.dscFieldList--popular {
padding-top: $euiSizeS;
}
.dscFieldList--selected,
.dscFieldList--unpopular,
.dscFieldList--popular {
padding-left: $euiSizeS;
padding-right: $euiSizeS;
}
// SASSTODO: replace the z-index value with a variable
.dscWrapper {
padding-right: $euiSizeS;
@ -109,107 +89,6 @@ discover-app {
text-align: center;
}
/**
* 1. Override sidebar-item-title styles.
*/
.dscSidebarItem {
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
padding-top: 0 !important; /* 1 */
padding-bottom: 0 !important; /* 1 */
height: $euiSizeXL;
&:hover,
&:focus {
.dscSidebarItem__action {
opacity: 1;
}
}
}
.dscSidebarItem--active {
background: shade($euiColorLightestShade, 5%);
color: $euiColorFullShade;
font-weight: bold;
}
/**
* 1. Truncate long text so it doesn't push the actions outside of the container.
*/
.dscSidebarItem__label {
overflow: hidden; /* 1 */
text-overflow: ellipsis; /* 1 */
}
/**
* 1. Only visually hide the action, so that it's still accessible to screen readers.
* 2. When tabbed to, this element needs to be visible for keyboard accessibility.
*/
.dscSidebarItem__action {
opacity: 0; /* 1 */
&:focus {
opacity: 1; /* 2 */
}
}
.dscFieldSearch {
padding: $euiSizeS;
}
.dscFieldFilter {
margin-top: $euiSizeS;
}
.dscFieldDetails {
padding: $euiSizeS;
background-color: $euiColorLightestShade;
color: $euiTextColor;
}
// SASSTODO: replace the padding and margin values with variables
.dscFieldDetails__progress {
background-color: $euiColorEmptyShade;
color: $euiColorDarkShade;
padding: $euiSizeXS;
}
// SASSTODO: replace the margin-top value with a variable
.dscFieldDetailsItem {
margin-top: 5px;
}
.dscFieldDetails__filter {
cursor: pointer;
}
.dscFieldDetailsItem__title {
line-height: 1.5;
display: flex;
align-items: center;
justify-content: space-between;
}
/**
* 1. If the field name is very long, don't let it squash the buttons.
*/
.dscFieldDetailsItem__buttonGroup {
flex: 0 0 auto; /* 1 */
}
.dscFieldDetailsItem__button {
appearance: none;
border: none;
padding: 0;
background-color: transparent;
}
.dscFieldName--noResults {
color: $euiColorDarkShade;
}
.dscResults {
h3 {
margin: -20px 0 10px 0;

View file

@ -1,61 +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.
*/
export function createCssTruncateDirective() {
return {
restrict: 'A',
scope: {},
link: ($scope: any, $elem: any, attrs: any) => {
$elem.css({
overflow: 'hidden',
'white-space': 'nowrap',
'text-overflow': 'ellipsis',
'word-break': 'break-all',
});
if (attrs.cssTruncateExpandable != null) {
$scope.$watch(
function() {
return $elem.html();
},
function() {
if ($elem[0].offsetWidth < $elem[0].scrollWidth) {
$elem.css({ cursor: 'pointer' });
$elem.bind('click', function() {
$scope.toggle();
});
}
}
);
}
$scope.toggle = function() {
if ($elem.css('white-space') !== 'normal') {
$elem.css({ 'white-space': 'normal' });
} else {
$elem.css({ 'white-space': 'nowrap' });
}
};
$scope.$on('$destroy', function() {
$elem.unbind('click');
$elem.unbind('mouseenter');
});
},
};
}

View file

@ -20,20 +20,21 @@
<main class="container-fluid">
<div class="row">
<div class="col-md-2 sidebar-container collapsible-sidebar" id="discover-sidebar" data-test-subj="discover-sidebar">
<div class="col-md-2 dscSidebar__container collapsible-sidebar" id="discover-sidebar" data-test-subj="discover-sidebar">
<div class="dscFieldChooser">
<disc-field-chooser
<discover-sidebar
columns="state.columns"
hits="rows"
field-counts="fieldCounts"
index-pattern="searchSource.getField('index')"
hits="rows"
index-pattern-list="opts.indexPatternList"
state="state"
on-add-field="addColumn"
on-add-filter="filterQuery"
on-remove-field="removeColumn"
selected-index-pattern="searchSource.getField('index')"
set-index-pattern="setIndexPattern"
state="state"
>
</disc-field-chooser>
</discover-sidebar>
</div>
</div>

View file

@ -28,7 +28,7 @@ import {
import { esFilters, Filter, Query } from '../../../../../../../plugins/data/public';
import { migrateLegacyQuery } from '../../../../../../../plugins/kibana_legacy/public';
interface AppState {
export interface AppState {
/**
* Columns displayed in the table
*/

View file

@ -1,2 +1,2 @@
@import 'fetch_error/index';
@import 'field_chooser/index';
@import 'sidebar/index';

View file

@ -1,36 +0,0 @@
.dscFieldChooser {
padding-left: $euiSizeS !important;
padding-right: $euiSizeS !important;
}
.dscFieldChooser__toggle {
color: $euiColorMediumShade;
margin-left: $euiSizeS !important;
}
.dscFieldName {
color: $euiColorDarkShade;
}
/*
Fixes EUI known issue https://github.com/elastic/eui/issues/1749
*/
.dscProgressBarTooltip__anchor {
display: block;
}
.dscToggleFieldFilterButton {
width: calc(100% - #{$euiSizeS});
color: $euiColorPrimary;
padding-left: $euiSizeXS;
margin-left: $euiSizeXS;
}
.dscFieldSearch__filterWrapper {
flex-grow: 0;
}
.dscFieldSearch__formWrapper {
padding: $euiSizeM;
}

View file

@ -1,26 +0,0 @@
<li
class="sidebar-item"
attr-field="{{::field.name}}"
>
<div
data-test-subj="field-{{::field.name}}"
ng-click="onClickToggleDetails($event, field)"
kbn-accessible-click
class="sidebar-item-title dscSidebarItem"
>
<div class="dscField dscSidebarItem__label">
<field-name
field="field"
></field-name>
</div>
<button
ng-if="field.name !== '_source'"
ng-click="toggleDisplay(field)"
ng-class="::field.display ? 'kuiButton--danger' : 'kuiButton--primary'"
ng-bind="::addRemoveButtonLabel"
class="dscSidebarItem__action kuiButton kuiButton--small"
data-test-subj="fieldToggle-{{::field.name}}"
></button>
</div>
</li>

View file

@ -1,139 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import $ from 'jquery';
import _ from 'lodash';
import { i18n } from '@kbn/i18n';
import { getServices } from '../../../kibana_services';
import html from './discover_field.html';
import './string_progress_bar';
import detailsHtml from './lib/detail_views/string.html';
export function createDiscoverFieldDirective($compile) {
return {
restrict: 'E',
template: html,
replace: true,
scope: {
field: '=',
onAddField: '=',
onAddFilter: '=',
onRemoveField: '=',
onShowDetails: '=',
},
link: function($scope, $elem) {
let detailsElem;
let detailScope;
const init = function() {
if ($scope.field.details) {
$scope.toggleDetails($scope.field, true);
}
$scope.addRemoveButtonLabel = $scope.field.display
? i18n.translate('kbn.discover.fieldChooser.discoverField.removeButtonLabel', {
defaultMessage: 'remove',
})
: i18n.translate('kbn.discover.fieldChooser.discoverField.addButtonLabel', {
defaultMessage: 'add',
});
};
const getWarnings = function(field) {
let warnings = [];
if (field.scripted) {
warnings.push(
i18n.translate(
'kbn.discover.fieldChooser.discoverField.scriptedFieldsTakeLongExecuteDescription',
{
defaultMessage: 'Scripted fields can take a long time to execute.',
}
)
);
}
if (warnings.length > 1) {
warnings = warnings.map(function(warning, i) {
return (i > 0 ? '\n' : '') + (i + 1) + ' - ' + warning;
});
}
return warnings;
};
$scope.canVisualize = getServices().capabilities.visualize.show;
$scope.toggleDisplay = function(field) {
if (field.display) {
$scope.onRemoveField(field.name);
} else {
$scope.onAddField(field.name);
}
if (field.details) {
$scope.toggleDetails(field);
}
};
$scope.onClickToggleDetails = function onClickToggleDetails($event, field) {
// Do nothing if the event originated from a child.
if ($event.currentTarget !== $event.target) {
$event.preventDefault();
}
$scope.toggleDetails(field);
};
$scope.toggleDetails = function(field, recompute) {
if (_.isUndefined(field.details) || recompute) {
$scope.onShowDetails(field, recompute);
detailScope = $scope.$new();
detailScope.warnings = getWarnings(field);
detailScope.getBucketAriaLabel = bucket => {
return i18n.translate('kbn.discover.fieldChooser.discoverField.bucketAriaLabel', {
defaultMessage: 'Value: {value}',
values: {
value:
bucket.display === ''
? i18n.translate('kbn.discover.fieldChooser.discoverField.emptyStringText', {
defaultMessage: 'Empty string',
})
: bucket.display,
},
});
};
detailsElem = $(detailsHtml);
$compile(detailsElem)(detailScope);
$elem.append(detailsElem).addClass('active');
$elem.find('.dscSidebarItem').addClass('dscSidebarItem--active');
} else {
delete field.details;
detailScope.$destroy();
detailsElem.remove();
$elem.removeClass('active');
$elem.find('.dscSidebarItem').removeClass('dscSidebarItem--active');
}
};
init();
},
};
}

View file

@ -1,99 +0,0 @@
<section class="sidebar-list" aria-label="{{::'kbn.discover.fieldChooser.filter.indexAndFieldsSectionAriaLabel' | i18n: {defaultMessage: 'Index and fields'} }}">
<discover-index-pattern-select
selected-index-pattern="$parent.indexPattern"
set-index-pattern="$parent.setIndexPattern"
index-pattern-list="indexPatternList"
>
</discover-index-pattern-select>
<div class="sidebar-item" >
<form>
<discover-field-search
on-change="setFilterValue"
value="filter.vals.name"
types="fieldTypes"
>
</discover-field-search>
</form>
</div>
<div class="dscSidebar__listHeader sidebar-list-header" ng-if="fields.length">
<h3
class="euiFlexItem euiTitle euiTitle--xxxsmall sidebar-list-header-heading"
id="selected_fields"
tabindex="0"
i18n-id="kbn.discover.fieldChooser.filter.selectedFieldsTitle"
i18n-default-message="Selected fields"
></h3>
</div>
<ul class="list-unstyled dscFieldList--selected" aria-labelledby="selected_fields">
<discover-field
ng-repeat="field in fields|filter:filter.isFieldFilteredAndDisplayed"
field="field"
on-add-field="onAddField"
on-add-filter="onAddFilter"
on-remove-field="onRemoveField"
on-show-details="computeDetails"
>
</discover-field>
</ul>
<div class="sidebar-list-header sidebar-item euiFlexGroup euiFlexGroup--gutterMedium" ng-if="fields.length">
<h3
class="euiFlexItem euiTitle euiTitle--xxxsmall sidebar-list-header-heading"
id="available_fields"
tabindex="0"
i18n-id="kbn.discover.fieldChooser.filter.availableFieldsTitle"
i18n-default-message="Available fields"
></h3>
<div class="euiFlexItem euiFlexItem--flexGrowZero">
<button
ng-click="$parent.showFields = !$parent.showFields"
aria-hidden="true"
class="kuiButton kuiButton--small visible-xs visible-sm pull-right dscFieldChooser__toggle"
>
<span
aria-hidden="true"
class="kuiIcon"
ng-class="{ 'fa-chevron-right': !$parent.showFields, 'fa-chevron-down': $parent.showFields }"
></span>
</button>
</div>
</div>
<ul
ng-show="(popularFields | filter:filter.isFieldFilteredAndNotDisplayed).length > 0"
ng-class="{ 'hidden-sm': !showFields, 'hidden-xs': !showFields }"
class="list-unstyled sidebar-well dscFieldList--popular">
<li class="sidebar-item sidebar-list-header">
<h6
i18n-id="kbn.discover.fieldChooser.filter.popularTitle"
i18n-default-message="Popular"
></h6>
</li>
<discover-field
ng-repeat="field in popularFields | filter:filter.isFieldFilteredAndNotDisplayed"
field="field"
on-add-field="onAddField"
on-add-filter="onAddFilter"
on-remove-field="onRemoveField"
on-show-details="computeDetails"
>
</discover-field>
</ul>
<ul
ng-class="{ 'hidden-sm': !showFields, 'hidden-xs': !showFields }"
class="list-unstyled dscFieldList--unpopular">
<discover-field
ng-repeat="field in unpopularFields | filter:filter.isFieldFilteredAndNotDisplayed"
field="field"
on-add-field="onAddField"
on-add-filter="onAddFilter"
on-remove-field="onRemoveField"
on-show-details="computeDetails"
>
</discover-field>
</ul>
</section>

View file

@ -1,296 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import _ from 'lodash';
import $ from 'jquery';
import rison from 'rison-node';
import { fieldCalculator } from './lib/field_calculator';
import './discover_field';
import './discover_field_search_directive';
import './discover_index_pattern_directive';
import fieldChooserTemplate from './field_chooser.html';
import {
IndexPatternFieldList,
KBN_FIELD_TYPES,
} from '../../../../../../../../plugins/data/public';
import { getMapsAppUrl, isFieldVisualizable, isMapsAppRegistered } from './lib/visualize_url_utils';
import { getServices } from '../../../kibana_services';
export function createFieldChooserDirective($location) {
return {
restrict: 'E',
scope: {
columns: '=',
hits: '=',
fieldCounts: '=',
state: '=',
indexPattern: '=',
indexPatternList: '=',
onAddField: '=',
onAddFilter: '=',
onRemoveField: '=',
},
template: fieldChooserTemplate,
link: function($scope) {
$scope.showFilter = false;
$scope.toggleShowFilter = () => ($scope.showFilter = !$scope.showFilter);
$scope.indexPatternList = _.sortBy($scope.indexPatternList, o => o.get('title'));
const config = getServices().uiSettings;
const filter = ($scope.filter = {
props: ['type', 'aggregatable', 'searchable', 'missing', 'name'],
defaults: {
missing: true,
type: 'any',
name: '',
},
boolOpts: [
{ label: 'any', value: undefined },
{ label: 'yes', value: true },
{ label: 'no', value: false },
],
reset: function() {
filter.vals = _.clone(filter.defaults);
},
/**
* filter for fields that are displayed / selected for the data table
*/
isFieldFilteredAndDisplayed: function(field) {
return field.display && isFieldFiltered(field);
},
/**
* filter for fields that are not displayed / selected for the data table
*/
isFieldFilteredAndNotDisplayed: function(field) {
return !field.display && isFieldFiltered(field) && field.type !== '_source';
},
getActive: function() {
return _.some(filter.props, function(prop) {
return filter.vals[prop] !== filter.defaults[prop];
});
},
});
function isFieldFiltered(field) {
const matchFilter = filter.vals.type === 'any' || field.type === filter.vals.type;
const isAggregatable =
filter.vals.aggregatable == null || field.aggregatable === filter.vals.aggregatable;
const isSearchable =
filter.vals.searchable == null || field.searchable === filter.vals.searchable;
const scriptedOrMissing =
!filter.vals.missing || field.type === '_source' || field.scripted || field.rowCount > 0;
const matchName = !filter.vals.name || field.name.indexOf(filter.vals.name) !== -1;
return matchFilter && isAggregatable && isSearchable && scriptedOrMissing && matchName;
}
$scope.setFilterValue = (name, value) => {
filter.vals[name] = value;
};
$scope.filtersActive = 0;
// set the initial values to the defaults
filter.reset();
$scope.$watchCollection('filter.vals', function() {
filter.active = filter.getActive();
if (filter.vals) {
let count = 0;
Object.keys(filter.vals).forEach(key => {
if (key === 'missing' || key === 'name') {
return;
}
const value = filter.vals[key];
if ((value && value !== 'any') || value === false) {
count++;
}
});
$scope.filtersActive = count;
}
});
$scope.$watchMulti(['[]fieldCounts', '[]columns', '[]hits'], function(cur, prev) {
const newHits = cur[2] !== prev[2];
let fields = $scope.fields;
const columns = $scope.columns || [];
const fieldCounts = $scope.fieldCounts;
if (!fields || newHits) {
$scope.fields = fields = getFields();
}
if (!fields) return;
// group the fields into popular and up-popular lists
_.chain(fields)
.each(function(field) {
field.displayOrder = _.indexOf(columns, field.name) + 1;
field.display = !!field.displayOrder;
field.rowCount = fieldCounts[field.name];
})
.sortBy(function(field) {
return (field.count || 0) * -1;
})
.groupBy(function(field) {
if (field.display) return 'selected';
return field.count > 0 ? 'popular' : 'unpopular';
})
.tap(function(groups) {
groups.selected = _.sortBy(groups.selected || [], 'displayOrder');
groups.popular = groups.popular || [];
groups.unpopular = groups.unpopular || [];
// move excess popular fields to un-popular list
const extras = groups.popular.splice(config.get('fields:popularLimit'));
groups.unpopular = extras.concat(groups.unpopular);
})
.each(function(group, name) {
$scope[name + 'Fields'] = _.sortBy(group, name === 'selected' ? 'display' : 'name');
})
.commit();
// include undefined so the user can clear the filter
$scope.fieldTypes = _.union(['any'], _.pluck(fields, 'type'));
});
$scope.increaseFieldCounter = function(fieldName) {
$scope.indexPattern.popularizeField(fieldName, 1);
};
function getVisualizeUrl(field) {
if (!$scope.state) {
return '';
}
if (
(field.type === KBN_FIELD_TYPES.GEO_POINT || field.type === KBN_FIELD_TYPES.GEO_SHAPE) &&
isMapsAppRegistered()
) {
return getMapsAppUrl(field, $scope.indexPattern, $scope.state, $scope.columns);
}
let agg = {};
const isGeoPoint = field.type === KBN_FIELD_TYPES.GEO_POINT;
const type = isGeoPoint ? 'tile_map' : 'histogram';
// If we're visualizing a date field, and our index is time based (and thus has a time filter),
// then run a date histogram
if (field.type === 'date' && $scope.indexPattern.timeFieldName === field.name) {
agg = {
type: 'date_histogram',
schema: 'segment',
params: {
field: field.name,
interval: 'auto',
},
};
} else if (isGeoPoint) {
agg = {
type: 'geohash_grid',
schema: 'segment',
params: {
field: field.name,
precision: 3,
},
};
} else {
agg = {
type: 'terms',
schema: 'segment',
params: {
field: field.name,
size: parseInt(config.get('discover:aggs:terms:size'), 10),
orderBy: '2',
},
};
}
return (
'#/visualize/create?' +
$.param(
_.assign(_.clone($location.search()), {
indexPattern: $scope.state.index,
type: type,
_a: rison.encode({
filters: $scope.state.filters || [],
query: $scope.state.query || undefined,
vis: {
type: type,
aggs: [{ schema: 'metric', type: 'count', id: '2' }, agg],
},
}),
})
)
);
}
$scope.computeDetails = function(field, recompute) {
if (_.isUndefined(field.details) || recompute) {
field.details = {
visualizeUrl: isFieldVisualizable(field) ? getVisualizeUrl(field) : null,
...fieldCalculator.getFieldValueCounts({
hits: $scope.hits,
field: field,
count: 5,
grouped: false,
}),
};
_.each(field.details.buckets, function(bucket) {
bucket.display = field.format.convert(bucket.value);
});
$scope.increaseFieldCounter(field, 1);
} else {
delete field.details;
}
};
function getFields() {
const prevFields = $scope.fields;
const indexPattern = $scope.indexPattern;
const hits = $scope.hits;
const fieldCounts = $scope.fieldCounts;
if (!indexPattern || !hits || !fieldCounts) return;
const fieldSpecs = indexPattern.fields.slice(0);
const fieldNamesInDocs = _.keys(fieldCounts);
const fieldNamesInIndexPattern = _.map(indexPattern.fields, 'name');
_.difference(fieldNamesInDocs, fieldNamesInIndexPattern).forEach(function(
unknownFieldName
) {
fieldSpecs.push({
name: unknownFieldName,
type: 'unknown',
});
});
const fields = new IndexPatternFieldList(indexPattern, fieldSpecs);
if (prevFields) {
fields.forEach(function(field) {
field.details = (prevFields.getByName(field.name) || {}).details;
});
}
return fields;
}
},
};
}

View file

@ -1,106 +0,0 @@
<div class="dscFieldDetails">
<div class="kuiVerticalRhythmSmall">
<p class="euiText euiText--extraSmall euiTextColor--subdued" ng-show="!field.details.error">
<span
i18n-id="kbn.discover.fieldChooser.detailViews.topValuesInRecordsDescription"
i18n-default-message="Top 5 values in"
></span>
<span ng-if="!field.details.error">
<a
class="kuiLink"
kbn-accessible-click
ng-show="!field.indexPattern.metaFields.includes(field.name) && !field.scripted"
ng-click="onAddFilter('_exists_', field.name, '+')">
{{::field.details.exists}}
</a>
<span
ng-show="field.indexPattern.metaFields.includes(field.name) || field.scripted">
{{::field.details.exists}}
</span>
/ {{::field.details.total}}
<span
i18n-id="kbn.discover.fieldChooser.detailViews.recordsText"
i18n-default-message="records"
></span>
</span>
</p>
<div class="clearfix"></div>
<div ng-if="field.details.error" class="euiText euiText--extraSmall euiTextColor--subdued">{{field.details.error}}</div>
<div ng-if="!field.details.error">
<div ng-repeat="bucket in ::field.details.buckets" class="dscFieldDetailsItem">
<div class="dscFieldDetailsItem__title">
<!-- Field value -->
<div
css-truncate
css-truncate-expandable="true"
class="dscFieldDetails__value"
aria-label="{{::getBucketAriaLabel(bucket)}}"
>
{{::bucket.display}}
<em
ng-show="bucket.display === ''"
i18n-id="kbn.discover.fieldChooser.detailViews.emptyStringText"
i18n-default-message="Empty string"
></em>
</div>
<!-- Add/remove filter buttons -->
<div
class="dscFieldDetailsItem__buttonGroup"
ng-show="field.filterable"
>
<button
class="dscFieldDetailsItem__button"
ng-click="onAddFilter(field, bucket.value, '+')"
aria-label="{{::'kbn.discover.fieldChooser.detailViews.filterValueButtonAriaLabel' | i18n: {defaultMessage: 'Filter for this value'} }}"
data-test-subj="plus-{{::field.name}}-{{::bucket.display}}"
>
<span
aria-hidden="true"
class="kuiIcon fa-search-plus dscFieldDetails__filter"
></span>
</button>
<button
class="dscFieldDetailsItem__button"
ng-click="onAddFilter(field, bucket.value, '-')"
aria-label="{{::'kbn.discover.fieldChooser.detailViews.filterOutValueButtonAriaLabel' | i18n: {defaultMessage: 'Filter out this value'} }}"
data-test-subj="minus-{{::field.name}}-{{::bucket.display}}"
>
<span
aria-hidden="true"
class="kuiIcon fa-search-minus dscFieldDetails__filter"
></span>
</button>
</div>
</div>
<string-field-progress-bar
percent="{{bucket.percent}}"
count="{{::bucket.count}}"
></string-field-progress-bar>
</div>
</div>
</div>
<a
ng-href="{{field.details.visualizeUrl}}"
ng-show="field.details.visualizeUrl && canVisualize"
class="kuiButton kuiButton--secondary kuiButton--small kuiVerticalRhythmSmall"
data-test-subj="fieldVisualize-{{::field.name}}"
>
<span
i18n-id="kbn.discover.fieldChooser.detailViews.visualizeLinkText"
i18n-default-message="Visualize"
></span>
<span class="discover-field-vis-warning" ng-show="warnings.length" tooltip="{{warnings.join(' ')}}">
( <span
i18n-id="kbn.discover.fieldChooser.detailViews.warningsText"
i18n-default-message="{warningsLength, plural, one {# warning} other {# warnings}}"
i18n-values="{ warningsLength: warnings.length }"
></span> <i aria-hidden="true" class="fa fa-warning"></i> )
</span>
</a>
</div>

View file

@ -1,110 +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 uuid from 'uuid/v4';
// @ts-ignore
import rison from 'rison-node';
import {
IFieldType,
IIndexPattern,
KBN_FIELD_TYPES,
} from '../../../../../../../../../plugins/data/public';
import { AppState } from '../../../angular/context_state';
import { getServices } from '../../../../kibana_services';
function getMapsAppBaseUrl() {
const mapsAppVisAlias = getServices()
.visualizations.getAliases()
.find(({ name }) => {
return name === 'maps';
});
return mapsAppVisAlias ? mapsAppVisAlias.aliasUrl : null;
}
export function isMapsAppRegistered() {
return getServices()
.visualizations.getAliases()
.some(({ name }) => {
return name === 'maps';
});
}
export function isFieldVisualizable(field: IFieldType) {
if (
(field.type === KBN_FIELD_TYPES.GEO_POINT || field.type === KBN_FIELD_TYPES.GEO_SHAPE) &&
isMapsAppRegistered()
) {
return true;
}
return field.visualizable;
}
export function getMapsAppUrl(
field: IFieldType,
indexPattern: IIndexPattern,
appState: AppState,
columns: string[]
) {
const mapAppParams = new URLSearchParams();
// Copy global state
const locationSplit = window.location.href.split('discover?');
if (locationSplit.length > 1) {
const discoverParams = new URLSearchParams(locationSplit[1]);
const globalStateUrlValue = discoverParams.get('_g');
if (globalStateUrlValue) {
mapAppParams.set('_g', globalStateUrlValue);
}
}
// Copy filters and query in app state
const mapsAppState: any = {
filters: appState.filters || [],
};
if (appState.query) {
mapsAppState.query = appState.query;
}
// @ts-ignore
mapAppParams.set('_a', rison.encode(mapsAppState));
// create initial layer descriptor
const hasColumns = columns && columns.length && columns[0] !== '_source';
const supportsClustering = field.aggregatable;
mapAppParams.set(
'initialLayers',
// @ts-ignore
rison.encode_array([
{
id: uuid(),
label: indexPattern.title,
sourceDescriptor: {
id: uuid(),
type: 'ES_SEARCH',
geoField: field.name,
tooltipProperties: hasColumns ? columns : [],
indexPatternId: indexPattern.id,
scalingType: supportsClustering ? 'CLUSTERS' : 'LIMIT',
},
visible: true,
type: supportsClustering ? 'BLENDED_VECTOR' : 'VECTOR',
},
])
);
return getServices().addBasePath(`${getMapsAppBaseUrl()}?${mapAppParams.toString()}`);
}

View file

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

View file

@ -0,0 +1,155 @@
.dscSidebar__container {
padding-left: 0 !important;
padding-right: 0 !important;
background-color: transparent;
border-right-color: transparent;
border-bottom-color: transparent;
}
.dscIndexPattern__container {
display: flex;
align-items: center;
height: $euiSize * 3;
margin-top: -$euiSizeS;
}
.dscIndexPattern__triggerButton {
@include euiTitle('xs');
line-height: $euiSizeXXL;
}
.dscFieldList {
list-style: none;
margin-bottom: 0;
}
.dscFieldList--selected,
.dscFieldList--unpopular,
.dscFieldList--popular {
padding-left: $euiSizeS;
padding-right: $euiSizeS;
}
.dscFieldListHeader {
padding: $euiSizeS $euiSizeS 0 $euiSizeS;
background-color: lightOrDarkTheme(tint($euiColorPrimary, 90%), $euiColorLightShade);
}
.dscFieldList--popular {
background-color: lightOrDarkTheme(tint($euiColorPrimary, 90%), $euiColorLightShade);
}
.dscFieldChooser {
padding-left: $euiSizeS !important;
padding-right: $euiSizeS !important;
}
.dscFieldChooser__toggle {
color: $euiColorMediumShade;
margin-left: $euiSizeS !important;
}
.dscSidebarItem {
border-top: 1px solid transparent;
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 2px;
cursor: pointer;
font-size: $euiFontSizeXS;
border-top: solid 1px transparent;
border-bottom: solid 1px transparent;
line-height: normal;
&:hover,
&:focus {
.dscSidebarItem__action {
opacity: 1;
}
}
}
.dscSidebarItem--active {
border-top: 1px solid $euiColorLightShade;
background: shade($euiColorLightestShade, 5%);
color: $euiColorFullShade;
.euiText {
font-weight: bold;
}
}
.dscSidebarField {
padding: $euiSizeXS 0;
display: flex;
align-items: flex-start;
max-width: 100%;
margin: 0;
width: 100%;
border: none;
border-radius: 0;
text-align: left;
}
.dscSidebarField__name {
margin-left: $euiSizeS;
flex-grow: 1;
}
.dscSidebarField__fieldIcon {
margin-top: $euiSizeXS / 2;
margin-right: $euiSizeXS / 2;
}
/**
* 1. Only visually hide the action, so that it's still accessible to screen readers.
* 2. When tabbed to, this element needs to be visible for keyboard accessibility.
*/
.dscSidebarItem__action {
opacity: 0; /* 1 */
&:focus {
opacity: 1; /* 2 */
}
font-size: 12px;
padding: 2px 6px !important;
height: 22px !important;
min-width: auto !important;
.euiButton__content {
padding: 0 4px;
}
}
/*
Fixes EUI known issue https://github.com/elastic/eui/issues/1749
*/
.dscProgressBarTooltip__anchor {
display: block;
}
.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 {
padding: $euiSizeS;
background-color: $euiColorLightestShade;
color: $euiTextColor;
margin-bottom: $euiSizeS;
}

View file

@ -0,0 +1,111 @@
/*
* 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';
// @ts-ignore
import { findTestSubject } from '@elastic/eui/lib/test';
// @ts-ignore
import StubIndexPattern from 'test_utils/stub_index_pattern';
// @ts-ignore
import stubbedLogstashFields from 'fixtures/logstash_fields';
import { mountWithIntl } from 'test_utils/enzyme_helpers';
import { DiscoverField } from './discover_field';
import { coreMock } from '../../../../../../../../core/public/mocks';
import { IndexPatternField } from '../../../../../../../../plugins/data/public';
jest.mock('../../../kibana_services', () => ({
getServices: () => ({
history: {
location: {
search: '',
},
},
capabilities: {
visualize: {
show: true,
},
},
uiSettings: {
get: (key: string) => {
if (key === 'fields:popularLimit') {
return 5;
} else if (key === 'shortDots:enable') {
return false;
}
},
},
}),
}));
function getComponent(selected = false, showDetails = false, useShortDots = false) {
const indexPattern = new StubIndexPattern(
'logstash-*',
(cfg: any) => cfg,
'time',
stubbedLogstashFields(),
coreMock.createStart()
);
const field = {
name: 'bytes',
type: 'number',
esTypes: ['long'],
count: 10,
scripted: false,
searchable: true,
aggregatable: true,
readFromDocValues: true,
format: null,
routes: {},
$$spec: {},
} as IndexPatternField;
const props = {
indexPattern,
field,
getDetails: jest.fn(),
onAddFilter: jest.fn(),
onAddField: jest.fn(),
onRemoveField: jest.fn(),
onShowDetails: jest.fn(),
showDetails,
selected,
useShortDots,
};
const comp = mountWithIntl(<DiscoverField {...props} />);
return { comp, props };
}
describe('discover sidebar field', function() {
it('should allow selecting fields', function() {
const { comp, props } = getComponent();
findTestSubject(comp, 'fieldToggle-bytes').simulate('click');
expect(props.onAddField).toHaveBeenCalledWith('bytes');
});
it('should allow deselecting fields', function() {
const { comp, props } = getComponent(true);
findTestSubject(comp, 'fieldToggle-bytes').simulate('click');
expect(props.onRemoveField).toHaveBeenCalledWith('bytes');
});
it('should trigger onShowDetails', function() {
const { comp, props } = getComponent();
findTestSubject(comp, 'field-bytes-showDetails').simulate('click');
expect(props.onShowDetails).toHaveBeenCalledWith(true, props.field);
});
});

View file

@ -0,0 +1,186 @@
/*
* 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 { EuiButton, EuiToolTip, EuiText } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { DiscoverFieldDetails } from './discover_field_details';
import { FieldIcon } from '../../../../../../../../plugins/kibana_react/public';
import { FieldDetails } from './types';
import { IndexPatternField, IndexPattern } from '../../../../../../../../plugins/data/public';
import { shortenDottedString } from '../../helpers';
import { getFieldTypeName } from './lib/get_field_type_name';
export interface DiscoverFieldProps {
/**
* The displayed field
*/
field: IndexPatternField;
/**
* The currently selected index pattern
*/
indexPattern: IndexPattern;
/**
* Callback to add/select the field
*/
onAddField: (fieldName: string) => void;
/**
* Callback to add a filter to filter bar
*/
onAddFilter: (field: IndexPatternField | string, value: string, type: '+' | '-') => void;
/**
* Callback to remove/deselect a the field
* @param fieldName
*/
onRemoveField: (fieldName: string) => void;
/**
* Callback to hide/show details, buckets of the field
*/
onShowDetails: (show: boolean, field: IndexPatternField) => void;
/**
* Determines, whether details of the field are displayed
*/
showDetails: boolean;
/**
* Retrieve details data for the field
*/
getDetails: (field: IndexPatternField) => FieldDetails;
/**
* Determines whether the field is selected
*/
selected?: boolean;
/**
* Determines whether the field name is shortened test.sub1.sub2 = t.s.sub2
*/
useShortDots?: boolean;
}
export function DiscoverField({
field,
indexPattern,
onAddField,
onRemoveField,
onAddFilter,
onShowDetails,
showDetails,
getDetails,
selected,
useShortDots,
}: DiscoverFieldProps) {
const addLabel = i18n.translate('kbn.discover.fieldChooser.discoverField.addButtonLabel', {
defaultMessage: 'Add',
});
const addLabelAria = i18n.translate(
'kbn.discover.fieldChooser.discoverField.addButtonAriaLabel',
{
defaultMessage: 'Add {field} to table',
values: { field: field.name },
}
);
const removeLabel = i18n.translate('kbn.discover.fieldChooser.discoverField.removeButtonLabel', {
defaultMessage: 'Remove',
});
const removeLabelAria = i18n.translate(
'kbn.discover.fieldChooser.discoverField.removeButtonAriaLabel',
{
defaultMessage: 'Remove {field} from table',
values: { field: field.name },
}
);
const toggleDisplay = (f: IndexPatternField) => {
if (selected) {
onRemoveField(f.name);
} else {
onAddField(f.name);
}
};
return (
<>
<div
className={`dscSidebarField dscSidebarItem ${showDetails ? 'dscSidebarItem--active' : ''}`}
tabIndex={0}
onClick={() => onShowDetails(!showDetails, field)}
onKeyPress={() => onShowDetails(!showDetails, field)}
data-test-subj={`field-${field.name}-showDetails`}
>
<span className="dscSidebarField__fieldIcon">
<FieldIcon
type={field.type}
label={getFieldTypeName(field.type)}
scripted={field.scripted}
/>
</span>
<span className="dscSidebarField__name eui-textTruncate">
<EuiToolTip
position="top"
content={field.name}
delay="long"
anchorClassName="eui-textTruncate"
>
<EuiText size="xs" data-test-subj={`field-${field.name}`} className="eui-textTruncate">
{useShortDots ? shortenDottedString(field.name) : field.displayName}
</EuiText>
</EuiToolTip>
</span>
<span>
{field.name !== '_source' && !selected && (
<EuiButton
fill
size="s"
className="dscSidebarItem__action"
onClick={(ev: React.MouseEvent<HTMLButtonElement>) => {
ev.preventDefault();
ev.stopPropagation();
toggleDisplay(field);
}}
data-test-subj={`fieldToggle-${field.name}`}
arial-label={addLabelAria}
>
{addLabel}
</EuiButton>
)}
{field.name !== '_source' && selected && (
<EuiButton
color="danger"
className="dscSidebarItem__action"
onClick={(ev: React.MouseEvent<HTMLButtonElement>) => {
ev.preventDefault();
ev.stopPropagation();
toggleDisplay(field);
}}
data-test-subj={`fieldToggle-${field.name}`}
arial-label={removeLabelAria}
>
{removeLabel}
</EuiButton>
)}
</span>
</div>
{showDetails && (
<DiscoverFieldDetails
indexPattern={indexPattern}
field={field}
details={getDetails(field)}
onAddFilter={onAddFilter}
/>
)}
</>
);
}

View file

@ -0,0 +1,99 @@
/*
* 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 { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { StringFieldProgressBar } from './string_progress_bar';
import { Bucket } from './types';
import { IndexPatternField } from '../../../../../../../../plugins/data/public';
interface Props {
bucket: Bucket;
field: IndexPatternField;
onAddFilter: (field: IndexPatternField | string, value: string, type: '+' | '-') => void;
}
export function DiscoverFieldBucket({ field, bucket, onAddFilter }: Props) {
const emptyTxt = i18n.translate('kbn.discover.fieldChooser.detailViews.emptyStringText', {
defaultMessage: 'Empty string',
});
const addLabel = i18n.translate(
'kbn.discover.fieldChooser.detailViews.filterValueButtonAriaLabel',
{
defaultMessage: 'Filter for {field}: "{value}"',
values: { value: bucket.value, field: field.name },
}
);
const removeLabel = i18n.translate(
'kbn.discover.fieldChooser.detailViews.filterOutValueButtonAriaLabel',
{
defaultMessage: 'Filter out {field}: "{value}"',
values: { value: bucket.value, field: field.name },
}
);
return (
<>
<EuiFlexGroup gutterSize="xs" responsive={false}>
<EuiFlexItem className="eui-textTruncate">
<EuiText size="xs" className="eui-textTruncate">
{bucket.display === '' ? emptyTxt : bucket.display}
</EuiText>
</EuiFlexItem>
{field.filterable && (
<EuiFlexItem grow={false}>
<div>
<EuiButtonIcon
iconSize="s"
iconType="magnifyWithPlus"
onClick={() => onAddFilter(field, bucket.value, '+')}
aria-label={addLabel}
data-test-subj={`plus-${field.name}-${bucket.value}`}
style={{
minHeight: 'auto',
minWidth: 'auto',
paddingRight: 2,
paddingLeft: 2,
paddingTop: 0,
paddingBottom: 0,
}}
/>
<EuiButtonIcon
iconSize="s"
iconType="magnifyWithMinus"
onClick={() => onAddFilter(field, bucket.value, '-')}
aria-label={removeLabel}
data-test-subj={`minus-${field.name}-${bucket.value}`}
style={{
minHeight: 'auto',
minWidth: 'auto',
paddingTop: 0,
paddingBottom: 0,
paddingRight: 2,
paddingLeft: 2,
}}
/>
</div>
</EuiFlexItem>
)}
</EuiFlexGroup>
<StringFieldProgressBar percent={bucket.percent} count={bucket.count} />
</>
);
}

View file

@ -0,0 +1,98 @@
/*
* 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 { EuiLink, EuiSpacer, EuiIconTip, EuiText } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { DiscoverFieldBucket } from './discover_field_bucket';
import { getWarnings } from './lib/get_warnings';
import { Bucket, FieldDetails } from './types';
import { IndexPatternField, IndexPattern } from '../../../../../../../../plugins/data/public';
interface DiscoverFieldDetailsProps {
field: IndexPatternField;
indexPattern: IndexPattern;
details: FieldDetails;
onAddFilter: (field: IndexPatternField | string, value: string, type: '+' | '-') => void;
}
export function DiscoverFieldDetails({
field,
indexPattern,
details,
onAddFilter,
}: DiscoverFieldDetailsProps) {
const warnings = getWarnings(field);
return (
<div className="dscFieldDetails">
{!details.error && (
<EuiText size="xs">
<FormattedMessage
id="kbn.discover.fieldChooser.detailViews.topValuesInRecordsDescription"
defaultMessage="Top 5 values in"
/>{' '}
{!indexPattern.metaFields.includes(field.name) && !field.scripted ? (
<EuiLink onClick={() => onAddFilter('_exists_', field.name, '+')}>
{details.exists}
</EuiLink>
) : (
<span>{details.exists}</span>
)}{' '}
/ {details.total}{' '}
<FormattedMessage
id="kbn.discover.fieldChooser.detailViews.recordsText"
defaultMessage="records"
/>
</EuiText>
)}
{details.error && <EuiText size="xs">{details.error}</EuiText>}
{!details.error && (
<div style={{ marginTop: '4px' }}>
{details.buckets.map((bucket: Bucket, idx: number) => (
<DiscoverFieldBucket
key={`bucket${idx}`}
bucket={bucket}
field={field}
onAddFilter={onAddFilter}
/>
))}
</div>
)}
{details.visualizeUrl && (
<>
<EuiSpacer size={'s'} />
<EuiLink
href={details.visualizeUrl}
className="kuiButton kuiButton--secondary kuiButton--small kuiVerticalRhythmSmall"
data-test-subj={`fieldVisualize-${field.name}`}
>
<FormattedMessage
id="kbn.discover.fieldChooser.detailViews.visualizeLinkText"
defaultMessage="Visualize"
/>
{warnings.length > 0 && (
<EuiIconTip type="alert" color="warning" content={warnings.join(' ')} />
)}
</EuiLink>
</>
)}
</div>
);
}

View file

@ -34,8 +34,7 @@ describe('DiscoverFieldSearch', () => {
function mountComponent(props?: Props) {
const compProps = props || defaultProps;
const comp = mountWithIntl(<DiscoverFieldSearch {...compProps} />);
return comp;
return mountWithIntl(<DiscoverFieldSearch {...compProps} />);
}
function findButtonGroup(component: ReactWrapper, id: string) {

View file

@ -165,7 +165,7 @@ export function DiscoverFieldSearch({ onChange, value, types }: Props) {
<EuiFacetButton
aria-label={filterBtnAriaLabel}
data-test-subj="toggleFieldFilterButton"
className="dscToggleFieldFilterButton"
className="dscFieldSearch__toggleButton"
icon={<EuiIcon type="filter" />}
isSelected={activeFiltersCount > 0}
quantity={activeFiltersCount}

View file

@ -17,7 +17,7 @@
* under the License.
*/
import React, { useState, useEffect } from 'react';
import { SavedObject } from 'kibana/server';
import { SavedObject } from 'kibana/public';
import { IIndexPattern, IndexPatternAttributes } from 'src/plugins/data/public';
import { I18nProvider } from '@kbn/i18n/react';
@ -65,14 +65,14 @@ export function DiscoverIndexPattern({
}
return (
<div className="indexPattern__container">
<div className="dscIndexPattern__container">
<I18nProvider>
<ChangeIndexPattern
trigger={{
label: selected.title,
title: selected.title,
'data-test-subj': 'indexPattern-switch-link',
className: 'indexPattern__triggerButton',
className: 'dscIndexPattern__triggerButton',
}}
indexPatternId={selected.id}
indexPatternRefs={options}

View file

@ -0,0 +1,131 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import _ from 'lodash';
import { ReactWrapper } from 'enzyme';
// @ts-ignore
import { findTestSubject } from '@elastic/eui/lib/test';
// @ts-ignore
import StubIndexPattern from 'test_utils/stub_index_pattern';
// @ts-ignore
import realHits from 'fixtures/real_hits.js';
// @ts-ignore
import stubbedLogstashFields from 'fixtures/logstash_fields';
import { mountWithIntl } from 'test_utils/enzyme_helpers';
import React from 'react';
import { DiscoverSidebar, DiscoverSidebarProps } from './discover_sidebar';
import { coreMock } from '../../../../../../../../core/public/mocks';
import { IndexPatternAttributes } from '../../../../../../../../plugins/data/common';
import { SavedObject } from '../../../../../../../../core/types';
jest.mock('../../../kibana_services', () => ({
getServices: () => ({
history: {
location: {
search: '',
},
},
capabilities: {
visualize: {
show: true,
},
},
uiSettings: {
get: (key: string) => {
if (key === 'fields:popularLimit') {
return 5;
} else if (key === 'shortDots:enable') {
return false;
}
},
},
}),
}));
function getCompProps() {
const indexPattern = new StubIndexPattern(
'logstash-*',
(cfg: any) => cfg,
'time',
stubbedLogstashFields(),
coreMock.createStart()
);
const hits = _.each(_.cloneDeep(realHits), indexPattern.flattenHit) as Array<
Record<string, unknown>
>;
const indexPatternList = [
{ id: '0', attributes: { title: 'b' } } as SavedObject<IndexPatternAttributes>,
{ id: '1', attributes: { title: 'a' } } as SavedObject<IndexPatternAttributes>,
{ id: '2', attributes: { title: 'c' } } as SavedObject<IndexPatternAttributes>,
];
const fieldCounts: Record<string, number> = {};
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,
setIndexPattern: jest.fn(),
state: {},
};
}
describe('discover sidebar', function() {
let props: DiscoverSidebarProps;
let comp: ReactWrapper<DiscoverSidebarProps>;
beforeAll(() => {
props = getCompProps();
comp = mountWithIntl(<DiscoverSidebar {...props} />);
});
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();
});
});

View file

@ -0,0 +1,326 @@
/*
* 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, { useCallback, useEffect, useState, useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiButtonIcon, EuiTitle } from '@elastic/eui';
import { 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';
import { IndexPatternAttributes } from '../../../../../../../../plugins/data/common';
import { SavedObject } from '../../../../../../../../core/types';
import { groupFields } from './lib/group_fields';
import {
IndexPatternFieldList,
IndexPatternField,
IndexPattern,
} from '../../../../../../../../plugins/data/public';
import { AppState } from '../../angular/discover_state';
import { getDetails } from './lib/get_details';
import { getDefaultFieldFilter, setFieldFilterProp } from './lib/field_filter';
import { getIndexPatternFieldList } from './lib/get_index_pattern_field_list';
import { getServices } from '../../../kibana_services';
export interface DiscoverSidebarProps {
/**
* 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<string, number>;
/**
* hits fetched from ES, displayed in the doc table
*/
hits: Array<Record<string, unknown>>;
/**
* List of available index patterns
*/
indexPatternList: Array<SavedObject<IndexPatternAttributes>>;
/**
* 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;
/**
* Callback function to select another index pattern
*/
setIndexPattern: (id: string) => void;
/**
* Current app state, used for generating a link to visualize
*/
state: AppState;
}
export function DiscoverSidebar({
columns,
fieldCounts,
hits,
indexPatternList,
onAddField,
onAddFilter,
onRemoveField,
selectedIndexPattern,
setIndexPattern,
state,
}: DiscoverSidebarProps) {
const [openFieldMap, setOpenFieldMap] = useState(new Map());
const [showFields, setShowFields] = useState(false);
const [fields, setFields] = useState<IndexPatternFieldList | null>(null);
const [fieldFilterState, setFieldFilterState] = useState(getDefaultFieldFilter());
const services = getServices();
useEffect(() => {
const newFields = getIndexPatternFieldList(selectedIndexPattern, fieldCounts);
setFields(newFields);
}, [selectedIndexPattern, fieldCounts, hits]);
const onShowDetails = useCallback(
(show: boolean, field: IndexPatternField) => {
if (!show) {
setOpenFieldMap(new Map(openFieldMap.set(field.name, false)));
} else {
setOpenFieldMap(new Map(openFieldMap.set(field.name, true)));
selectedIndexPattern.popularizeField(field.name, 1);
}
},
[openFieldMap, selectedIndexPattern]
);
const onChangeFieldSearch = useCallback(
(field: string, value: string | boolean | undefined) => {
const newState = setFieldFilterProp(fieldFilterState, field, value);
setFieldFilterState(newState);
},
[fieldFilterState]
);
const getDetailsByField = useCallback(
(ipField: IndexPatternField) =>
getDetails(ipField, selectedIndexPattern, state, columns, hits, services),
[selectedIndexPattern, state, columns, hits, services]
);
const popularLimit = services.uiSettings.get('fields:popularLimit');
const useShortDots = services.uiSettings.get('shortDots:enable');
const {
selected: selectedFields,
popular: popularFields,
unpopular: unpopularFields,
} = useMemo(() => groupFields(fields, columns, popularLimit, fieldCounts, fieldFilterState), [
fields,
columns,
popularLimit,
fieldCounts,
fieldFilterState,
]);
const fieldTypes = useMemo(() => {
const result = ['any'];
if (Array.isArray(fields)) {
for (const field of fields) {
if (result.indexOf(field.type) === -1) {
result.push(field.type);
}
}
}
return result;
}, [fields]);
if (!selectedIndexPattern || !fields) {
return null;
}
return (
<section
className="sidebar-list"
aria-label={i18n.translate(
'kbn.discover.fieldChooser.filter.indexAndFieldsSectionAriaLabel',
{
defaultMessage: 'Index and fields',
}
)}
>
<DiscoverIndexPattern
selectedIndexPattern={selectedIndexPattern}
setIndexPattern={setIndexPattern}
indexPatternList={sortBy(indexPatternList, o => o.attributes.title)}
/>
<div className="dscSidebar__item">
<form>
<DiscoverFieldSearch
onChange={onChangeFieldSearch}
value={fieldFilterState.name}
types={fieldTypes}
/>
</form>
</div>
<div className="sidebar-list">
{fields.length > 0 && (
<>
<EuiTitle size="xxxs" id="selected_fields">
<h3>
<FormattedMessage
id="kbn.discover.fieldChooser.filter.selectedFieldsTitle"
defaultMessage="Selected fields"
/>
</h3>
</EuiTitle>
<ul
className="dscSidebarList dscFieldList--selected"
aria-labelledby="selected_fields"
data-test-subj={`fieldList-selected`}
>
{selectedFields.map((field: IndexPatternField, idx: number) => {
return (
<li key={`field${idx}`} data-attr-field={field.name} className="dscSidebar__item">
<DiscoverField
field={field}
indexPattern={selectedIndexPattern}
onAddField={onAddField}
onRemoveField={onRemoveField}
onAddFilter={onAddFilter}
onShowDetails={onShowDetails}
getDetails={getDetailsByField}
showDetails={openFieldMap.get(field.name) || false}
selected={true}
useShortDots={useShortDots}
/>
</li>
);
})}
</ul>
<div className="euiFlexGroup euiFlexGroup--gutterMedium">
<EuiTitle size="xxxs" id="available_fields" className="euiFlexItem">
<h3>
<FormattedMessage
id="kbn.discover.fieldChooser.filter.availableFieldsTitle"
defaultMessage="Available fields"
/>
</h3>
</EuiTitle>
<div className="euiFlexItem euiFlexItem--flexGrowZero">
<EuiButtonIcon
className={'visible-xs visible-sm dscFieldChooser__toggle'}
iconType={showFields ? 'arrowDown' : 'arrowRight'}
onClick={() => setShowFields(!showFields)}
aria-label={
showFields
? i18n.translate(
'kbn.discover.fieldChooser.filter.indexAndFieldsSectionHideAriaLabel',
{
defaultMessage: 'Hide fields',
}
)
: i18n.translate(
'kbn.discover.fieldChooser.filter.indexAndFieldsSectionShowAriaLabel',
{
defaultMessage: 'Show fields',
}
)
}
/>
</div>
</div>
</>
)}
{popularFields.length > 0 && (
<div>
<EuiTitle
size="xxxs"
className={`dscFieldListHeader ${!showFields ? 'hidden-sm hidden-xs' : ''}`}
>
<h4 style={{ fontWeight: 'normal' }} id="available_fields_popular">
<FormattedMessage
id="kbn.discover.fieldChooser.filter.popularTitle"
defaultMessage="Popular"
/>
</h4>
</EuiTitle>
<ul
className={`dscFieldList dscFieldList--popular ${
!showFields ? 'hidden-sm hidden-xs' : ''
}`}
aria-labelledby="available_fields available_fields_popular"
data-test-subj={`fieldList-popular`}
>
{popularFields.map((field: IndexPatternField, idx: number) => {
return (
<li key={`field${idx}`} data-attr-field={field.name} className="dscSidebar__item">
<DiscoverField
field={field}
indexPattern={selectedIndexPattern}
onAddField={onAddField}
onRemoveField={onRemoveField}
onAddFilter={onAddFilter}
onShowDetails={onShowDetails}
getDetails={getDetailsByField}
showDetails={openFieldMap.get(field.name) || false}
useShortDots={useShortDots}
/>
</li>
);
})}
</ul>
</div>
)}
<ul
className={`dscFieldList dscFieldList--unpopular ${
!showFields ? 'hidden-sm hidden-xs' : ''
}`}
aria-labelledby="available_fields"
data-test-subj={`fieldList-unpopular`}
>
{unpopularFields.map((field: IndexPatternField, idx: number) => {
return (
<li key={`field${idx}`} data-attr-field={field.name} className="dscSidebar__item">
<DiscoverField
field={field}
indexPattern={selectedIndexPattern}
onAddField={onAddField}
onRemoveField={onRemoveField}
onAddFilter={onAddFilter}
onShowDetails={onShowDetails}
getDetails={getDetailsByField}
showDetails={openFieldMap.get(field.name) || false}
useShortDots={useShortDots}
/>
</li>
);
})}
</ul>
</div>
</section>
);
}

View file

@ -16,25 +16,20 @@
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { wrapInI18nContext } from '../../../kibana_services';
import { DiscoverIndexPattern, DiscoverIndexPatternProps } from './discover_index_pattern';
import { DiscoverSidebar } from './discover_sidebar';
/**
* At initial rendering the angular directive the selectedIndexPattern prop is undefined
* This wrapper catches this, had to be introduced to satisfy eslint
*/
export function DiscoverIndexPatternWrapper(props: DiscoverIndexPatternProps) {
if (!props.selectedIndexPattern || !Array.isArray(props.indexPatternList)) {
return null;
}
return <DiscoverIndexPattern {...props} />;
}
export function createIndexPatternSelectDirective(reactDirective: any) {
return reactDirective(wrapInI18nContext(DiscoverIndexPatternWrapper), [
export function createDiscoverSidebarDirective(reactDirective: any) {
return reactDirective(wrapInI18nContext(DiscoverSidebar), [
['columns', { watchDepth: 'reference' }],
['fieldCounts', { watchDepth: 'reference' }],
['hits', { watchDepth: 'reference' }],
['indexPatternList', { watchDepth: 'reference' }],
['onAddField', { watchDepth: 'reference' }],
['onAddFilter', { watchDepth: 'reference' }],
['onRemoveField', { watchDepth: 'reference' }],
['selectedIndexPattern', { watchDepth: 'reference' }],
['setIndexPattern', { watchDepth: 'reference' }],
['state', { watchDepth: 'reference' }],
]);
}

View file

@ -16,13 +16,6 @@
* specific language governing permissions and limitations
* under the License.
*/
import { wrapInI18nContext } from '../../../kibana_services';
import { DiscoverFieldSearch } from './discover_field_search';
export function createFieldSearchDirective(reactDirective: any) {
return reactDirective(wrapInI18nContext(DiscoverFieldSearch), [
['onChange', { watchDepth: 'reference' }],
['value', { watchDepth: 'value' }],
['types', { watchDepth: 'value' }],
]);
}
export { DiscoverSidebar } from './discover_sidebar';
export { createDiscoverSidebarDirective } from './discover_sidebar_directive';

View file

@ -18,25 +18,29 @@
*/
import _ from 'lodash';
import { pluginInstance } from 'plugins/kibana/discover/legacy';
import ngMock from 'ng_mock';
import { fieldCalculator } from '../../np_ready/components/field_chooser/lib/field_calculator';
import expect from '@kbn/expect';
import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern';
// @ts-ignore
import realHits from 'fixtures/real_hits.js';
// @ts-ignore
import StubIndexPattern from 'test_utils/stub_index_pattern';
// @ts-ignore
import stubbedLogstashFields from 'fixtures/logstash_fields';
import { coreMock } from '../../../../../../../../../core/public/mocks';
import { IndexPattern } from '../../../../../../../../../plugins/data/public';
// @ts-ignore
import { fieldCalculator } from './field_calculator';
// Load the kibana app dependencies.
let indexPattern;
let indexPattern: IndexPattern;
describe('fieldCalculator', function() {
beforeEach(() => pluginInstance.initializeInnerAngular());
beforeEach(ngMock.module('app/discover'));
beforeEach(
ngMock.inject(function(Private) {
indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider);
})
);
beforeEach(function() {
indexPattern = new StubIndexPattern(
'logstash-*',
(cfg: any) => cfg,
'time',
stubbedLogstashFields(),
coreMock.createStart()
);
});
it('should have a _countMissing that counts nulls & undefineds in an array', function() {
const values = [
['foo', 'bar'],
@ -52,13 +56,13 @@ describe('fieldCalculator', function() {
'foo',
undefined,
];
expect(fieldCalculator._countMissing(values)).to.be(5);
expect(fieldCalculator._countMissing(values)).toBe(5);
});
describe('_groupValues', function() {
let groups;
let params;
let values;
let groups: Record<string, any>;
let params: any;
let values: any;
beforeEach(function() {
values = [
['foo', 'bar'],
@ -79,36 +83,36 @@ describe('fieldCalculator', function() {
});
it('should have a _groupValues that counts values', function() {
expect(groups).to.be.an(Object);
expect(groups).toBeInstanceOf(Object);
});
it('should throw an error if any value is a plain object', function() {
expect(function() {
fieldCalculator._groupValues([{}, true, false], params);
}).to.throwError();
}).toThrowError();
});
it('should handle values with dots in them', function() {
values = ['0', '0.........', '0.......,.....'];
params = {};
groups = fieldCalculator._groupValues(values, params);
expect(groups[values[0]].count).to.be(1);
expect(groups[values[1]].count).to.be(1);
expect(groups[values[2]].count).to.be(1);
expect(groups[values[0]].count).toBe(1);
expect(groups[values[1]].count).toBe(1);
expect(groups[values[2]].count).toBe(1);
});
it('should have a a key for value in the array when not grouping array terms', function() {
expect(_.keys(groups).length).to.be(3);
expect(groups.foo).to.be.a(Object);
expect(groups.bar).to.be.a(Object);
expect(groups.baz).to.be.a(Object);
expect(_.keys(groups).length).toBe(3);
expect(groups.foo).toBeInstanceOf(Object);
expect(groups.bar).toBeInstanceOf(Object);
expect(groups.baz).toBeInstanceOf(Object);
});
it('should count array terms independently', function() {
expect(groups['foo,bar']).to.be(undefined);
expect(groups.foo.count).to.be(5);
expect(groups.bar.count).to.be(3);
expect(groups.baz.count).to.be(1);
expect(groups['foo,bar']).toBe(undefined);
expect(groups.foo.count).toBe(5);
expect(groups.bar.count).toBe(3);
expect(groups.baz.count).toBe(1);
});
describe('grouped array terms', function() {
@ -118,27 +122,27 @@ describe('fieldCalculator', function() {
});
it('should group array terms when passed params.grouped', function() {
expect(_.keys(groups).length).to.be(4);
expect(groups['foo,bar']).to.be.a(Object);
expect(_.keys(groups).length).toBe(4);
expect(groups['foo,bar']).toBeInstanceOf(Object);
});
it('should contain the original array as the value', function() {
expect(groups['foo,bar'].value).to.eql(['foo', 'bar']);
expect(groups['foo,bar'].value).toEqual(['foo', 'bar']);
});
it('should count the pairs separately from the values they contain', function() {
expect(groups['foo,bar'].count).to.be(2);
expect(groups.foo.count).to.be(3);
expect(groups.bar.count).to.be(1);
expect(groups['foo,bar'].count).toBe(2);
expect(groups.foo.count).toBe(3);
expect(groups.bar.count).toBe(1);
});
});
});
describe('getFieldValues', function() {
let hits;
let hits: any;
beforeEach(function() {
hits = _.each(require('fixtures/real_hits.js'), indexPattern.flattenHit);
hits = _.each(_.cloneDeep(realHits), indexPattern.flattenHit);
});
it('Should return an array of values for _source fields', function() {
@ -146,32 +150,32 @@ describe('fieldCalculator', function() {
hits,
indexPattern.fields.getByName('extension')
);
expect(extensions).to.be.an(Array);
expect(extensions).toBeInstanceOf(Array);
expect(
_.filter(extensions, function(v) {
return v === 'html';
}).length
).to.be(8);
expect(_.uniq(_.clone(extensions)).sort()).to.eql(['gif', 'html', 'php', 'png']);
).toBe(8);
expect(_.uniq(_.clone(extensions)).sort()).toEqual(['gif', 'html', 'php', 'png']);
});
it('Should return an array of values for core meta fields', function() {
const types = fieldCalculator.getFieldValues(hits, indexPattern.fields.getByName('_type'));
expect(types).to.be.an(Array);
expect(types).toBeInstanceOf(Array);
expect(
_.filter(types, function(v) {
return v === 'apache';
}).length
).to.be(18);
expect(_.uniq(_.clone(types)).sort()).to.eql(['apache', 'nginx']);
).toBe(18);
expect(_.uniq(_.clone(types)).sort()).toEqual(['apache', 'nginx']);
});
});
describe('getFieldValueCounts', function() {
let params;
let params: { hits: any; field: any; count: number };
beforeEach(function() {
params = {
hits: require('fixtures/real_hits.js'),
hits: _.cloneDeep(realHits),
field: indexPattern.fields.getByName('extension'),
count: 3,
};
@ -179,36 +183,36 @@ describe('fieldCalculator', function() {
it('counts the top 3 values', function() {
const extensions = fieldCalculator.getFieldValueCounts(params);
expect(extensions).to.be.an(Object);
expect(extensions.buckets).to.be.an(Array);
expect(extensions.buckets.length).to.be(3);
expect(_.pluck(extensions.buckets, 'value')).to.eql(['html', 'php', 'gif']);
expect(extensions.error).to.be(undefined);
expect(extensions).toBeInstanceOf(Object);
expect(extensions.buckets).toBeInstanceOf(Array);
expect(extensions.buckets.length).toBe(3);
expect(_.pluck(extensions.buckets, 'value')).toEqual(['html', 'php', 'gif']);
expect(extensions.error).toBe(undefined);
});
it('fails to analyze geo and attachment types', function() {
params.field = indexPattern.fields.getByName('point');
expect(fieldCalculator.getFieldValueCounts(params).error).to.not.be(undefined);
expect(fieldCalculator.getFieldValueCounts(params).error).not.toBe(undefined);
params.field = indexPattern.fields.getByName('area');
expect(fieldCalculator.getFieldValueCounts(params).error).to.not.be(undefined);
expect(fieldCalculator.getFieldValueCounts(params).error).not.toBe(undefined);
params.field = indexPattern.fields.getByName('request_body');
expect(fieldCalculator.getFieldValueCounts(params).error).to.not.be(undefined);
expect(fieldCalculator.getFieldValueCounts(params).error).not.toBe(undefined);
});
it('fails to analyze fields that are in the mapping, but not the hits', function() {
params.field = indexPattern.fields.getByName('ip');
expect(fieldCalculator.getFieldValueCounts(params).error).to.not.be(undefined);
expect(fieldCalculator.getFieldValueCounts(params).error).not.toBe(undefined);
});
it('counts the total hits', function() {
expect(fieldCalculator.getFieldValueCounts(params).total).to.be(params.hits.length);
expect(fieldCalculator.getFieldValueCounts(params).total).toBe(params.hits.length);
});
it('counts the hits the field exists in', function() {
params.field = indexPattern.fields.getByName('phpmemory');
expect(fieldCalculator.getFieldValueCounts(params).exists).to.be(5);
expect(fieldCalculator.getFieldValueCounts(params).exists).toBe(5);
});
});
});

View file

@ -0,0 +1,96 @@
/*
* 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 { getDefaultFieldFilter, setFieldFilterProp, isFieldFiltered } from './field_filter';
import { IndexPatternField } from '../../../../../../../../../plugins/data/public';
describe('field_filter', function() {
it('getDefaultFieldFilter should return default filter state', function() {
expect(getDefaultFieldFilter()).toMatchInlineSnapshot(`
Object {
"aggregatable": null,
"missing": true,
"name": "",
"searchable": null,
"type": "any",
}
`);
});
it('setFieldFilterProp should return allow filter changes', function() {
const state = getDefaultFieldFilter();
const targetState = {
aggregatable: true,
missing: true,
name: 'test',
searchable: true,
type: 'string',
};
const actualState = Object.entries(targetState).reduce((acc, kv) => {
return setFieldFilterProp(acc, kv[0], kv[1]);
}, state);
expect(actualState).toMatchInlineSnapshot(`
Object {
"aggregatable": true,
"missing": true,
"name": "test",
"searchable": true,
"type": "string",
}
`);
});
it('filters a given list', () => {
const defaultState = getDefaultFieldFilter();
const fieldList = [
{
name: 'bytes',
type: 'number',
esTypes: ['long'],
count: 10,
scripted: false,
searchable: false,
aggregatable: false,
},
{
name: 'extension',
type: 'string',
esTypes: ['text'],
count: 10,
scripted: true,
searchable: true,
aggregatable: true,
},
] as IndexPatternField[];
[
{ filter: {}, result: ['bytes', 'extension'] },
{ filter: { name: 'by' }, result: ['bytes'] },
{ filter: { aggregatable: true }, result: ['extension'] },
{ filter: { aggregatable: true, searchable: false }, result: [] },
{ filter: { type: 'string' }, result: ['extension'] },
].forEach(test => {
const filtered = fieldList
.filter(field =>
isFieldFiltered(field, { ...defaultState, ...test.filter }, { bytes: 1, extension: 1 })
)
.map(field => field.name);
expect(filtered).toEqual(test.result);
});
});
});

View file

@ -0,0 +1,78 @@
/*
* 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 { IndexPatternField } from '../../../../../../../../../plugins/data/public';
export interface FieldFilterState {
missing: boolean;
type: string;
name: string;
aggregatable: null | boolean;
searchable: null | boolean;
}
export function getDefaultFieldFilter(): FieldFilterState {
return {
missing: true,
type: 'any',
name: '',
aggregatable: null,
searchable: null,
};
}
export function setFieldFilterProp(
state: FieldFilterState,
name: string,
value: string | boolean | null | undefined
): FieldFilterState {
const newState = { ...state };
if (name === 'missing') {
newState.missing = Boolean(value);
} else if (name === 'aggregatable') {
newState.aggregatable = typeof value !== 'boolean' ? null : value;
} else if (name === 'searchable') {
newState.searchable = typeof value !== 'boolean' ? null : value;
} else if (name === 'name') {
newState.name = String(value);
} else if (name === 'type') {
newState.type = String(value);
}
return newState;
}
export function isFieldFiltered(
field: IndexPatternField,
filterState: FieldFilterState,
fieldCounts: Record<string, number>
): boolean {
const matchFilter = filterState.type === 'any' || field.type === filterState.type;
const isAggregatable =
filterState.aggregatable === null || field.aggregatable === filterState.aggregatable;
const isSearchable =
filterState.searchable === null || field.searchable === filterState.searchable;
const scriptedOrMissing =
!filterState.missing ||
field.type === '_source' ||
field.scripted ||
fieldCounts[field.name] > 0;
const matchName = !filterState.name || field.name.indexOf(filterState.name) !== -1;
return matchFilter && isAggregatable && isSearchable && scriptedOrMissing && matchName;
}

View file

@ -0,0 +1,52 @@
/*
* 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 { getVisualizeUrl, isFieldVisualizable } from './visualize_url_utils';
import { AppState } from '../../../angular/discover_state';
// @ts-ignore
import { fieldCalculator } from './field_calculator';
import { IndexPatternField, IndexPattern } from '../../../../../../../../../plugins/data/public';
import { DiscoverServices } from '../../../../build_services';
export function getDetails(
field: IndexPatternField,
indexPattern: IndexPattern,
state: AppState,
columns: string[],
hits: Array<Record<string, unknown>>,
services: DiscoverServices
) {
const details = {
visualizeUrl:
services.capabilities.visualize.show && isFieldVisualizable(field, services.visualizations)
? getVisualizeUrl(field, indexPattern, state, columns, services)
: null,
...fieldCalculator.getFieldValueCounts({
hits,
field,
count: 5,
grouped: false,
}),
};
if (details.buckets) {
for (const bucket of details.buckets) {
bucket.display = field.format.convert(bucket.value);
}
}
return details;
}

View file

@ -0,0 +1,73 @@
/*
* 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 { i18n } from '@kbn/i18n';
export function getFieldTypeName(type: string) {
switch (type) {
case 'boolean':
return i18n.translate('kbn.discover.fieldNameIcons.booleanAriaLabel', {
defaultMessage: 'Boolean field',
});
case 'conflict':
return i18n.translate('kbn.discover.fieldNameIcons.conflictFieldAriaLabel', {
defaultMessage: 'Conflicting field',
});
case 'date':
return i18n.translate('kbn.discover.fieldNameIcons.dateFieldAriaLabel', {
defaultMessage: 'Date field',
});
case 'geo_point':
return i18n.translate('kbn.discover.fieldNameIcons.geoPointFieldAriaLabel', {
defaultMessage: 'Geo point field',
});
case 'geo_shape':
return i18n.translate('kbn.discover.fieldNameIcons.geoShapeFieldAriaLabel', {
defaultMessage: 'Geo shape field',
});
case 'ip':
return i18n.translate('kbn.discover.fieldNameIcons.ipAddressFieldAriaLabel', {
defaultMessage: 'IP address field',
});
case 'murmur3':
return i18n.translate('kbn.discover.fieldNameIcons.murmur3FieldAriaLabel', {
defaultMessage: 'Murmur3 field',
});
case 'number':
return i18n.translate('kbn.discover.fieldNameIcons.numberFieldAriaLabel', {
defaultMessage: 'Number field',
});
case 'source':
// Note that this type is currently not provided, type for _source is undefined
return i18n.translate('kbn.discover.fieldNameIcons.sourceFieldAriaLabel', {
defaultMessage: 'Source field',
});
case 'string':
return i18n.translate('kbn.discover.fieldNameIcons.stringFieldAriaLabel', {
defaultMessage: 'String field',
});
case 'nested':
return i18n.translate('kbn.discover.fieldNameIcons.nestedFieldAriaLabel', {
defaultMessage: 'Nested field',
});
default:
return i18n.translate('kbn.discover.fieldNameIcons.unknownFieldAriaLabel', {
defaultMessage: 'Unknown field',
});
}
}

View file

@ -0,0 +1,44 @@
/*
* 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 { difference, map } from 'lodash';
import {
IndexPatternFieldList,
IndexPattern,
IndexPatternField,
} from '../../../../../../../../../plugins/data/public';
export function getIndexPatternFieldList(
indexPattern: IndexPattern,
fieldCounts: Record<string, number>
): IndexPatternFieldList {
if (!indexPattern || !fieldCounts) return new IndexPatternFieldList(indexPattern, []);
const fieldSpecs = indexPattern.fields.slice(0);
const fieldNamesInDocs = Object.keys(fieldCounts);
const fieldNamesInIndexPattern = map(indexPattern.fields, 'name');
difference(fieldNamesInDocs, fieldNamesInIndexPattern).forEach(unknownFieldName => {
fieldSpecs.push({
name: String(unknownFieldName),
type: 'unknown',
} as IndexPatternField);
});
return new IndexPatternFieldList(indexPattern, fieldSpecs);
}

View file

@ -16,20 +16,28 @@
* specific language governing permissions and limitations
* under the License.
*/
import { FieldName } from '../../../../../../../../plugins/discover/public';
import { getServices, wrapInI18nContext } from '../../../kibana_services';
import { i18n } from '@kbn/i18n';
import { IndexPatternField } from '../../../../../../../../../plugins/data/public';
export function FieldNameDirectiveProvider(reactDirective) {
return reactDirective(
wrapInI18nContext(FieldName),
[
['field', { watchDepth: 'collection' }],
['fieldName', { watchDepth: 'reference' }],
['fieldType', { watchDepth: 'reference' }],
],
{ restrict: 'AE' },
{
useShortDots: getServices().uiSettings.get('shortDots:enable'),
}
);
export function getWarnings(field: IndexPatternField) {
let warnings = [];
if (field.scripted) {
warnings.push(
i18n.translate(
'kbn.discover.fieldChooser.discoverField.scriptedFieldsTakeLongExecuteDescription',
{
defaultMessage: 'Scripted fields can take a long time to execute.',
}
)
);
}
if (warnings.length > 1) {
warnings = warnings.map(function(warning, i) {
return (i > 0 ? '\n' : '') + (i + 1) + ' - ' + warning;
});
}
return warnings;
}

View file

@ -0,0 +1,114 @@
/*
* 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 { groupFields } from './group_fields';
import { getDefaultFieldFilter } from './field_filter';
describe('group_fields', function() {
it('should group fields in selected, popular, unpopular group', function() {
const fields = [
{
name: 'category',
type: 'string',
esTypes: ['text'],
count: 1,
scripted: false,
searchable: true,
aggregatable: true,
readFromDocValues: true,
},
{
name: 'currency',
type: 'string',
esTypes: ['keyword'],
count: 0,
scripted: false,
searchable: true,
aggregatable: true,
readFromDocValues: true,
},
{
name: 'customer_birth_date',
type: 'date',
esTypes: ['date'],
count: 0,
scripted: false,
searchable: true,
aggregatable: true,
readFromDocValues: true,
},
];
const fieldCounts = {
category: 1,
currency: 1,
customer_birth_date: 1,
};
const fieldFilterState = getDefaultFieldFilter();
const actual = groupFields(fields as any, ['currency'], 5, fieldCounts, fieldFilterState);
expect(actual).toMatchInlineSnapshot(`
Object {
"popular": Array [
Object {
"aggregatable": true,
"count": 1,
"esTypes": Array [
"text",
],
"name": "category",
"readFromDocValues": true,
"scripted": false,
"searchable": true,
"type": "string",
},
],
"selected": Array [
Object {
"aggregatable": true,
"count": 0,
"esTypes": Array [
"keyword",
],
"name": "currency",
"readFromDocValues": true,
"scripted": false,
"searchable": true,
"type": "string",
},
],
"unpopular": Array [
Object {
"aggregatable": true,
"count": 0,
"esTypes": Array [
"date",
],
"name": "customer_birth_date",
"readFromDocValues": true,
"scripted": false,
"searchable": true,
"type": "date",
},
],
}
`);
});
});

View file

@ -0,0 +1,78 @@
/*
* 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 {
IndexPatternFieldList,
IndexPatternField,
} from '../../../../../../../../../plugins/data/public';
import { FieldFilterState, isFieldFiltered } from './field_filter';
interface GroupedFields {
selected: IndexPatternField[];
popular: IndexPatternField[];
unpopular: IndexPatternField[];
}
/**
* group the fields into selected, popular and unpopular, filter by fieldFilterState
*/
export function groupFields(
fields: IndexPatternFieldList | null,
columns: string[],
popularLimit: number,
fieldCounts: Record<string, number>,
fieldFilterState: FieldFilterState
): GroupedFields {
const result: GroupedFields = {
selected: [],
popular: [],
unpopular: [],
};
if (!Array.isArray(fields) || !Array.isArray(columns) || typeof fieldCounts !== 'object') {
return result;
}
const popular = fields
.filter(field => !columns.includes(field.name) && field.count)
.sort((a: IndexPatternField, b: IndexPatternField) => (b.count || 0) - (a.count || 0))
.map(field => field.name)
.slice(0, popularLimit);
const compareFn = (a: IndexPatternField, b: IndexPatternField) => {
if (!a.displayName) {
return 0;
}
return a.displayName.localeCompare(b.displayName || '');
};
const fieldsSorted = fields.sort(compareFn);
for (const field of fieldsSorted) {
if (!isFieldFiltered(field, fieldFilterState, fieldCounts)) {
continue;
}
if (columns.includes(field.name)) {
result.selected.push(field);
} else if (popular.includes(field.name) && field.type !== '_source') {
result.popular.push(field);
} else if (field.type !== '_source') {
result.unpopular.push(field);
}
}
return result;
}

View file

@ -0,0 +1,188 @@
/*
* 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 uuid from 'uuid/v4';
import rison from 'rison-node';
import { parse, stringify } from 'query-string';
import {
IFieldType,
IIndexPattern,
IndexPatternField,
KBN_FIELD_TYPES,
} from '../../../../../../../../../plugins/data/public';
import { AppState } from '../../../angular/discover_state';
import { DiscoverServices } from '../../../../build_services';
import {
VisualizationsStart,
VisTypeAlias,
} from '../../../../../../../../../plugins/visualizations/public';
function getMapsAppBaseUrl(visualizations: VisualizationsStart) {
const mapsAppVisAlias = visualizations.getAliases().find(({ name }) => {
return name === 'maps';
});
return mapsAppVisAlias ? mapsAppVisAlias.aliasUrl : null;
}
export function isMapsAppRegistered(visualizations: VisualizationsStart) {
return visualizations.getAliases().some(({ name }: VisTypeAlias) => {
return name === 'maps';
});
}
export function isFieldVisualizable(field: IFieldType, visualizations: VisualizationsStart) {
if (field.name === '_id') {
// Else you'd get a 'Fielddata access on the _id field is disallowed' error on ES side.
return false;
}
if (
(field.type === KBN_FIELD_TYPES.GEO_POINT || field.type === KBN_FIELD_TYPES.GEO_SHAPE) &&
isMapsAppRegistered(visualizations)
) {
return true;
}
return field.visualizable;
}
export function getMapsAppUrl(
field: IFieldType,
indexPattern: IIndexPattern,
appState: AppState,
columns: string[],
services: DiscoverServices
) {
const mapAppParams = new URLSearchParams();
// Copy global state
const locationSplit = window.location.href.split('discover?');
if (locationSplit.length > 1) {
const discoverParams = new URLSearchParams(locationSplit[1]);
const globalStateUrlValue = discoverParams.get('_g');
if (globalStateUrlValue) {
mapAppParams.set('_g', globalStateUrlValue);
}
}
// Copy filters and query in app state
const mapsAppState: any = {
filters: appState.filters || [],
};
if (appState.query) {
mapsAppState.query = appState.query;
}
// @ts-ignore
mapAppParams.set('_a', rison.encode(mapsAppState));
// create initial layer descriptor
const hasColumns = columns && columns.length && columns[0] !== '_source';
const supportsClustering = field.aggregatable;
mapAppParams.set(
'initialLayers',
// @ts-ignore
rison.encode_array([
{
id: uuid(),
label: indexPattern.title,
sourceDescriptor: {
id: uuid(),
type: 'ES_SEARCH',
geoField: field.name,
tooltipProperties: hasColumns ? columns : [],
indexPatternId: indexPattern.id,
scalingType: supportsClustering ? 'CLUSTERS' : 'LIMIT',
},
visible: true,
type: supportsClustering ? 'BLENDED_VECTOR' : 'VECTOR',
},
])
);
return services.addBasePath(
`${getMapsAppBaseUrl(services.visualizations)}?${mapAppParams.toString()}`
);
}
export function getVisualizeUrl(
field: IndexPatternField,
indexPattern: IIndexPattern,
state: AppState,
columns: string[],
services: DiscoverServices
) {
const aggsTermSize = services.uiSettings.get('discover:aggs:terms:size');
const urlParams = parse(services.history.location.search) as Record<string, string>;
if (
(field.type === KBN_FIELD_TYPES.GEO_POINT || field.type === KBN_FIELD_TYPES.GEO_SHAPE) &&
isMapsAppRegistered(services.visualizations)
) {
return getMapsAppUrl(field, indexPattern, state, columns, services);
}
let agg;
const isGeoPoint = field.type === KBN_FIELD_TYPES.GEO_POINT;
const type = isGeoPoint ? 'tile_map' : 'histogram';
// If we're visualizing a date field, and our index is time based (and thus has a time filter),
// then run a date histogram
if (field.type === 'date' && indexPattern.timeFieldName === field.name) {
agg = {
type: 'date_histogram',
schema: 'segment',
params: {
field: field.name,
interval: 'auto',
},
};
} else if (isGeoPoint) {
agg = {
type: 'geohash_grid',
schema: 'segment',
params: {
field: field.name,
precision: 3,
},
};
} else {
agg = {
type: 'terms',
schema: 'segment',
params: {
field: field.name,
size: parseInt(aggsTermSize, 10),
orderBy: '2',
},
};
}
const linkUrlParams = {
...urlParams,
...{
indexPattern: state.index!,
type,
_a: rison.encode({
filters: state.filters || [],
query: state.query,
vis: {
type,
aggs: [{ schema: 'metric', type: 'count', id: '2' }, agg],
},
} as any),
},
};
return `#/visualize/create?${stringify(linkUrlParams)}`;
}

View file

@ -18,14 +18,13 @@
*/
import React from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiProgress, EuiText, EuiToolTip } from '@elastic/eui';
import { wrapInI18nContext } from '../../../kibana_services';
interface Props {
percent: number;
count: number;
}
function StringFieldProgressBar(props: Props) {
export function StringFieldProgressBar(props: Props) {
return (
<EuiToolTip
anchorClassName="dscProgressBarTooltip__anchor"
@ -33,13 +32,13 @@ function StringFieldProgressBar(props: Props) {
delay="regular"
position="right"
>
<EuiFlexGroup alignItems="center">
<EuiFlexGroup alignItems="center" responsive={false}>
<EuiFlexItem>
<EuiProgress
value={props.percent}
max={100}
color="secondary"
aria-labelledby="CanvasAssetManagerLabel"
aria-hidden={true}
size="l"
/>
</EuiFlexItem>
@ -50,7 +49,3 @@ function StringFieldProgressBar(props: Props) {
</EuiToolTip>
);
}
export function createStringFieldProgressBarDirective(reactDirective: any) {
return reactDirective(wrapInI18nContext(StringFieldProgressBar));
}

View file

@ -21,3 +21,18 @@ export interface IndexPatternRef {
id: string;
title: string;
}
export interface FieldDetails {
error: string;
exists: number;
total: boolean;
buckets: Bucket[];
visualizeUrl: string;
}
export interface Bucket {
display: string;
value: string;
percent: number;
count: number;
}

View file

@ -16,6 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import angular from 'angular';
import _ from 'lodash';
import * as Rx from 'rxjs';
import { Subscription } from 'rxjs';
@ -23,7 +24,7 @@ import { i18n } from '@kbn/i18n';
import {
UiActionsStart,
APPLY_FILTER_TRIGGER,
} from '../../../../../../..//plugins/ui_actions/public';
} from '../../../../../../../plugins/ui_actions/public';
import { RequestAdapter, Adapters } from '../../../../../../../plugins/inspector/public';
import {
esFilters,
@ -41,7 +42,6 @@ import { ISearchEmbeddable, SearchInput, SearchOutput } from './types';
import { SortOrder } from '../angular/doc_table/components/table_header/helpers';
import { getSortForSearchSource } from '../angular/doc_table/lib/get_sort_for_search_source';
import {
angular,
getRequestInspectorStats,
getResponseInspectorStats,
getServices,

View file

@ -7,7 +7,6 @@
@import './navbar';
@import './config';
@import './pagination';
@import './sidebar';
@import './spinner';
@import './table';
@import './truncate';

View file

@ -1,127 +0,0 @@
// ONLY USED IN DISCOVER
.sidebar-container {
padding-left: 0 !important;
padding-right: 0 !important;
background-color: $euiColorLightestShade;
border-right-color: transparent;
border-bottom-color: transparent;
.sidebar-well {
background-color: lightOrDarkTheme(tint($euiColorPrimary, 90%), $euiColorLightShade);
}
.sidebar-list {
.sidebar-controls {
border-radius: $euiBorderRadius;
margin-right: -13px;
margin-top: $euiSizeXS / 2;
.navbar-btn-link {
padding-left: $euiSizeS;
padding-right: $euiSizeS;
}
.sidebar-controls-error {
cursor: default;
}
}
ul {
list-style: none;
margin-bottom: 0;
}
.sidebar-item {
border-top-color: transparent;
font-size: $euiFontSizeXS;
border-top: solid 1px transparent;
border-bottom: solid 1px transparent;
line-height: normal;
label {
@include __legacyLabelStyles__bad;
margin-bottom: $euiSizeXS;
display: block;
}
&.active {
background-color: shade($euiColorLightestShade, 10%);
color: $euiColorDarkestShade;
border-color: $euiColorLightShade;
}
}
.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;
&.full-title {
white-space: normal;
}
}
.sidebar-item-text {
background: $euiColorEmptyShade;
}
}
.sidebar-list-header {
.sidebar-list-header-heading {
color: $euiColorDarkestShade;
border: 1px solid transparent;
}
.sidebar-list-header-label {
padding-left: $euiSizeS;
font-size: $euiFontSizeXS;
line-height: $euiLineHeight;
font-weight: $euiFontWeightBold;
color: $euiColorDarkShade;
border-bottom: 1px solid $euiColorLightShade;
}
}
.index-pattern {
font-weight: $euiFontWeightBold;
padding: $euiSizeXS $euiSizeS;
display: flex;
justify-content: space-between;
background-color: shadeOrTint($euiColorPrimary, 60%, 60%);
color: $euiColorEmptyShade;
line-height: $euiSizeL;
.index-pattern-label {
font-size: $euiFontSizeS;
font-weight: $euiFontWeightBold;
margin: 0;
}
> * {
flex: 0 1 auto;
align-self: center;
}
}
}
.indexPattern__container {
display: flex;
align-items: center;
height: $euiSize * 3;
margin-top: -$euiSizeS;
}
.indexPattern__triggerButton {
@include euiTitle('xs');
line-height: $euiSizeXXL;
}

View file

@ -2,7 +2,7 @@
exports[`FieldName renders a geo field, useShortDots is set to true 1`] = `
<div
class="euiFlexGroup euiFlexGroup--gutterSmall euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow dscFieldName dscFieldName--noResults"
class="euiFlexGroup euiFlexGroup--gutterSmall euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow"
>
<div
class="euiFlexItem euiFlexItem--flexGrowZero"
@ -33,7 +33,7 @@ exports[`FieldName renders a geo field, useShortDots is set to true 1`] = `
exports[`FieldName renders a number field by providing a field record, useShortDots is set to false 1`] = `
<div
class="euiFlexGroup euiFlexGroup--gutterSmall euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow dscFieldName"
class="euiFlexGroup euiFlexGroup--gutterSmall euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow"
>
<div
class="euiFlexItem euiFlexItem--flexGrowZero"
@ -64,7 +64,7 @@ exports[`FieldName renders a number field by providing a field record, useShortD
exports[`FieldName renders a string field by providing fieldType and fieldName 1`] = `
<div
class="euiFlexGroup euiFlexGroup--gutterSmall euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow dscFieldName"
class="euiFlexGroup euiFlexGroup--gutterSmall euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow"
>
<div
class="euiFlexItem euiFlexItem--flexGrowZero"

View file

@ -30,23 +30,13 @@ test('FieldName renders a string field by providing fieldType and fieldName', ()
});
test('FieldName renders a number field by providing a field record, useShortDots is set to false', () => {
const field = {
type: 'number',
name: 'test.test.test',
rowCount: 100,
scripted: false,
};
const component = render(<FieldName field={field} />);
const component = render(<FieldName fieldName={'test.test.test'} fieldType={'number'} />);
expect(component).toMatchSnapshot();
});
test('FieldName renders a geo field, useShortDots is set to true', () => {
const field = {
type: 'geo_point',
name: 'test.test.test',
rowCount: 0,
scripted: false,
};
const component = render(<FieldName field={field} useShortDots={true} />);
const component = render(
<FieldName fieldName={'test.test.test'} fieldType={'geo_point'} useShortDots={true} />
);
expect(component).toMatchSnapshot();
});

View file

@ -17,51 +17,36 @@
* under the License.
*/
import React from 'react';
import classNames from 'classnames';
import { EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui';
import { FieldIcon, FieldIconProps } from '../../../../kibana_react/public';
import { shortenDottedString } from '../../helpers';
import { getFieldTypeName } from './field_type_name';
// property field is provided at discover's field chooser
// properties fieldType and fieldName are provided in kbn_doc_view
// this should be changed when both components are deangularized
interface Props {
field?: {
type: string;
name: string;
rowCount?: number;
scripted?: boolean;
};
fieldName?: string;
fieldType?: string;
fieldName: string;
fieldType: string;
useShortDots?: boolean;
fieldIconProps?: Omit<FieldIconProps, 'type'>;
scripted?: boolean;
}
export function FieldName({ field, fieldName, fieldType, useShortDots, fieldIconProps }: Props) {
const type = field ? String(field.type) : String(fieldType);
const typeName = getFieldTypeName(type);
const name = field ? String(field.name) : String(fieldName);
const displayName = useShortDots ? shortenDottedString(name) : name;
const noResults = field ? !field.rowCount && !field.scripted : false;
const className = classNames('dscFieldName', {
'dscFieldName--noResults': noResults,
});
export function FieldName({
fieldName,
fieldType,
useShortDots,
fieldIconProps,
scripted = false,
}: Props) {
const typeName = getFieldTypeName(fieldType);
const displayName = useShortDots ? shortenDottedString(fieldName) : fieldName;
return (
<EuiFlexGroup className={className} alignItems="center" gutterSize="s" responsive={false}>
<EuiFlexGroup alignItems="center" gutterSize="s" responsive={false}>
<EuiFlexItem grow={false}>
<FieldIcon
type={type}
label={typeName}
scripted={field ? field.scripted : false}
{...fieldIconProps}
/>
<FieldIcon type={fieldType} label={typeName} scripted={scripted} {...fieldIconProps} />
</EuiFlexItem>
<EuiFlexItem className="eui-textTruncate">
<EuiToolTip

View file

@ -119,7 +119,7 @@ export function DocViewTable({
key={field}
field={field}
fieldMapping={mapping(field)}
fieldType={fieldType}
fieldType={String(fieldType)}
displayUnderscoreWarning={displayUnderscoreWarning}
displayNoMappingWarning={displayNoMappingWarning}
isCollapsed={isCollapsed}

View file

@ -31,7 +31,7 @@ import { FieldName } from '../field_name/field_name';
export interface Props {
field: string;
fieldMapping?: FieldMapping;
fieldType?: string;
fieldType: string;
displayNoMappingWarning: boolean;
displayUnderscoreWarning: boolean;
isCollapsible: boolean;
@ -88,10 +88,10 @@ export function DocViewTableRow({
)}
<td className="kbnDocViewer__field">
<FieldName
field={fieldMapping}
fieldName={field}
fieldType={fieldType}
fieldIconProps={{ fill: 'none', color: 'gray' }}
scripted={Boolean(fieldMapping?.scripted)}
/>
</td>
<td>

View file

@ -63,7 +63,7 @@ export default function({ getService, getPageObjects }: FtrProviderContext) {
await a11y.testAppSnapshot();
});
it.skip('Click on new to clear the search', async () => {
it('Click on new to clear the search', async () => {
await PageObjects.discover.clickNewSearchButton();
await a11y.testAppSnapshot();
});
@ -121,7 +121,7 @@ export default function({ getService, getPageObjects }: FtrProviderContext) {
await a11y.testAppSnapshot();
});
it.skip('Add more fields from sidebar', async () => {
it('Add more fields from sidebar', async () => {
for (const [columnName, value] of TEST_FILTER_COLUMN_NAMES) {
await PageObjects.discover.clickFieldListItem(columnName);
await PageObjects.discover.clickFieldListPlusFilter(columnName, value);

View file

@ -388,7 +388,7 @@ export default function({ getService, getPageObjects }) {
await log.debug('filter by "Sep 17, 2015 @ 23:00" in the expanded scripted field list');
await PageObjects.discover.clickFieldListPlusFilter(
scriptedPainlessFieldName2,
'2015-09-17 23:00'
'1442531297065'
);
await PageObjects.header.waitUntilLoadingHasFinished();

View file

@ -112,6 +112,7 @@ export function DiscoverPageProvider({ getService, getPageObjects }: FtrProvider
public async clickNewSearchButton() {
await testSubjects.click('discoverNewButton');
await header.waitUntilLoadingHasFinished();
}
public async clickSaveSearchButton() {
@ -207,7 +208,7 @@ export function DiscoverPageProvider({ getService, getPageObjects }: FtrProvider
public async getAllFieldNames() {
const sidebar = await testSubjects.find('discover-sidebar');
const $ = await sidebar.parseDomContent();
return $('.sidebar-item[attr-field]')
return $('.dscSidebar__item[attr-field]')
.toArray()
.map(field =>
$(field)
@ -249,13 +250,17 @@ export function DiscoverPageProvider({ getService, getPageObjects }: FtrProvider
}
public async expectMissingFieldListItemVisualize(field: string) {
await testSubjects.missingOrFail(`fieldVisualize-${field}`, { allowHidden: true });
await testSubjects.missingOrFail(`fieldVisualize-${field}`);
}
public async clickFieldListPlusFilter(field: string, value: string) {
// this method requires the field details to be open from clickFieldListItem()
const plusFilterTestSubj = `plus-${field}-${value}`;
if (!(await testSubjects.exists(plusFilterTestSubj))) {
// field has to be open
await this.clickFieldListItem(field);
}
// testSubjects.find doesn't handle spaces in the data-test-subj value
await testSubjects.click(`plus-${field}-${value}`);
await testSubjects.click(plusFilterTestSubj);
await header.waitUntilLoadingHasFinished();
}

View file

@ -34,7 +34,7 @@ filter-bar,
/* hide unusable controls */
discover-app .dscTimechart,
discover-app .sidebar-container,
discover-app .dscSidebar__container,
discover-app .kbnCollapsibleSidebar__collapseButton,
discover-app navbar[name=discover-search],
discover-app .discover-table-footer {

View file

@ -33,7 +33,7 @@ filter-bar,
/* hide unusable controls */
discover-app .dscTimechart,
discover-app .sidebar-container,
discover-app .dscSidebar__container,
discover-app .kbnCollapsibleSidebar__collapseButton,
discover-app navbar[name="discover-search"],
discover-app .discover-table-footer {

View file

@ -1060,16 +1060,9 @@
"kbn.discover.fetchError.managmentLinkText": "管理 > インデックスパターン",
"kbn.discover.fetchError.scriptedFieldsText": "「スクリプトフィールド」",
"kbn.discover.fieldChooser.detailViews.emptyStringText": "空の文字列",
"kbn.discover.fieldChooser.detailViews.filterOutValueButtonAriaLabel": "この値を除外",
"kbn.discover.fieldChooser.detailViews.filterValueButtonAriaLabel": "この値でフィルターを適用",
"kbn.discover.fieldChooser.detailViews.recordsText": "記録",
"kbn.discover.fieldChooser.detailViews.topValuesInRecordsDescription": "次の記録のトップ 5 の値",
"kbn.discover.fieldChooser.detailViews.visualizeLinkText": "可視化",
"kbn.discover.fieldChooser.detailViews.warningsText": "{warningsLength, plural, one {# 警告} other {# 警告}}",
"kbn.discover.fieldChooser.discoverField.addButtonLabel": "追加",
"kbn.discover.fieldChooser.discoverField.bucketAriaLabel": "値: {value}",
"kbn.discover.fieldChooser.discoverField.emptyStringText": "空の文字列",
"kbn.discover.fieldChooser.discoverField.removeButtonLabel": "削除",
"kbn.discover.fieldChooser.discoverField.scriptedFieldsTakeLongExecuteDescription": "スクリプトフィールドは実行に時間がかかる場合があります。",
"kbn.discover.fieldChooser.fieldCalculator.analysisIsNotAvailableForGeoFieldsErrorMessage": "ジオフィールドは分析できません。",
"kbn.discover.fieldChooser.fieldCalculator.analysisIsNotAvailableForObjectFieldsErrorMessage": "オブジェクトフィールドは分析できません。",

View file

@ -1060,16 +1060,9 @@
"kbn.discover.fetchError.managmentLinkText": "管理 > 索引模式",
"kbn.discover.fetchError.scriptedFieldsText": "“脚本字段”",
"kbn.discover.fieldChooser.detailViews.emptyStringText": "空字符串",
"kbn.discover.fieldChooser.detailViews.filterOutValueButtonAriaLabel": "筛除此值",
"kbn.discover.fieldChooser.detailViews.filterValueButtonAriaLabel": "筛留此值",
"kbn.discover.fieldChooser.detailViews.recordsText": "个记录",
"kbn.discover.fieldChooser.detailViews.topValuesInRecordsDescription": "排名前 5 位的值,范围:",
"kbn.discover.fieldChooser.detailViews.visualizeLinkText": "可视化",
"kbn.discover.fieldChooser.detailViews.warningsText": "{warningsLength, plural, one {# 个警告} other {# 个警告}}",
"kbn.discover.fieldChooser.discoverField.addButtonLabel": "添加",
"kbn.discover.fieldChooser.discoverField.bucketAriaLabel": "值:{value}",
"kbn.discover.fieldChooser.discoverField.emptyStringText": "空字符串",
"kbn.discover.fieldChooser.discoverField.removeButtonLabel": "移除",
"kbn.discover.fieldChooser.discoverField.scriptedFieldsTakeLongExecuteDescription": "脚本字段执行时间会很长。",
"kbn.discover.fieldChooser.fieldCalculator.analysisIsNotAvailableForGeoFieldsErrorMessage": "分析不适用于地理字段。",
"kbn.discover.fieldChooser.fieldCalculator.analysisIsNotAvailableForObjectFieldsErrorMessage": "分析不适用于对象字段。",