mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
* 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:
parent
1f1d5195aa
commit
886979bca5
64 changed files with 2005 additions and 1774 deletions
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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(`
|
||||
|
|
|
@ -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]);
|
||||
}
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
@import 'fetch_error/index';
|
||||
@import 'field_chooser/index';
|
||||
@import 'sidebar/index';
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
@import 'field_chooser';
|
|
@ -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>
|
|
@ -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();
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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>
|
|
@ -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()}`);
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
@import './_sidebar';
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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} />
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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) {
|
|
@ -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}
|
|
@ -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}
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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' }],
|
||||
]);
|
||||
}
|
|
@ -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';
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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',
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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",
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
}
|
|
@ -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)}`;
|
||||
}
|
|
@ -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));
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
@import './navbar';
|
||||
@import './config';
|
||||
@import './pagination';
|
||||
@import './sidebar';
|
||||
@import './spinner';
|
||||
@import './table';
|
||||
@import './truncate';
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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"
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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": "オブジェクトフィールドは分析できません。",
|
||||
|
|
|
@ -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": "分析不适用于对象字段。",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue