mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Discover] Use fields API to retrieve fields (#83891)
* Add search source to example plugin. * Add uiSetting for fields API. * Update SearchSource to support fields API. * [PoC] reading from the fields API in Discover * Add N fields as a default column * Make fields column non-removeable * Do not add 'fields' to state * Remove fields from app state and read from source when needed * Remove fields column if a new column is added * Add search source to example plugin. * Add uiSetting for fields API. * Update SearchSource to support fields API. * Improve error handling in search examples plugin. * Add unit tests for legacy behavior. * Remove uiSettings feature flag; add fieldsFromSource config. * Rewrite flatten() based on final API design. * Update example app based on final API design. * Update maps app to use legacy fieldsFromSource. * Update Discover to use legacy fieldsFromSource. * Rename source filters to field filters. * Address feedback. * Update generated docs. * Update maps functional test. * Formatting fields column similar to _source * Moving logic for using search API to updating search source * Fix small merge error * Move useSource switch to Discover section of advanced settings * Do not use fields and source at the same time * Remove unmapped fields switch * Add basic support for grouping multifields * Remove output.txt * Fix some merge leftovers * Fix some merge leftovers * Fix merge errors * Fix typescript errors and update nested fields logic * Add a unit test * Fixing field formats * Fix multifield selection logic * Request all fields from source * Fix eslint * Fix default columns when switching between _source and fields * More unit tests * Update API changes * Add unit test for discover field details footer * Remove unused file * Remove fields formatting from index pattern * Remove unnecessary check * Addressing design comments * Fixing fields column display and renaming it to Document * Adding more unit tests * Adding a missing check for useNewFieldsAPI; minor fixes * Fixing typescript error * Remove unnecessary console statement * Add missing prop * Fixing import order * Adding functional test to test fields API * [Functional test] Clean up in after * Fixing context app * Addressing PR comments * Updating failed snapshot * Addressing PR comments * Fixing i18n translations, updating type * Addressing PR comments * Updating a functional test * Add a separate functional test for fields API * Read fields from source in a functional test * Skip buggy test * Use default behavior in functional tests * Fixing remaining failing tests * Fixing date-nanos test * Updating FLS test * Fixing yet another functional test * Skipping non-relevant tests * Fixing more tests * Update stub import in test * Fix import * Fix invalid import Co-authored-by: Luke Elmers <luke.elmers@elastic.co> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
31a481a9dd
commit
9b22789c3c
66 changed files with 1920 additions and 186 deletions
|
@ -29,3 +29,4 @@ export const CONTEXT_STEP_SETTING = 'context:step';
|
|||
export const CONTEXT_TIE_BREAKER_FIELDS_SETTING = 'context:tieBreakerFields';
|
||||
export const DOC_TABLE_LEGACY = 'doc_table:legacy';
|
||||
export const MODIFY_COLUMNS_ON_SWITCH = 'discover:modifyColumnsOnSwitch';
|
||||
export const SEARCH_FIELDS_FROM_SOURCE = 'discover:searchFieldsFromSource';
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
*/
|
||||
|
||||
// @ts-expect-error
|
||||
import stubbedLogstashFields from './logstash_fields';
|
||||
import stubbedLogstashFields from '../../../../fixtures/logstash_fields';
|
||||
|
||||
const mockLogstashFields = stubbedLogstashFields();
|
||||
|
|
@ -47,6 +47,7 @@ export function createSearchSourceStub(hits, timeField) {
|
|||
|
||||
searchSourceStub.setParent = sinon.spy(() => searchSourceStub);
|
||||
searchSourceStub.setField = sinon.spy(() => searchSourceStub);
|
||||
searchSourceStub.removeField = sinon.spy(() => searchSourceStub);
|
||||
|
||||
searchSourceStub.getField = sinon.spy((key) => {
|
||||
const previousSetCall = searchSourceStub.setField.withArgs(key).lastCall;
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
import _ from 'lodash';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export function fetchAnchorProvider(indexPatterns, searchSource) {
|
||||
export function fetchAnchorProvider(indexPatterns, searchSource, useNewFieldsApi = false) {
|
||||
return async function fetchAnchor(indexPatternId, anchorId, sort) {
|
||||
const indexPattern = await indexPatterns.get(indexPatternId);
|
||||
searchSource
|
||||
|
@ -41,7 +41,10 @@ export function fetchAnchorProvider(indexPatterns, searchSource) {
|
|||
language: 'lucene',
|
||||
})
|
||||
.setField('sort', sort);
|
||||
|
||||
if (useNewFieldsApi) {
|
||||
searchSource.removeField('fieldsFromSource');
|
||||
searchSource.setField('fields', ['*']);
|
||||
}
|
||||
const response = await searchSource.fetch();
|
||||
|
||||
if (_.get(response, ['hits', 'total'], 0) < 1) {
|
||||
|
|
|
@ -144,4 +144,29 @@ describe('context app', function () {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('useNewFields API', () => {
|
||||
let fetchAnchor;
|
||||
let searchSourceStub;
|
||||
|
||||
beforeEach(() => {
|
||||
searchSourceStub = createSearchSourceStub([{ _id: 'hit1' }]);
|
||||
fetchAnchor = fetchAnchorProvider(createIndexPatternsStub(), searchSourceStub, true);
|
||||
});
|
||||
|
||||
it('should request fields if useNewFieldsApi set', function () {
|
||||
searchSourceStub._stubHits = [{ property1: 'value1' }, { property2: 'value2' }];
|
||||
|
||||
return fetchAnchor('INDEX_PATTERN_ID', 'id', [
|
||||
{ '@timestamp': 'desc' },
|
||||
{ _doc: 'desc' },
|
||||
]).then(() => {
|
||||
const setFieldsSpy = searchSourceStub.setField.withArgs('fields');
|
||||
const removeFieldsSpy = searchSourceStub.removeField.withArgs('fieldsFromSource');
|
||||
expect(setFieldsSpy.calledOnce).toBe(true);
|
||||
expect(removeFieldsSpy.calledOnce).toBe(true);
|
||||
expect(setFieldsSpy.firstCall.args[1]).toEqual(['*']);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -227,4 +227,81 @@ describe('context app', function () {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('function fetchPredecessors with useNewFieldsApi set', function () {
|
||||
let fetchPredecessors;
|
||||
let mockSearchSource;
|
||||
|
||||
beforeEach(() => {
|
||||
mockSearchSource = createContextSearchSourceStub([], '@timestamp', MS_PER_DAY * 8);
|
||||
|
||||
setServices({
|
||||
data: {
|
||||
search: {
|
||||
searchSource: {
|
||||
create: jest.fn().mockImplementation(() => mockSearchSource),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
fetchPredecessors = (
|
||||
indexPatternId,
|
||||
timeField,
|
||||
sortDir,
|
||||
timeValIso,
|
||||
timeValNr,
|
||||
tieBreakerField,
|
||||
tieBreakerValue,
|
||||
size
|
||||
) => {
|
||||
const anchor = {
|
||||
_source: {
|
||||
[timeField]: timeValIso,
|
||||
},
|
||||
sort: [timeValNr, tieBreakerValue],
|
||||
};
|
||||
|
||||
return fetchContextProvider(createIndexPatternsStub(), true).fetchSurroundingDocs(
|
||||
'predecessors',
|
||||
indexPatternId,
|
||||
anchor,
|
||||
timeField,
|
||||
tieBreakerField,
|
||||
sortDir,
|
||||
size,
|
||||
[]
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
it('should perform exactly one query when enough hits are returned', function () {
|
||||
mockSearchSource._stubHits = [
|
||||
mockSearchSource._createStubHit(MS_PER_DAY * 3000 + 2),
|
||||
mockSearchSource._createStubHit(MS_PER_DAY * 3000 + 1),
|
||||
mockSearchSource._createStubHit(MS_PER_DAY * 3000),
|
||||
mockSearchSource._createStubHit(MS_PER_DAY * 2000),
|
||||
mockSearchSource._createStubHit(MS_PER_DAY * 1000),
|
||||
];
|
||||
|
||||
return fetchPredecessors(
|
||||
'INDEX_PATTERN_ID',
|
||||
'@timestamp',
|
||||
'desc',
|
||||
ANCHOR_TIMESTAMP_3000,
|
||||
MS_PER_DAY * 3000,
|
||||
'_doc',
|
||||
0,
|
||||
3,
|
||||
[]
|
||||
).then((hits) => {
|
||||
const setFieldsSpy = mockSearchSource.setField.withArgs('fields');
|
||||
const removeFieldsSpy = mockSearchSource.removeField.withArgs('fieldsFromSource');
|
||||
expect(mockSearchSource.fetch.calledOnce).toBe(true);
|
||||
expect(removeFieldsSpy.calledOnce).toBe(true);
|
||||
expect(setFieldsSpy.calledOnce).toBe(true);
|
||||
expect(hits).toEqual(mockSearchSource._stubHits.slice(0, 3));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -231,4 +231,81 @@ describe('context app', function () {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('function fetchSuccessors with useNewFieldsApi set', function () {
|
||||
let fetchSuccessors;
|
||||
let mockSearchSource;
|
||||
|
||||
beforeEach(() => {
|
||||
mockSearchSource = createContextSearchSourceStub([], '@timestamp');
|
||||
|
||||
setServices({
|
||||
data: {
|
||||
search: {
|
||||
searchSource: {
|
||||
create: jest.fn().mockImplementation(() => mockSearchSource),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
fetchSuccessors = (
|
||||
indexPatternId,
|
||||
timeField,
|
||||
sortDir,
|
||||
timeValIso,
|
||||
timeValNr,
|
||||
tieBreakerField,
|
||||
tieBreakerValue,
|
||||
size
|
||||
) => {
|
||||
const anchor = {
|
||||
_source: {
|
||||
[timeField]: timeValIso,
|
||||
},
|
||||
sort: [timeValNr, tieBreakerValue],
|
||||
};
|
||||
|
||||
return fetchContextProvider(createIndexPatternsStub(), true).fetchSurroundingDocs(
|
||||
'successors',
|
||||
indexPatternId,
|
||||
anchor,
|
||||
timeField,
|
||||
tieBreakerField,
|
||||
sortDir,
|
||||
size,
|
||||
[]
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
it('should perform exactly one query when enough hits are returned', function () {
|
||||
mockSearchSource._stubHits = [
|
||||
mockSearchSource._createStubHit(MS_PER_DAY * 5000),
|
||||
mockSearchSource._createStubHit(MS_PER_DAY * 4000),
|
||||
mockSearchSource._createStubHit(MS_PER_DAY * 3000),
|
||||
mockSearchSource._createStubHit(MS_PER_DAY * 3000 - 1),
|
||||
mockSearchSource._createStubHit(MS_PER_DAY * 3000 - 2),
|
||||
];
|
||||
|
||||
return fetchSuccessors(
|
||||
'INDEX_PATTERN_ID',
|
||||
'@timestamp',
|
||||
'desc',
|
||||
ANCHOR_TIMESTAMP_3000,
|
||||
MS_PER_DAY * 3000,
|
||||
'_doc',
|
||||
0,
|
||||
3,
|
||||
[]
|
||||
).then((hits) => {
|
||||
expect(mockSearchSource.fetch.calledOnce).toBe(true);
|
||||
expect(hits).toEqual(mockSearchSource._stubHits.slice(-3));
|
||||
const setFieldsSpy = mockSearchSource.setField.withArgs('fields');
|
||||
const removeFieldsSpy = mockSearchSource.removeField.withArgs('fieldsFromSource');
|
||||
expect(removeFieldsSpy.calledOnce).toBe(true);
|
||||
expect(setFieldsSpy.calledOnce).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -40,7 +40,7 @@ const DAY_MILLIS = 24 * 60 * 60 * 1000;
|
|||
// look from 1 day up to 10000 days into the past and future
|
||||
const LOOKUP_OFFSETS = [0, 1, 7, 30, 365, 10000].map((days) => days * DAY_MILLIS);
|
||||
|
||||
function fetchContextProvider(indexPatterns: IndexPatternsContract) {
|
||||
function fetchContextProvider(indexPatterns: IndexPatternsContract, useNewFieldsApi?: boolean) {
|
||||
return {
|
||||
fetchSurroundingDocs,
|
||||
};
|
||||
|
@ -89,7 +89,14 @@ function fetchContextProvider(indexPatterns: IndexPatternsContract) {
|
|||
break;
|
||||
}
|
||||
|
||||
const searchAfter = getEsQuerySearchAfter(type, documents, timeField, anchor, nanos);
|
||||
const searchAfter = getEsQuerySearchAfter(
|
||||
type,
|
||||
documents,
|
||||
timeField,
|
||||
anchor,
|
||||
nanos,
|
||||
useNewFieldsApi
|
||||
);
|
||||
|
||||
const sort = getEsQuerySort(timeField, tieBreakerField, sortDirToApply);
|
||||
|
||||
|
@ -116,6 +123,10 @@ function fetchContextProvider(indexPatterns: IndexPatternsContract) {
|
|||
const { data } = getServices();
|
||||
|
||||
const searchSource = await data.search.searchSource.create();
|
||||
if (useNewFieldsApi) {
|
||||
searchSource.removeField('fieldsFromSource');
|
||||
searchSource.setField('fields', ['*']);
|
||||
}
|
||||
return searchSource
|
||||
.setParent(undefined)
|
||||
.setField('index', indexPattern)
|
||||
|
|
|
@ -31,16 +31,30 @@ export function getEsQuerySearchAfter(
|
|||
documents: EsHitRecordList,
|
||||
timeFieldName: string,
|
||||
anchor: EsHitRecord,
|
||||
nanoSeconds: string
|
||||
nanoSeconds: string,
|
||||
useNewFieldsApi?: boolean
|
||||
): EsQuerySearchAfter {
|
||||
if (documents.length) {
|
||||
// already surrounding docs -> first or last record is used
|
||||
const afterTimeRecIdx = type === 'successors' && documents.length ? documents.length - 1 : 0;
|
||||
const afterTimeDoc = documents[afterTimeRecIdx];
|
||||
const afterTimeValue = nanoSeconds ? afterTimeDoc._source[timeFieldName] : afterTimeDoc.sort[0];
|
||||
let afterTimeValue = afterTimeDoc.sort[0];
|
||||
if (nanoSeconds) {
|
||||
afterTimeValue = useNewFieldsApi
|
||||
? afterTimeDoc.fields[timeFieldName][0]
|
||||
: afterTimeDoc._source[timeFieldName];
|
||||
}
|
||||
return [afterTimeValue, afterTimeDoc.sort[1]];
|
||||
}
|
||||
// if data_nanos adapt timestamp value for sorting, since numeric value was rounded by browser
|
||||
// ES search_after also works when number is provided as string
|
||||
return [nanoSeconds ? anchor._source[timeFieldName] : anchor.sort[0], anchor.sort[1]];
|
||||
const searchAfter = new Array(2) as EsQuerySearchAfter;
|
||||
searchAfter[0] = anchor.sort[0];
|
||||
if (nanoSeconds) {
|
||||
searchAfter[0] = useNewFieldsApi
|
||||
? anchor.fields[timeFieldName][0]
|
||||
: anchor._source[timeFieldName];
|
||||
}
|
||||
searchAfter[1] = anchor.sort[1];
|
||||
return searchAfter;
|
||||
}
|
||||
|
|
|
@ -27,11 +27,17 @@ import { fetchContextProvider } from '../api/context';
|
|||
import { getQueryParameterActions } from '../query_parameters';
|
||||
import { FAILURE_REASONS, LOADING_STATUS } from './index';
|
||||
import { MarkdownSimple } from '../../../../../../kibana_react/public';
|
||||
import { SEARCH_FIELDS_FROM_SOURCE } from '../../../../../common';
|
||||
|
||||
export function QueryActionsProvider(Promise) {
|
||||
const { filterManager, indexPatterns, data } = getServices();
|
||||
const fetchAnchor = fetchAnchorProvider(indexPatterns, data.search.searchSource.createEmpty());
|
||||
const { fetchSurroundingDocs } = fetchContextProvider(indexPatterns);
|
||||
const { filterManager, indexPatterns, data, uiSettings } = getServices();
|
||||
const useNewFieldsApi = !uiSettings.get(SEARCH_FIELDS_FROM_SOURCE);
|
||||
const fetchAnchor = fetchAnchorProvider(
|
||||
indexPatterns,
|
||||
data.search.searchSource.createEmpty(),
|
||||
useNewFieldsApi
|
||||
);
|
||||
const { fetchSurroundingDocs } = fetchContextProvider(indexPatterns, useNewFieldsApi);
|
||||
const { setPredecessorCount, setQueryParameters, setSuccessorCount } = getQueryParameterActions(
|
||||
filterManager,
|
||||
indexPatterns
|
||||
|
|
|
@ -17,5 +17,6 @@
|
|||
successor-available="contextApp.state.rows.successors.length"
|
||||
successor-status="contextApp.state.loadingStatus.successors.status"
|
||||
on-change-successor-count="contextApp.actions.fetchGivenSuccessorRows"
|
||||
use-new-fields-api="contextApp.state.useNewFieldsApi"
|
||||
top-nav-menu="contextApp.topNavMenu"
|
||||
></context-app-legacy>
|
||||
|
|
|
@ -18,7 +18,11 @@
|
|||
*/
|
||||
|
||||
import _ from 'lodash';
|
||||
import { CONTEXT_STEP_SETTING, CONTEXT_TIE_BREAKER_FIELDS_SETTING } from '../../../common';
|
||||
import {
|
||||
CONTEXT_STEP_SETTING,
|
||||
CONTEXT_TIE_BREAKER_FIELDS_SETTING,
|
||||
SEARCH_FIELDS_FROM_SOURCE,
|
||||
} from '../../../common';
|
||||
import { getAngularModule, getServices } from '../../kibana_services';
|
||||
import contextAppTemplate from './context_app.html';
|
||||
import './context/components/action_bar';
|
||||
|
@ -59,9 +63,11 @@ function ContextAppController($scope, Private) {
|
|||
const { filterManager, indexPatterns, uiSettings, navigation } = getServices();
|
||||
const queryParameterActions = getQueryParameterActions(filterManager, indexPatterns);
|
||||
const queryActions = Private(QueryActionsProvider);
|
||||
const useNewFieldsApi = !uiSettings.get(SEARCH_FIELDS_FROM_SOURCE);
|
||||
this.state = createInitialState(
|
||||
parseInt(uiSettings.get(CONTEXT_STEP_SETTING), 10),
|
||||
getFirstSortableField(this.indexPattern, uiSettings.get(CONTEXT_TIE_BREAKER_FIELDS_SETTING))
|
||||
getFirstSortableField(this.indexPattern, uiSettings.get(CONTEXT_TIE_BREAKER_FIELDS_SETTING)),
|
||||
useNewFieldsApi
|
||||
);
|
||||
this.topNavMenu = navigation.ui.TopNavMenu;
|
||||
|
||||
|
@ -127,7 +133,7 @@ function ContextAppController($scope, Private) {
|
|||
);
|
||||
}
|
||||
|
||||
function createInitialState(defaultStepSize, tieBreakerField) {
|
||||
function createInitialState(defaultStepSize, tieBreakerField, useNewFieldsApi) {
|
||||
return {
|
||||
queryParameters: createInitialQueryParametersState(defaultStepSize, tieBreakerField),
|
||||
rows: {
|
||||
|
@ -137,5 +143,6 @@ function createInitialState(defaultStepSize, tieBreakerField) {
|
|||
successors: [],
|
||||
},
|
||||
loadingStatus: createInitialLoadingStatusState(),
|
||||
useNewFieldsApi,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -64,6 +64,7 @@ import {
|
|||
DEFAULT_COLUMNS_SETTING,
|
||||
MODIFY_COLUMNS_ON_SWITCH,
|
||||
SAMPLE_SIZE_SETTING,
|
||||
SEARCH_FIELDS_FROM_SOURCE,
|
||||
SEARCH_ON_PAGE_LOAD_SETTING,
|
||||
SORT_DEFAULT_ORDER_SETTING,
|
||||
} from '../../../common';
|
||||
|
@ -197,6 +198,8 @@ function discoverController($element, $route, $scope, $timeout, Promise) {
|
|||
$scope.searchSource,
|
||||
toastNotifications
|
||||
);
|
||||
$scope.useNewFieldsApi = !config.get(SEARCH_FIELDS_FROM_SOURCE);
|
||||
|
||||
//used for functional testing
|
||||
$scope.fetchCounter = 0;
|
||||
|
||||
|
@ -308,7 +311,8 @@ function discoverController($element, $route, $scope, $timeout, Promise) {
|
|||
nextIndexPattern,
|
||||
$scope.state.columns,
|
||||
$scope.state.sort,
|
||||
config.get(MODIFY_COLUMNS_ON_SWITCH)
|
||||
config.get(MODIFY_COLUMNS_ON_SWITCH),
|
||||
$scope.useNewFieldsApi
|
||||
);
|
||||
await setAppState(nextAppState);
|
||||
}
|
||||
|
@ -415,19 +419,33 @@ function discoverController($element, $route, $scope, $timeout, Promise) {
|
|||
|
||||
setBreadcrumbsTitle(savedSearch, chrome);
|
||||
|
||||
function removeSourceFromColumns(columns) {
|
||||
return columns.filter((col) => col !== '_source');
|
||||
}
|
||||
|
||||
function getDefaultColumns() {
|
||||
const columns = [...savedSearch.columns];
|
||||
|
||||
if ($scope.useNewFieldsApi) {
|
||||
return removeSourceFromColumns(columns);
|
||||
}
|
||||
if (columns.length > 0) {
|
||||
return columns;
|
||||
}
|
||||
return [...config.get(DEFAULT_COLUMNS_SETTING)];
|
||||
}
|
||||
|
||||
function getStateDefaults() {
|
||||
const query = $scope.searchSource.getField('query') || data.query.queryString.getDefaultQuery();
|
||||
const sort = getSortArray(savedSearch.sort, $scope.indexPattern);
|
||||
const columns = getDefaultColumns();
|
||||
|
||||
const defaultState = {
|
||||
query,
|
||||
sort: !sort.length
|
||||
? getDefaultSort($scope.indexPattern, config.get(SORT_DEFAULT_ORDER_SETTING, 'desc'))
|
||||
: sort,
|
||||
columns:
|
||||
savedSearch.columns.length > 0
|
||||
? savedSearch.columns
|
||||
: config.get(DEFAULT_COLUMNS_SETTING).slice(),
|
||||
columns,
|
||||
index: $scope.indexPattern.id,
|
||||
interval: 'auto',
|
||||
filters: _.cloneDeep($scope.searchSource.getOwnField('filter')),
|
||||
|
@ -739,10 +757,14 @@ function discoverController($element, $route, $scope, $timeout, Promise) {
|
|||
};
|
||||
|
||||
$scope.updateDataSource = () => {
|
||||
updateSearchSource($scope.searchSource, {
|
||||
indexPattern: $scope.indexPattern,
|
||||
const { indexPattern, searchSource, useNewFieldsApi } = $scope;
|
||||
const { columns, sort } = $scope.state;
|
||||
updateSearchSource(searchSource, {
|
||||
indexPattern,
|
||||
services,
|
||||
sort: $scope.state.sort,
|
||||
sort,
|
||||
columns,
|
||||
useNewFieldsApi,
|
||||
});
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
@ -770,20 +792,20 @@ function discoverController($element, $route, $scope, $timeout, Promise) {
|
|||
};
|
||||
|
||||
$scope.addColumn = function addColumn(columnName) {
|
||||
const { indexPattern, useNewFieldsApi } = $scope;
|
||||
if (capabilities.discover.save) {
|
||||
const { indexPattern } = $scope;
|
||||
popularizeField(indexPattern, columnName, indexPatterns);
|
||||
}
|
||||
const columns = columnActions.addColumn($scope.state.columns, columnName);
|
||||
const columns = columnActions.addColumn($scope.state.columns, columnName, useNewFieldsApi);
|
||||
setAppState({ columns });
|
||||
};
|
||||
|
||||
$scope.removeColumn = function removeColumn(columnName) {
|
||||
const { indexPattern, useNewFieldsApi } = $scope;
|
||||
if (capabilities.discover.save) {
|
||||
const { indexPattern } = $scope;
|
||||
popularizeField(indexPattern, columnName, indexPatterns);
|
||||
}
|
||||
const columns = columnActions.removeColumn($scope.state.columns, columnName);
|
||||
const columns = columnActions.removeColumn($scope.state.columns, columnName, useNewFieldsApi);
|
||||
// The state's sort property is an array of [sortByColumn,sortDirection]
|
||||
const sort = $scope.state.sort.length
|
||||
? $scope.state.sort.filter((subArr) => subArr[0] !== columnName)
|
||||
|
|
|
@ -29,6 +29,7 @@
|
|||
top-nav-menu="topNavMenu"
|
||||
update-query="handleRefresh"
|
||||
update-saved-query-id="updateSavedQueryId"
|
||||
use-new-fields-api="useNewFieldsApi"
|
||||
>
|
||||
</discover-legacy>
|
||||
</discover-app>
|
||||
|
|
|
@ -21,28 +21,32 @@
|
|||
* Helper function to provide a fallback to a single _source column if the given array of columns
|
||||
* is empty, and removes _source if there are more than 1 columns given
|
||||
* @param columns
|
||||
* @param useNewFieldsApi should a new fields API be used
|
||||
*/
|
||||
function buildColumns(columns: string[]) {
|
||||
function buildColumns(columns: string[], useNewFieldsApi = false) {
|
||||
if (columns.length > 1 && columns.indexOf('_source') !== -1) {
|
||||
return columns.filter((col) => col !== '_source');
|
||||
} else if (columns.length !== 0) {
|
||||
return columns;
|
||||
}
|
||||
return ['_source'];
|
||||
return useNewFieldsApi ? [] : ['_source'];
|
||||
}
|
||||
|
||||
export function addColumn(columns: string[], columnName: string) {
|
||||
export function addColumn(columns: string[], columnName: string, useNewFieldsApi?: boolean) {
|
||||
if (columns.includes(columnName)) {
|
||||
return columns;
|
||||
}
|
||||
return buildColumns([...columns, columnName]);
|
||||
return buildColumns([...columns, columnName], useNewFieldsApi);
|
||||
}
|
||||
|
||||
export function removeColumn(columns: string[], columnName: string) {
|
||||
export function removeColumn(columns: string[], columnName: string, useNewFieldsApi?: boolean) {
|
||||
if (!columns.includes(columnName)) {
|
||||
return columns;
|
||||
}
|
||||
return buildColumns(columns.filter((col) => col !== columnName));
|
||||
return buildColumns(
|
||||
columns.filter((col) => col !== columnName),
|
||||
useNewFieldsApi
|
||||
);
|
||||
}
|
||||
|
||||
export function moveColumn(columns: string[], columnName: string, newIndex: number) {
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { IndexPattern } from '../../../../../kibana_services';
|
||||
|
||||
export type SortOrder = [string, string];
|
||||
|
@ -62,17 +63,33 @@ export function getDisplayedColumns(
|
|||
if (!Array.isArray(columns) || typeof indexPattern !== 'object' || !indexPattern.getFieldByName) {
|
||||
return [];
|
||||
}
|
||||
const columnProps = columns.map((column, idx) => {
|
||||
const field = indexPattern.getFieldByName(column);
|
||||
return {
|
||||
name: column,
|
||||
displayName: field ? field.displayName : column,
|
||||
isSortable: field && field.sortable ? true : false,
|
||||
isRemoveable: column !== '_source' || columns.length > 1,
|
||||
colLeftIdx: idx - 1 < 0 ? -1 : idx - 1,
|
||||
colRightIdx: idx + 1 >= columns.length ? -1 : idx + 1,
|
||||
};
|
||||
});
|
||||
|
||||
const columnProps =
|
||||
columns.length === 0
|
||||
? [
|
||||
{
|
||||
name: '__document__',
|
||||
displayName: i18n.translate('discover.docTable.tableHeader.documentHeader', {
|
||||
defaultMessage: 'Document',
|
||||
}),
|
||||
isSortable: false,
|
||||
isRemoveable: false,
|
||||
colLeftIdx: -1,
|
||||
colRightIdx: -1,
|
||||
},
|
||||
]
|
||||
: columns.map((column, idx) => {
|
||||
const field = indexPattern.getFieldByName(column);
|
||||
return {
|
||||
name: column,
|
||||
displayName: field?.displayName ?? column,
|
||||
isSortable: !!(field && field.sortable),
|
||||
isRemoveable: column !== '_source' || columns.length > 1,
|
||||
colLeftIdx: idx - 1 < 0 ? -1 : idx - 1,
|
||||
colRightIdx: idx + 1 >= columns.length ? -1 : idx + 1,
|
||||
};
|
||||
});
|
||||
|
||||
return !hideTimeField && indexPattern.timeFieldName
|
||||
? [getTimeColumn(indexPattern.timeFieldName), ...columnProps]
|
||||
: columnProps;
|
||||
|
|
|
@ -27,6 +27,7 @@ import cellTemplateHtml from '../components/table_row/cell.html';
|
|||
import truncateByHeightTemplateHtml from '../components/table_row/truncate_by_height.html';
|
||||
import { getServices } from '../../../../kibana_services';
|
||||
import { getContextUrl } from '../../../helpers/get_context_url';
|
||||
import { formatRow } from '../../helpers';
|
||||
|
||||
const TAGS_WITH_WS = />\s+</g;
|
||||
|
||||
|
@ -58,6 +59,7 @@ export function createTableRowDirective($compile: ng.ICompileService) {
|
|||
row: '=kbnTableRow',
|
||||
onAddColumn: '=?',
|
||||
onRemoveColumn: '=?',
|
||||
useNewFieldsApi: '<',
|
||||
},
|
||||
link: ($scope: LazyScope, $el: JQuery) => {
|
||||
$el.after('<tr data-test-subj="docTableDetailsRow" class="kbnDocTableDetails__row">');
|
||||
|
@ -139,19 +141,33 @@ export function createTableRowDirective($compile: ng.ICompileService) {
|
|||
);
|
||||
}
|
||||
|
||||
$scope.columns.forEach(function (column: any) {
|
||||
const isFilterable = mapping(column) && mapping(column).filterable && $scope.filter;
|
||||
if ($scope.columns.length === 0 && $scope.useNewFieldsApi) {
|
||||
const formatted = formatRow(row, indexPattern);
|
||||
|
||||
newHtmls.push(
|
||||
cellTemplate({
|
||||
timefield: false,
|
||||
sourcefield: column === '_source',
|
||||
formatted: _displayField(row, column, true),
|
||||
filterable: isFilterable,
|
||||
column,
|
||||
sourcefield: true,
|
||||
formatted,
|
||||
filterable: false,
|
||||
column: '__document__',
|
||||
})
|
||||
);
|
||||
});
|
||||
} else {
|
||||
$scope.columns.forEach(function (column: string) {
|
||||
const isFilterable = mapping(column) && mapping(column).filterable && $scope.filter;
|
||||
|
||||
newHtmls.push(
|
||||
cellTemplate({
|
||||
timefield: false,
|
||||
sourcefield: column === '_source',
|
||||
formatted: _displayField(row, column, true),
|
||||
filterable: isFilterable,
|
||||
column,
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
let $cells = $el.children();
|
||||
newHtmls.forEach(function (html, i) {
|
||||
|
|
|
@ -1,9 +1,5 @@
|
|||
.kbnDocTableCell__dataField {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.kbnDocTableCell__toggleDetails {
|
||||
padding: 4px 0 0 0!important;
|
||||
padding: $euiSizeXS 0 0 0!important;
|
||||
}
|
||||
|
||||
.kbnDocTableCell__filter {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<td colspan="{{ columns.length + 2 }}">
|
||||
<td colspan="{{ (columns.length || 1) + 2 }}">
|
||||
<div class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--justifyContentSpaceBetween">
|
||||
<div class="euiFlexItem euiFlexItem--flexGrowZero euiText euiText--small">
|
||||
<div class="euiFlexGroup euiFlexGroup--gutterSmall euiFlexGroup--directionRow">
|
||||
|
|
|
@ -97,6 +97,7 @@ export interface DocTableLegacyProps {
|
|||
onMoveColumn?: (columns: string, newIdx: number) => void;
|
||||
onRemoveColumn?: (column: string) => void;
|
||||
sort?: string[][];
|
||||
useNewFieldsApi?: boolean;
|
||||
}
|
||||
|
||||
export function DocTableLegacy(renderProps: DocTableLegacyProps) {
|
||||
|
@ -118,6 +119,7 @@ export function DocTableLegacy(renderProps: DocTableLegacyProps) {
|
|||
on-move-column="onMoveColumn"
|
||||
on-remove-column="onRemoveColumn"
|
||||
render-complete
|
||||
use-new-fields-api="useNewFieldsApi"
|
||||
sorting="sort"></doc_table>`,
|
||||
},
|
||||
() => getServices().getEmbeddableInjector()
|
||||
|
|
|
@ -46,6 +46,7 @@
|
|||
class="kbnDocTable__row"
|
||||
on-add-column="onAddColumn"
|
||||
on-remove-column="onRemoveColumn"
|
||||
use-new-fields-api="useNewFieldsApi"
|
||||
></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
@ -97,6 +98,7 @@
|
|||
data-test-subj="docTableRow{{ row['$$_isAnchor'] ? ' docTableAnchorRow' : ''}}"
|
||||
on-add-column="onAddColumn"
|
||||
on-remove-column="onRemoveColumn"
|
||||
use-new-fields-api="useNewFieldsApi"
|
||||
></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
|
|
@ -48,6 +48,7 @@ export function createDocTableDirective(pagerFactory: any, $filter: any) {
|
|||
onMoveColumn: '=?',
|
||||
onRemoveColumn: '=?',
|
||||
inspectorAdapters: '=?',
|
||||
useNewFieldsApi: '<',
|
||||
},
|
||||
link: ($scope: LazyScope, $el: JQuery) => {
|
||||
$scope.persist = {
|
||||
|
|
|
@ -18,3 +18,4 @@
|
|||
*/
|
||||
|
||||
export { buildPointSeriesData } from './point_series';
|
||||
export { formatRow } from './row_formatter';
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* 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 { formatRow } from './row_formatter';
|
||||
import { stubbedSavedObjectIndexPattern } from '../../../__mocks__/stubbed_saved_object_index_pattern';
|
||||
import { IndexPattern } from '../../../../../data/common/index_patterns/index_patterns';
|
||||
import { fieldFormatsMock } from '../../../../../data/common/field_formats/mocks';
|
||||
|
||||
describe('Row formatter', () => {
|
||||
const hit = {
|
||||
foo: 'bar',
|
||||
number: 42,
|
||||
hello: '<h1>World</h1>',
|
||||
also: 'with "quotes" or \'single quotes\'',
|
||||
};
|
||||
|
||||
const createIndexPattern = () => {
|
||||
const id = 'my-index';
|
||||
const {
|
||||
type,
|
||||
version,
|
||||
attributes: { timeFieldName, fields, title },
|
||||
} = stubbedSavedObjectIndexPattern(id);
|
||||
|
||||
return new IndexPattern({
|
||||
spec: { id, type, version, timeFieldName, fields, title },
|
||||
fieldFormats: fieldFormatsMock,
|
||||
shortDotsEnable: false,
|
||||
metaFields: [],
|
||||
});
|
||||
};
|
||||
|
||||
const indexPattern = createIndexPattern();
|
||||
|
||||
const formatHitReturnValue = {
|
||||
also: 'with \\"quotes\\" or 'single qoutes'',
|
||||
number: '42',
|
||||
foo: 'bar',
|
||||
hello: '<h1>World</h1>',
|
||||
};
|
||||
const formatHitMock = jest.fn().mockReturnValueOnce(formatHitReturnValue);
|
||||
|
||||
beforeEach(() => {
|
||||
// @ts-ignore
|
||||
indexPattern.formatHit = formatHitMock;
|
||||
});
|
||||
|
||||
it('formats document properly', () => {
|
||||
expect(formatRow(hit, indexPattern).trim()).toBe(
|
||||
'<dl class="source truncate-by-height"><dt>also:</dt><dd>with \\"quotes\\" or 'single qoutes'</dd> <dt>number:</dt><dd>42</dd> <dt>foo:</dt><dd>bar</dd> <dt>hello:</dt><dd><h1>World</h1></dd> </dl>'
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* 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 { template } from 'lodash';
|
||||
import { IndexPattern } from '../../../kibana_services';
|
||||
|
||||
function noWhiteSpace(html: string) {
|
||||
const TAGS_WITH_WS = />\s+</g;
|
||||
return html.replace(TAGS_WITH_WS, '><');
|
||||
}
|
||||
|
||||
const templateHtml = `
|
||||
<dl class="source truncate-by-height">
|
||||
<% defPairs.forEach(function (def) { %>
|
||||
<dt><%- def[0] %>:</dt>
|
||||
<dd><%= def[1] %></dd>
|
||||
<%= ' ' %>
|
||||
<% }); %>
|
||||
</dl>`;
|
||||
export const doTemplate = template(noWhiteSpace(templateHtml));
|
||||
|
||||
export const formatRow = (hit: Record<string, any>, indexPattern: IndexPattern) => {
|
||||
const highlights = hit?.highlight ?? {};
|
||||
const formatted = indexPattern.formatHit(hit);
|
||||
const highlightPairs: Array<[string, unknown]> = [];
|
||||
const sourcePairs: Array<[string, unknown]> = [];
|
||||
Object.entries(formatted).forEach(([key, val]) => {
|
||||
const pairs = highlights[key] ? highlightPairs : sourcePairs;
|
||||
pairs.push([key, val]);
|
||||
});
|
||||
return doTemplate({ defPairs: [...highlightPairs, ...sourcePairs] });
|
||||
};
|
|
@ -48,6 +48,7 @@ export interface ContextAppProps {
|
|||
onChangeSuccessorCount: (count: number) => void;
|
||||
predecessorStatus: string;
|
||||
successorStatus: string;
|
||||
useNewFieldsApi?: boolean;
|
||||
}
|
||||
|
||||
const PREDECESSOR_TYPE = 'predecessors';
|
||||
|
@ -87,7 +88,15 @@ export function ContextAppLegacy(renderProps: ContextAppProps) {
|
|||
};
|
||||
|
||||
const docTableProps = () => {
|
||||
const { hits, filter, sorting, columns, indexPattern, minimumVisibleRows } = renderProps;
|
||||
const {
|
||||
hits,
|
||||
filter,
|
||||
sorting,
|
||||
columns,
|
||||
indexPattern,
|
||||
minimumVisibleRows,
|
||||
useNewFieldsApi,
|
||||
} = renderProps;
|
||||
return {
|
||||
columns,
|
||||
indexPattern,
|
||||
|
@ -95,6 +104,7 @@ export function ContextAppLegacy(renderProps: ContextAppProps) {
|
|||
rows: hits,
|
||||
onFilter: filter,
|
||||
sort: sorting.map((el) => [el]),
|
||||
useNewFieldsApi,
|
||||
} as DocTableLegacyProps;
|
||||
};
|
||||
|
||||
|
|
|
@ -37,6 +37,7 @@ export function createContextAppLegacy(reactDirective: any) {
|
|||
['successorAvailable', { watchDepth: 'reference' }],
|
||||
['successorStatus', { watchDepth: 'reference' }],
|
||||
['onChangeSuccessorCount', { watchDepth: 'reference' }],
|
||||
['useNewFieldsApi', { watchDepth: 'reference' }],
|
||||
['topNavMenu', { watchDepth: 'reference' }],
|
||||
]);
|
||||
}
|
||||
|
|
|
@ -51,5 +51,6 @@ export function createDiscoverLegacyDirective(reactDirective: any) {
|
|||
['topNavMenu', { watchDepth: 'reference' }],
|
||||
['updateQuery', { watchDepth: 'reference' }],
|
||||
['updateSavedQueryId', { watchDepth: 'reference' }],
|
||||
['useNewFieldsApi', { watchDepth: 'reference' }],
|
||||
]);
|
||||
}
|
||||
|
|
|
@ -219,6 +219,7 @@ export interface DiscoverProps {
|
|||
* Function to update the actual savedQuery id
|
||||
*/
|
||||
updateSavedQueryId: (savedQueryId?: string) => void;
|
||||
useNewFieldsApi?: boolean;
|
||||
}
|
||||
|
||||
export const DocTableLegacyMemoized = React.memo((props: DocTableLegacyProps) => (
|
||||
|
@ -257,6 +258,7 @@ export function DiscoverLegacy({
|
|||
topNavMenu,
|
||||
updateQuery,
|
||||
updateSavedQueryId,
|
||||
useNewFieldsApi,
|
||||
}: DiscoverProps) {
|
||||
const scrollableDesktop = useRef<HTMLDivElement>(null);
|
||||
const collapseIcon = useRef<HTMLButtonElement>(null);
|
||||
|
@ -278,6 +280,17 @@ export function DiscoverLegacy({
|
|||
: undefined;
|
||||
const contentCentered = resultState === 'uninitialized';
|
||||
|
||||
const getDisplayColumns = () => {
|
||||
if (!state.columns) {
|
||||
return [];
|
||||
}
|
||||
const columns = [...state.columns];
|
||||
if (useNewFieldsApi) {
|
||||
return columns.filter((column) => column !== '_source');
|
||||
}
|
||||
return columns.length === 0 ? ['_source'] : columns;
|
||||
};
|
||||
|
||||
return (
|
||||
<I18nProvider>
|
||||
<EuiPage className="dscPage" data-fetch-counter={fetchCounter}>
|
||||
|
@ -315,6 +328,7 @@ export function DiscoverLegacy({
|
|||
setIndexPattern={setIndexPattern}
|
||||
isClosed={isSidebarClosed}
|
||||
trackUiMetric={trackUiMetric}
|
||||
useNewFieldsApi={useNewFieldsApi}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiHideFor sizes={['xs', 's']}>
|
||||
|
@ -445,7 +459,7 @@ export function DiscoverLegacy({
|
|||
{rows && rows.length && (
|
||||
<div>
|
||||
<DocTableLegacyMemoized
|
||||
columns={state.columns || []}
|
||||
columns={getDisplayColumns()}
|
||||
indexPattern={indexPattern}
|
||||
minimumVisibleRows={minimumVisibleRows}
|
||||
rows={rows}
|
||||
|
@ -457,6 +471,7 @@ export function DiscoverLegacy({
|
|||
onMoveColumn={onMoveColumn}
|
||||
onRemoveColumn={onRemoveColumn}
|
||||
onSort={onSort}
|
||||
useNewFieldsApi={useNewFieldsApi}
|
||||
/>
|
||||
{rows.length === opts.sampleSize ? (
|
||||
<div
|
||||
|
|
|
@ -0,0 +1,704 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`discover sidebar field details footer renders properly 1`] = `
|
||||
<DiscoverFieldDetailsFooter
|
||||
details={
|
||||
Object {
|
||||
"buckets": Array [],
|
||||
"columns": Array [],
|
||||
"error": "",
|
||||
"exists": 1,
|
||||
"total": 2,
|
||||
}
|
||||
}
|
||||
field={
|
||||
Object {
|
||||
"aggregatable": true,
|
||||
"conflictDescriptions": undefined,
|
||||
"count": 10,
|
||||
"customLabel": undefined,
|
||||
"esTypes": Array [
|
||||
"long",
|
||||
],
|
||||
"lang": undefined,
|
||||
"name": "bytes",
|
||||
"readFromDocValues": true,
|
||||
"script": undefined,
|
||||
"scripted": false,
|
||||
"searchable": true,
|
||||
"subType": undefined,
|
||||
"type": "number",
|
||||
}
|
||||
}
|
||||
indexPattern={
|
||||
StubIndexPattern {
|
||||
"_reindexFields": [Function],
|
||||
"fieldFormatMap": Object {},
|
||||
"fields": FldList [
|
||||
Object {
|
||||
"aggregatable": true,
|
||||
"conflictDescriptions": undefined,
|
||||
"count": 10,
|
||||
"customLabel": undefined,
|
||||
"esTypes": Array [
|
||||
"long",
|
||||
],
|
||||
"lang": undefined,
|
||||
"name": "bytes",
|
||||
"readFromDocValues": true,
|
||||
"script": undefined,
|
||||
"scripted": false,
|
||||
"searchable": true,
|
||||
"subType": undefined,
|
||||
"type": "number",
|
||||
},
|
||||
Object {
|
||||
"aggregatable": true,
|
||||
"conflictDescriptions": undefined,
|
||||
"count": 20,
|
||||
"customLabel": undefined,
|
||||
"esTypes": Array [
|
||||
"boolean",
|
||||
],
|
||||
"lang": undefined,
|
||||
"name": "ssl",
|
||||
"readFromDocValues": true,
|
||||
"script": undefined,
|
||||
"scripted": false,
|
||||
"searchable": true,
|
||||
"subType": undefined,
|
||||
"type": "boolean",
|
||||
},
|
||||
Object {
|
||||
"aggregatable": true,
|
||||
"conflictDescriptions": undefined,
|
||||
"count": 30,
|
||||
"customLabel": undefined,
|
||||
"esTypes": Array [
|
||||
"date",
|
||||
],
|
||||
"lang": undefined,
|
||||
"name": "@timestamp",
|
||||
"readFromDocValues": true,
|
||||
"script": undefined,
|
||||
"scripted": false,
|
||||
"searchable": true,
|
||||
"subType": undefined,
|
||||
"type": "date",
|
||||
},
|
||||
Object {
|
||||
"aggregatable": true,
|
||||
"conflictDescriptions": undefined,
|
||||
"count": 30,
|
||||
"customLabel": undefined,
|
||||
"esTypes": Array [
|
||||
"date",
|
||||
],
|
||||
"lang": undefined,
|
||||
"name": "time",
|
||||
"readFromDocValues": true,
|
||||
"script": undefined,
|
||||
"scripted": false,
|
||||
"searchable": true,
|
||||
"subType": undefined,
|
||||
"type": "date",
|
||||
},
|
||||
Object {
|
||||
"aggregatable": true,
|
||||
"conflictDescriptions": undefined,
|
||||
"count": 0,
|
||||
"customLabel": undefined,
|
||||
"esTypes": Array [
|
||||
"keyword",
|
||||
],
|
||||
"lang": undefined,
|
||||
"name": "@tags",
|
||||
"readFromDocValues": true,
|
||||
"script": undefined,
|
||||
"scripted": false,
|
||||
"searchable": true,
|
||||
"subType": undefined,
|
||||
"type": "string",
|
||||
},
|
||||
Object {
|
||||
"aggregatable": true,
|
||||
"conflictDescriptions": undefined,
|
||||
"count": 0,
|
||||
"customLabel": undefined,
|
||||
"esTypes": Array [
|
||||
"date",
|
||||
],
|
||||
"lang": undefined,
|
||||
"name": "utc_time",
|
||||
"readFromDocValues": true,
|
||||
"script": undefined,
|
||||
"scripted": false,
|
||||
"searchable": true,
|
||||
"subType": undefined,
|
||||
"type": "date",
|
||||
},
|
||||
Object {
|
||||
"aggregatable": true,
|
||||
"conflictDescriptions": undefined,
|
||||
"count": 0,
|
||||
"customLabel": undefined,
|
||||
"esTypes": Array [
|
||||
"integer",
|
||||
],
|
||||
"lang": undefined,
|
||||
"name": "phpmemory",
|
||||
"readFromDocValues": true,
|
||||
"script": undefined,
|
||||
"scripted": false,
|
||||
"searchable": true,
|
||||
"subType": undefined,
|
||||
"type": "number",
|
||||
},
|
||||
Object {
|
||||
"aggregatable": true,
|
||||
"conflictDescriptions": undefined,
|
||||
"count": 0,
|
||||
"customLabel": undefined,
|
||||
"esTypes": Array [
|
||||
"ip",
|
||||
],
|
||||
"lang": undefined,
|
||||
"name": "ip",
|
||||
"readFromDocValues": true,
|
||||
"script": undefined,
|
||||
"scripted": false,
|
||||
"searchable": true,
|
||||
"subType": undefined,
|
||||
"type": "ip",
|
||||
},
|
||||
Object {
|
||||
"aggregatable": true,
|
||||
"conflictDescriptions": undefined,
|
||||
"count": 0,
|
||||
"customLabel": undefined,
|
||||
"esTypes": Array [
|
||||
"attachment",
|
||||
],
|
||||
"lang": undefined,
|
||||
"name": "request_body",
|
||||
"readFromDocValues": true,
|
||||
"script": undefined,
|
||||
"scripted": false,
|
||||
"searchable": true,
|
||||
"subType": undefined,
|
||||
"type": "attachment",
|
||||
},
|
||||
Object {
|
||||
"aggregatable": true,
|
||||
"conflictDescriptions": undefined,
|
||||
"count": 0,
|
||||
"customLabel": undefined,
|
||||
"esTypes": Array [
|
||||
"geo_point",
|
||||
],
|
||||
"lang": undefined,
|
||||
"name": "point",
|
||||
"readFromDocValues": true,
|
||||
"script": undefined,
|
||||
"scripted": false,
|
||||
"searchable": true,
|
||||
"subType": undefined,
|
||||
"type": "geo_point",
|
||||
},
|
||||
Object {
|
||||
"aggregatable": true,
|
||||
"conflictDescriptions": undefined,
|
||||
"count": 0,
|
||||
"customLabel": undefined,
|
||||
"esTypes": Array [
|
||||
"geo_shape",
|
||||
],
|
||||
"lang": undefined,
|
||||
"name": "area",
|
||||
"readFromDocValues": false,
|
||||
"script": undefined,
|
||||
"scripted": false,
|
||||
"searchable": true,
|
||||
"subType": undefined,
|
||||
"type": "geo_shape",
|
||||
},
|
||||
Object {
|
||||
"aggregatable": false,
|
||||
"conflictDescriptions": undefined,
|
||||
"count": 0,
|
||||
"customLabel": undefined,
|
||||
"esTypes": Array [
|
||||
"murmur3",
|
||||
],
|
||||
"lang": undefined,
|
||||
"name": "hashed",
|
||||
"readFromDocValues": false,
|
||||
"script": undefined,
|
||||
"scripted": false,
|
||||
"searchable": true,
|
||||
"subType": undefined,
|
||||
"type": "murmur3",
|
||||
},
|
||||
Object {
|
||||
"aggregatable": true,
|
||||
"conflictDescriptions": undefined,
|
||||
"count": 0,
|
||||
"customLabel": undefined,
|
||||
"esTypes": Array [
|
||||
"geo_point",
|
||||
],
|
||||
"lang": undefined,
|
||||
"name": "geo.coordinates",
|
||||
"readFromDocValues": true,
|
||||
"script": undefined,
|
||||
"scripted": false,
|
||||
"searchable": true,
|
||||
"subType": undefined,
|
||||
"type": "geo_point",
|
||||
},
|
||||
Object {
|
||||
"aggregatable": true,
|
||||
"conflictDescriptions": undefined,
|
||||
"count": 0,
|
||||
"customLabel": undefined,
|
||||
"esTypes": Array [
|
||||
"text",
|
||||
],
|
||||
"lang": undefined,
|
||||
"name": "extension",
|
||||
"readFromDocValues": false,
|
||||
"script": undefined,
|
||||
"scripted": false,
|
||||
"searchable": true,
|
||||
"subType": undefined,
|
||||
"type": "string",
|
||||
},
|
||||
Object {
|
||||
"aggregatable": true,
|
||||
"conflictDescriptions": undefined,
|
||||
"count": 0,
|
||||
"customLabel": undefined,
|
||||
"esTypes": Array [
|
||||
"keyword",
|
||||
],
|
||||
"lang": undefined,
|
||||
"name": "extension.keyword",
|
||||
"readFromDocValues": true,
|
||||
"script": undefined,
|
||||
"scripted": false,
|
||||
"searchable": true,
|
||||
"subType": Object {
|
||||
"multi": Object {
|
||||
"parent": "extension",
|
||||
},
|
||||
},
|
||||
"type": "string",
|
||||
},
|
||||
Object {
|
||||
"aggregatable": true,
|
||||
"conflictDescriptions": undefined,
|
||||
"count": 0,
|
||||
"customLabel": undefined,
|
||||
"esTypes": Array [
|
||||
"text",
|
||||
],
|
||||
"lang": undefined,
|
||||
"name": "machine.os",
|
||||
"readFromDocValues": false,
|
||||
"script": undefined,
|
||||
"scripted": false,
|
||||
"searchable": true,
|
||||
"subType": undefined,
|
||||
"type": "string",
|
||||
},
|
||||
Object {
|
||||
"aggregatable": true,
|
||||
"conflictDescriptions": undefined,
|
||||
"count": 0,
|
||||
"customLabel": undefined,
|
||||
"esTypes": Array [
|
||||
"keyword",
|
||||
],
|
||||
"lang": undefined,
|
||||
"name": "machine.os.raw",
|
||||
"readFromDocValues": true,
|
||||
"script": undefined,
|
||||
"scripted": false,
|
||||
"searchable": true,
|
||||
"subType": Object {
|
||||
"multi": Object {
|
||||
"parent": "machine.os",
|
||||
},
|
||||
},
|
||||
"type": "string",
|
||||
},
|
||||
Object {
|
||||
"aggregatable": true,
|
||||
"conflictDescriptions": undefined,
|
||||
"count": 0,
|
||||
"customLabel": undefined,
|
||||
"esTypes": Array [
|
||||
"keyword",
|
||||
],
|
||||
"lang": undefined,
|
||||
"name": "geo.src",
|
||||
"readFromDocValues": true,
|
||||
"script": undefined,
|
||||
"scripted": false,
|
||||
"searchable": true,
|
||||
"subType": undefined,
|
||||
"type": "string",
|
||||
},
|
||||
Object {
|
||||
"aggregatable": true,
|
||||
"conflictDescriptions": undefined,
|
||||
"count": 0,
|
||||
"customLabel": undefined,
|
||||
"esTypes": Array [
|
||||
"_id",
|
||||
],
|
||||
"lang": undefined,
|
||||
"name": "_id",
|
||||
"readFromDocValues": false,
|
||||
"script": undefined,
|
||||
"scripted": false,
|
||||
"searchable": true,
|
||||
"subType": undefined,
|
||||
"type": "string",
|
||||
},
|
||||
Object {
|
||||
"aggregatable": true,
|
||||
"conflictDescriptions": undefined,
|
||||
"count": 0,
|
||||
"customLabel": undefined,
|
||||
"esTypes": Array [
|
||||
"_type",
|
||||
],
|
||||
"lang": undefined,
|
||||
"name": "_type",
|
||||
"readFromDocValues": false,
|
||||
"script": undefined,
|
||||
"scripted": false,
|
||||
"searchable": true,
|
||||
"subType": undefined,
|
||||
"type": "string",
|
||||
},
|
||||
Object {
|
||||
"aggregatable": true,
|
||||
"conflictDescriptions": undefined,
|
||||
"count": 0,
|
||||
"customLabel": undefined,
|
||||
"esTypes": Array [
|
||||
"_source",
|
||||
],
|
||||
"lang": undefined,
|
||||
"name": "_source",
|
||||
"readFromDocValues": false,
|
||||
"script": undefined,
|
||||
"scripted": false,
|
||||
"searchable": true,
|
||||
"subType": undefined,
|
||||
"type": "_source",
|
||||
},
|
||||
Object {
|
||||
"aggregatable": true,
|
||||
"conflictDescriptions": undefined,
|
||||
"count": 0,
|
||||
"customLabel": undefined,
|
||||
"esTypes": Array [
|
||||
"text",
|
||||
],
|
||||
"lang": undefined,
|
||||
"name": "non-filterable",
|
||||
"readFromDocValues": false,
|
||||
"script": undefined,
|
||||
"scripted": false,
|
||||
"searchable": false,
|
||||
"subType": undefined,
|
||||
"type": "string",
|
||||
},
|
||||
Object {
|
||||
"aggregatable": false,
|
||||
"conflictDescriptions": undefined,
|
||||
"count": 0,
|
||||
"customLabel": undefined,
|
||||
"esTypes": Array [
|
||||
"text",
|
||||
],
|
||||
"lang": undefined,
|
||||
"name": "non-sortable",
|
||||
"readFromDocValues": false,
|
||||
"script": undefined,
|
||||
"scripted": false,
|
||||
"searchable": false,
|
||||
"subType": undefined,
|
||||
"type": "string",
|
||||
},
|
||||
Object {
|
||||
"aggregatable": true,
|
||||
"conflictDescriptions": undefined,
|
||||
"count": 0,
|
||||
"customLabel": undefined,
|
||||
"esTypes": Array [
|
||||
"conflict",
|
||||
],
|
||||
"lang": undefined,
|
||||
"name": "custom_user_field",
|
||||
"readFromDocValues": true,
|
||||
"script": undefined,
|
||||
"scripted": false,
|
||||
"searchable": true,
|
||||
"subType": undefined,
|
||||
"type": "conflict",
|
||||
},
|
||||
Object {
|
||||
"aggregatable": true,
|
||||
"conflictDescriptions": undefined,
|
||||
"count": 0,
|
||||
"customLabel": undefined,
|
||||
"esTypes": Array [
|
||||
"text",
|
||||
],
|
||||
"lang": "expression",
|
||||
"name": "script string",
|
||||
"readFromDocValues": false,
|
||||
"script": "'i am a string'",
|
||||
"scripted": true,
|
||||
"searchable": true,
|
||||
"subType": undefined,
|
||||
"type": "string",
|
||||
},
|
||||
Object {
|
||||
"aggregatable": true,
|
||||
"conflictDescriptions": undefined,
|
||||
"count": 0,
|
||||
"customLabel": undefined,
|
||||
"esTypes": Array [
|
||||
"long",
|
||||
],
|
||||
"lang": "expression",
|
||||
"name": "script number",
|
||||
"readFromDocValues": false,
|
||||
"script": "1234",
|
||||
"scripted": true,
|
||||
"searchable": true,
|
||||
"subType": undefined,
|
||||
"type": "number",
|
||||
},
|
||||
Object {
|
||||
"aggregatable": true,
|
||||
"conflictDescriptions": undefined,
|
||||
"count": 0,
|
||||
"customLabel": undefined,
|
||||
"esTypes": Array [
|
||||
"date",
|
||||
],
|
||||
"lang": "painless",
|
||||
"name": "script date",
|
||||
"readFromDocValues": false,
|
||||
"script": "1234",
|
||||
"scripted": true,
|
||||
"searchable": true,
|
||||
"subType": undefined,
|
||||
"type": "date",
|
||||
},
|
||||
Object {
|
||||
"aggregatable": true,
|
||||
"conflictDescriptions": undefined,
|
||||
"count": 0,
|
||||
"customLabel": undefined,
|
||||
"esTypes": Array [
|
||||
"murmur3",
|
||||
],
|
||||
"lang": "expression",
|
||||
"name": "script murmur3",
|
||||
"readFromDocValues": false,
|
||||
"script": "1234",
|
||||
"scripted": true,
|
||||
"searchable": true,
|
||||
"subType": undefined,
|
||||
"type": "murmur3",
|
||||
},
|
||||
],
|
||||
"fieldsFetcher": Object {
|
||||
"apiClient": Object {
|
||||
"baseUrl": "",
|
||||
},
|
||||
},
|
||||
"flattenHit": [Function],
|
||||
"formatField": [Function],
|
||||
"formatHit": [Function],
|
||||
"getComputedFields": [Function],
|
||||
"getConfig": [Function],
|
||||
"getFieldByName": [Function],
|
||||
"getFormatterForField": [Function],
|
||||
"getNonScriptedFields": [Function],
|
||||
"getScriptedFields": [Function],
|
||||
"getSourceFiltering": [Function],
|
||||
"id": "logstash-*",
|
||||
"isTimeBased": [Function],
|
||||
"metaFields": Array [
|
||||
"_id",
|
||||
"_type",
|
||||
"_source",
|
||||
],
|
||||
"popularizeField": [Function],
|
||||
"stubSetFieldFormat": [Function],
|
||||
"timeFieldName": "time",
|
||||
"title": "logstash-*",
|
||||
}
|
||||
}
|
||||
intl={
|
||||
Object {
|
||||
"defaultFormats": Object {},
|
||||
"defaultLocale": "en",
|
||||
"formatDate": [Function],
|
||||
"formatHTMLMessage": [Function],
|
||||
"formatMessage": [Function],
|
||||
"formatNumber": [Function],
|
||||
"formatPlural": [Function],
|
||||
"formatRelative": [Function],
|
||||
"formatTime": [Function],
|
||||
"formats": Object {
|
||||
"date": Object {
|
||||
"full": Object {
|
||||
"day": "numeric",
|
||||
"month": "long",
|
||||
"weekday": "long",
|
||||
"year": "numeric",
|
||||
},
|
||||
"long": Object {
|
||||
"day": "numeric",
|
||||
"month": "long",
|
||||
"year": "numeric",
|
||||
},
|
||||
"medium": Object {
|
||||
"day": "numeric",
|
||||
"month": "short",
|
||||
"year": "numeric",
|
||||
},
|
||||
"short": Object {
|
||||
"day": "numeric",
|
||||
"month": "numeric",
|
||||
"year": "2-digit",
|
||||
},
|
||||
},
|
||||
"number": Object {
|
||||
"currency": Object {
|
||||
"style": "currency",
|
||||
},
|
||||
"percent": Object {
|
||||
"style": "percent",
|
||||
},
|
||||
},
|
||||
"relative": Object {
|
||||
"days": Object {
|
||||
"units": "day",
|
||||
},
|
||||
"hours": Object {
|
||||
"units": "hour",
|
||||
},
|
||||
"minutes": Object {
|
||||
"units": "minute",
|
||||
},
|
||||
"months": Object {
|
||||
"units": "month",
|
||||
},
|
||||
"seconds": Object {
|
||||
"units": "second",
|
||||
},
|
||||
"years": Object {
|
||||
"units": "year",
|
||||
},
|
||||
},
|
||||
"time": Object {
|
||||
"full": Object {
|
||||
"hour": "numeric",
|
||||
"minute": "numeric",
|
||||
"second": "numeric",
|
||||
"timeZoneName": "short",
|
||||
},
|
||||
"long": Object {
|
||||
"hour": "numeric",
|
||||
"minute": "numeric",
|
||||
"second": "numeric",
|
||||
"timeZoneName": "short",
|
||||
},
|
||||
"medium": Object {
|
||||
"hour": "numeric",
|
||||
"minute": "numeric",
|
||||
"second": "numeric",
|
||||
},
|
||||
"short": Object {
|
||||
"hour": "numeric",
|
||||
"minute": "numeric",
|
||||
},
|
||||
},
|
||||
},
|
||||
"formatters": Object {
|
||||
"getDateTimeFormat": [Function],
|
||||
"getMessageFormat": [Function],
|
||||
"getNumberFormat": [Function],
|
||||
"getPluralFormat": [Function],
|
||||
"getRelativeFormat": [Function],
|
||||
},
|
||||
"locale": "en",
|
||||
"messages": Object {},
|
||||
"now": [Function],
|
||||
"onError": [Function],
|
||||
"textComponent": Symbol(react.fragment),
|
||||
"timeZone": null,
|
||||
}
|
||||
}
|
||||
onAddFilter={[MockFunction]}
|
||||
>
|
||||
<EuiPopoverFooter>
|
||||
<div
|
||||
className="euiPopoverFooter"
|
||||
>
|
||||
<EuiText
|
||||
size="xs"
|
||||
textAlign="center"
|
||||
>
|
||||
<div
|
||||
className="euiText euiText--extraSmall"
|
||||
>
|
||||
<EuiTextAlign
|
||||
textAlign="center"
|
||||
>
|
||||
<div
|
||||
className="euiTextAlign euiTextAlign--center"
|
||||
>
|
||||
<EuiLink
|
||||
data-test-subj="onAddFilterButton"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<button
|
||||
className="euiLink euiLink--primary"
|
||||
data-test-subj="onAddFilterButton"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Exists in {value} / {totalValue} records"
|
||||
id="discover.fieldChooser.detailViews.existsInRecordsText"
|
||||
values={
|
||||
Object {
|
||||
"totalValue": 2,
|
||||
"value": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
Exists in 1 / 2 records
|
||||
</FormattedMessage>
|
||||
</button>
|
||||
</EuiLink>
|
||||
</div>
|
||||
</EuiTextAlign>
|
||||
</div>
|
||||
</EuiText>
|
||||
</div>
|
||||
</EuiPopoverFooter>
|
||||
</DiscoverFieldDetailsFooter>
|
||||
`;
|
|
@ -1,4 +1,10 @@
|
|||
.dscSidebarItem__fieldPopoverPanel {
|
||||
min-width: 260px;
|
||||
max-width: 300px;
|
||||
min-width: $euiSizeXXL * 6.5;
|
||||
max-width: $euiSizeXXL * 7.5;
|
||||
}
|
||||
|
||||
.dscSidebarItem--multi {
|
||||
.kbnFieldButton__button {
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -82,7 +82,7 @@ function getComponent({
|
|||
const props = {
|
||||
indexPattern,
|
||||
field: finalField,
|
||||
getDetails: jest.fn(() => ({ buckets: [], error: '', exists: 1, total: true, columns: [] })),
|
||||
getDetails: jest.fn(() => ({ buckets: [], error: '', exists: 1, total: 2, columns: [] })),
|
||||
onAddFilter: jest.fn(),
|
||||
onAddField: jest.fn(),
|
||||
onRemoveField: jest.fn(),
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
import './discover_field.scss';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { EuiPopover, EuiPopoverTitle, EuiButtonIcon, EuiToolTip } from '@elastic/eui';
|
||||
import { EuiPopover, EuiPopoverTitle, EuiButtonIcon, EuiToolTip, EuiTitle } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { UiCounterMetricType } from '@kbn/analytics';
|
||||
import classNames from 'classnames';
|
||||
|
@ -28,6 +28,7 @@ import { FieldIcon, FieldButton } from '../../../../../kibana_react/public';
|
|||
import { FieldDetails } from './types';
|
||||
import { IndexPatternField, IndexPattern } from '../../../../../data/public';
|
||||
import { getFieldTypeName } from './lib/get_field_type_name';
|
||||
import { DiscoverFieldDetailsFooter } from './discover_field_details_footer';
|
||||
|
||||
export interface DiscoverFieldProps {
|
||||
/**
|
||||
|
@ -69,6 +70,8 @@ export interface DiscoverFieldProps {
|
|||
* @param eventName
|
||||
*/
|
||||
trackUiMetric?: (metricType: UiCounterMetricType, eventName: string | string[]) => void;
|
||||
|
||||
multiFields?: Array<{ field: IndexPatternField; isSelected: boolean }>;
|
||||
}
|
||||
|
||||
export function DiscoverField({
|
||||
|
@ -81,6 +84,7 @@ export function DiscoverField({
|
|||
getDetails,
|
||||
selected,
|
||||
trackUiMetric,
|
||||
multiFields,
|
||||
}: DiscoverFieldProps) {
|
||||
const addLabelAria = i18n.translate('discover.fieldChooser.discoverField.addButtonAriaLabel', {
|
||||
defaultMessage: 'Add {field} to table',
|
||||
|
@ -96,8 +100,8 @@ export function DiscoverField({
|
|||
|
||||
const [infoIsOpen, setOpen] = useState(false);
|
||||
|
||||
const toggleDisplay = (f: IndexPatternField) => {
|
||||
if (selected) {
|
||||
const toggleDisplay = (f: IndexPatternField, isSelected: boolean) => {
|
||||
if (isSelected) {
|
||||
onRemoveField(f.name);
|
||||
} else {
|
||||
onAddField(f.name);
|
||||
|
@ -115,72 +119,100 @@ export function DiscoverField({
|
|||
return str ? str.replace(/\./g, '.\u200B') : '';
|
||||
}
|
||||
|
||||
const dscFieldIcon = (
|
||||
<FieldIcon type={field.type} label={getFieldTypeName(field.type)} scripted={field.scripted} />
|
||||
);
|
||||
const getDscFieldIcon = (indexPatternField: IndexPatternField) => {
|
||||
return (
|
||||
<FieldIcon
|
||||
type={indexPatternField.type}
|
||||
label={getFieldTypeName(indexPatternField.type)}
|
||||
scripted={indexPatternField.scripted}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const title =
|
||||
field.displayName !== field.name ? `${field.name} (${field.displayName} )` : field.displayName;
|
||||
const dscFieldIcon = getDscFieldIcon(field);
|
||||
|
||||
const getTitle = (indexPatternField: IndexPatternField) => {
|
||||
return indexPatternField.displayName !== indexPatternField.name
|
||||
? i18n.translate('discover.field.title', {
|
||||
defaultMessage: '{fieldName} ({fieldDisplayName})',
|
||||
values: {
|
||||
fieldName: indexPatternField.name,
|
||||
fieldDisplayName: indexPatternField.displayName,
|
||||
},
|
||||
})
|
||||
: indexPatternField.displayName;
|
||||
};
|
||||
|
||||
const getFieldName = (indexPatternField: IndexPatternField) => {
|
||||
return (
|
||||
<span
|
||||
data-test-subj={`field-${indexPatternField.name}`}
|
||||
title={getTitle(indexPatternField)}
|
||||
className="dscSidebarField__name"
|
||||
>
|
||||
{wrapOnDot(indexPatternField.displayName)}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
const fieldName = getFieldName(field);
|
||||
|
||||
const fieldName = (
|
||||
<span data-test-subj={`field-${field.name}`} title={title} className="dscSidebarField__name">
|
||||
{wrapOnDot(field.displayName)}
|
||||
</span>
|
||||
);
|
||||
const actionBtnClassName = classNames('dscSidebarItem__action', {
|
||||
['dscSidebarItem__mobile']: alwaysShowActionButton,
|
||||
});
|
||||
let actionButton;
|
||||
if (field.name !== '_source' && !selected) {
|
||||
actionButton = (
|
||||
<EuiToolTip
|
||||
delay="long"
|
||||
content={i18n.translate('discover.fieldChooser.discoverField.addFieldTooltip', {
|
||||
defaultMessage: 'Add field as column',
|
||||
})}
|
||||
>
|
||||
<EuiButtonIcon
|
||||
iconType="plusInCircleFilled"
|
||||
className={actionBtnClassName}
|
||||
onClick={(ev: React.MouseEvent<HTMLButtonElement>) => {
|
||||
if (ev.type === 'click') {
|
||||
ev.currentTarget.focus();
|
||||
}
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
toggleDisplay(field);
|
||||
}}
|
||||
data-test-subj={`fieldToggle-${field.name}`}
|
||||
aria-label={addLabelAria}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
);
|
||||
} else if (field.name !== '_source' && selected) {
|
||||
actionButton = (
|
||||
<EuiToolTip
|
||||
delay="long"
|
||||
content={i18n.translate('discover.fieldChooser.discoverField.removeFieldTooltip', {
|
||||
defaultMessage: 'Remove field from table',
|
||||
})}
|
||||
>
|
||||
<EuiButtonIcon
|
||||
color="danger"
|
||||
iconType="cross"
|
||||
className={actionBtnClassName}
|
||||
onClick={(ev: React.MouseEvent<HTMLButtonElement>) => {
|
||||
if (ev.type === 'click') {
|
||||
ev.currentTarget.focus();
|
||||
}
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
toggleDisplay(field);
|
||||
}}
|
||||
data-test-subj={`fieldToggle-${field.name}`}
|
||||
aria-label={removeLabelAria}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
);
|
||||
}
|
||||
const getActionButton = (f: IndexPatternField, isSelected?: boolean) => {
|
||||
if (f.name !== '_source' && !isSelected) {
|
||||
return (
|
||||
<EuiToolTip
|
||||
delay="long"
|
||||
content={i18n.translate('discover.fieldChooser.discoverField.addFieldTooltip', {
|
||||
defaultMessage: 'Add field as column',
|
||||
})}
|
||||
>
|
||||
<EuiButtonIcon
|
||||
iconType="plusInCircleFilled"
|
||||
className={actionBtnClassName}
|
||||
onClick={(ev: React.MouseEvent<HTMLButtonElement>) => {
|
||||
if (ev.type === 'click') {
|
||||
ev.currentTarget.focus();
|
||||
}
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
toggleDisplay(f, false);
|
||||
}}
|
||||
data-test-subj={`fieldToggle-${f.name}`}
|
||||
aria-label={addLabelAria}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
);
|
||||
} else if (f.name !== '_source' && isSelected) {
|
||||
return (
|
||||
<EuiToolTip
|
||||
delay="long"
|
||||
content={i18n.translate('discover.fieldChooser.discoverField.removeFieldTooltip', {
|
||||
defaultMessage: 'Remove field from table',
|
||||
})}
|
||||
>
|
||||
<EuiButtonIcon
|
||||
color="danger"
|
||||
iconType="cross"
|
||||
className={actionBtnClassName}
|
||||
onClick={(ev: React.MouseEvent<HTMLButtonElement>) => {
|
||||
if (ev.type === 'click') {
|
||||
ev.currentTarget.focus();
|
||||
}
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
toggleDisplay(f, isSelected);
|
||||
}}
|
||||
data-test-subj={`fieldToggle-${f.name}`}
|
||||
aria-label={removeLabelAria}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const actionButton = getActionButton(field, selected);
|
||||
|
||||
if (field.type === '_source') {
|
||||
return (
|
||||
|
@ -195,6 +227,37 @@ export function DiscoverField({
|
|||
);
|
||||
}
|
||||
|
||||
const shouldRenderMultiFields = !!multiFields;
|
||||
const renderMultiFields = () => {
|
||||
if (!multiFields) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<React.Fragment>
|
||||
<EuiTitle size="xxxs">
|
||||
<h5>
|
||||
{i18n.translate('discover.fieldChooser.discoverField.multiFields', {
|
||||
defaultMessage: 'Multi fields',
|
||||
})}
|
||||
</h5>
|
||||
</EuiTitle>
|
||||
{multiFields.map((entry) => (
|
||||
<FieldButton
|
||||
size="s"
|
||||
className="dscSidebarItem dscSidebarItem--multi"
|
||||
isActive={false}
|
||||
onClick={() => {}}
|
||||
dataTestSubj={`field-${entry.field.name}-showDetails`}
|
||||
fieldIcon={getDscFieldIcon(entry.field)}
|
||||
fieldAction={getActionButton(entry.field, entry.isSelected)}
|
||||
fieldName={getFieldName(entry.field)}
|
||||
key={entry.field.name}
|
||||
/>
|
||||
))}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
display="block"
|
||||
|
@ -217,12 +280,14 @@ export function DiscoverField({
|
|||
anchorPosition="rightUp"
|
||||
panelClassName="dscSidebarItem__fieldPopoverPanel"
|
||||
>
|
||||
<EuiPopoverTitle>
|
||||
{' '}
|
||||
{i18n.translate('discover.fieldChooser.discoverField.fieldTopValuesLabel', {
|
||||
defaultMessage: 'Top 5 values',
|
||||
})}
|
||||
</EuiPopoverTitle>
|
||||
<EuiPopoverTitle style={{ textTransform: 'none' }}>{field.displayName}</EuiPopoverTitle>
|
||||
<EuiTitle size="xxxs">
|
||||
<h5>
|
||||
{i18n.translate('discover.fieldChooser.discoverField.fieldTopValuesLabel', {
|
||||
defaultMessage: 'Top 5 values',
|
||||
})}
|
||||
</h5>
|
||||
</EuiTitle>
|
||||
{infoIsOpen && (
|
||||
<DiscoverFieldDetails
|
||||
indexPattern={indexPattern}
|
||||
|
@ -230,8 +295,20 @@ export function DiscoverField({
|
|||
details={getDetails(field)}
|
||||
onAddFilter={onAddFilter}
|
||||
trackUiMetric={trackUiMetric}
|
||||
showFooter={!shouldRenderMultiFields}
|
||||
/>
|
||||
)}
|
||||
{shouldRenderMultiFields ? (
|
||||
<>
|
||||
{renderMultiFields()}
|
||||
<DiscoverFieldDetailsFooter
|
||||
indexPattern={indexPattern}
|
||||
field={field}
|
||||
details={getDetails(field)}
|
||||
onAddFilter={onAddFilter}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
</EuiPopover>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -38,7 +38,7 @@ const indexPattern = getStubIndexPattern(
|
|||
describe('discover sidebar field details', function () {
|
||||
const defaultProps = {
|
||||
indexPattern,
|
||||
details: { buckets: [], error: '', exists: 1, total: true, columns: [] },
|
||||
details: { buckets: [], error: '', exists: 1, total: 2, columns: [] },
|
||||
onAddFilter: jest.fn(),
|
||||
};
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
* under the License.
|
||||
*/
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { EuiLink, EuiIconTip, EuiText, EuiPopoverFooter, EuiButton, EuiSpacer } from '@elastic/eui';
|
||||
import { EuiIconTip, EuiText, EuiButton, EuiSpacer } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { METRIC_TYPE, UiCounterMetricType } from '@kbn/analytics';
|
||||
import { DiscoverFieldBucket } from './discover_field_bucket';
|
||||
|
@ -30,6 +30,7 @@ import {
|
|||
import { Bucket, FieldDetails } from './types';
|
||||
import { IndexPatternField, IndexPattern } from '../../../../../data/public';
|
||||
import './discover_field_details.scss';
|
||||
import { DiscoverFieldDetailsFooter } from './discover_field_details_footer';
|
||||
|
||||
interface DiscoverFieldDetailsProps {
|
||||
field: IndexPatternField;
|
||||
|
@ -37,6 +38,7 @@ interface DiscoverFieldDetailsProps {
|
|||
details: FieldDetails;
|
||||
onAddFilter: (field: IndexPatternField | string, value: string, type: '+' | '-') => void;
|
||||
trackUiMetric?: (metricType: UiCounterMetricType, eventName: string | string[]) => void;
|
||||
showFooter?: boolean;
|
||||
}
|
||||
|
||||
export function DiscoverFieldDetails({
|
||||
|
@ -45,6 +47,7 @@ export function DiscoverFieldDetails({
|
|||
details,
|
||||
onAddFilter,
|
||||
trackUiMetric,
|
||||
showFooter = true,
|
||||
}: DiscoverFieldDetailsProps) {
|
||||
const warnings = getWarnings(field);
|
||||
const [showVisualizeLink, setShowVisualizeLink] = useState<boolean>(false);
|
||||
|
@ -118,27 +121,13 @@ export function DiscoverFieldDetails({
|
|||
</>
|
||||
)}
|
||||
</div>
|
||||
{!details.error && (
|
||||
<EuiPopoverFooter>
|
||||
<EuiText size="xs" textAlign="center">
|
||||
{!indexPattern.metaFields.includes(field.name) && !field.scripted ? (
|
||||
<EuiLink onClick={() => onAddFilter('_exists_', field.name, '+')}>
|
||||
<FormattedMessage
|
||||
id="discover.fieldChooser.detailViews.existsText"
|
||||
defaultMessage="Exists in"
|
||||
/>{' '}
|
||||
{details.exists}
|
||||
</EuiLink>
|
||||
) : (
|
||||
<span>{details.exists}</span>
|
||||
)}{' '}
|
||||
/ {details.total}{' '}
|
||||
<FormattedMessage
|
||||
id="discover.fieldChooser.detailViews.recordsText"
|
||||
defaultMessage="records"
|
||||
/>
|
||||
</EuiText>
|
||||
</EuiPopoverFooter>
|
||||
{!details.error && showFooter && (
|
||||
<DiscoverFieldDetailsFooter
|
||||
field={field}
|
||||
indexPattern={indexPattern}
|
||||
details={details}
|
||||
onAddFilter={onAddFilter}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* 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 { findTestSubject } from '@elastic/eui/lib/test';
|
||||
// @ts-ignore
|
||||
import stubbedLogstashFields from 'fixtures/logstash_fields';
|
||||
import { mountWithIntl } from '@kbn/test/jest';
|
||||
import { coreMock } from '../../../../../../core/public/mocks';
|
||||
import { IndexPatternField } from '../../../../../data/public';
|
||||
import { getStubIndexPattern } from '../../../../../data/public/test_utils';
|
||||
import { DiscoverFieldDetailsFooter } from './discover_field_details_footer';
|
||||
|
||||
const indexPattern = getStubIndexPattern(
|
||||
'logstash-*',
|
||||
(cfg: any) => cfg,
|
||||
'time',
|
||||
stubbedLogstashFields(),
|
||||
coreMock.createSetup()
|
||||
);
|
||||
|
||||
describe('discover sidebar field details footer', function () {
|
||||
const onAddFilter = jest.fn();
|
||||
const defaultProps = {
|
||||
indexPattern,
|
||||
details: { buckets: [], error: '', exists: 1, total: 2, columns: [] },
|
||||
onAddFilter,
|
||||
};
|
||||
|
||||
function mountComponent(field: IndexPatternField) {
|
||||
const compProps = { ...defaultProps, field };
|
||||
return mountWithIntl(<DiscoverFieldDetailsFooter {...compProps} />);
|
||||
}
|
||||
|
||||
it('renders properly', function () {
|
||||
const visualizableField = new IndexPatternField({
|
||||
name: 'bytes',
|
||||
type: 'number',
|
||||
esTypes: ['long'],
|
||||
count: 10,
|
||||
scripted: false,
|
||||
searchable: true,
|
||||
aggregatable: true,
|
||||
readFromDocValues: true,
|
||||
});
|
||||
const component = mountComponent(visualizableField);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('click on addFilter calls the function', function () {
|
||||
const visualizableField = new IndexPatternField({
|
||||
name: 'bytes',
|
||||
type: 'number',
|
||||
esTypes: ['long'],
|
||||
count: 10,
|
||||
scripted: false,
|
||||
searchable: true,
|
||||
aggregatable: true,
|
||||
readFromDocValues: true,
|
||||
});
|
||||
const component = mountComponent(visualizableField);
|
||||
const onAddButton = findTestSubject(component, 'onAddFilterButton');
|
||||
onAddButton.simulate('click');
|
||||
expect(onAddFilter).toHaveBeenCalledWith('_exists_', visualizableField.name, '+');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* 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, EuiPopoverFooter, EuiText } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { IndexPatternField } from '../../../../../data/common/index_patterns/fields';
|
||||
import { IndexPattern } from '../../../../../data/common/index_patterns/index_patterns';
|
||||
import { FieldDetails } from './types';
|
||||
|
||||
interface DiscoverFieldDetailsFooterProps {
|
||||
field: IndexPatternField;
|
||||
indexPattern: IndexPattern;
|
||||
details: FieldDetails;
|
||||
onAddFilter: (field: IndexPatternField | string, value: string, type: '+' | '-') => void;
|
||||
}
|
||||
|
||||
export function DiscoverFieldDetailsFooter({
|
||||
field,
|
||||
indexPattern,
|
||||
details,
|
||||
onAddFilter,
|
||||
}: DiscoverFieldDetailsFooterProps) {
|
||||
return (
|
||||
<EuiPopoverFooter>
|
||||
<EuiText size="xs" textAlign="center">
|
||||
{!indexPattern.metaFields.includes(field.name) && !field.scripted ? (
|
||||
<EuiLink
|
||||
onClick={() => onAddFilter('_exists_', field.name, '+')}
|
||||
data-test-subj="onAddFilterButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="discover.fieldChooser.detailViews.existsInRecordsText"
|
||||
defaultMessage="Exists in {value} / {totalValue} records"
|
||||
values={{
|
||||
value: details.exists,
|
||||
totalValue: details.total,
|
||||
}}
|
||||
/>
|
||||
</EuiLink>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="discover.fieldChooser.detailViews.valueOfRecordsText"
|
||||
defaultMessage="{value} / {totalValue} records"
|
||||
values={{
|
||||
value: details.exists,
|
||||
totalValue: details.total,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</EuiText>
|
||||
</EuiPopoverFooter>
|
||||
);
|
||||
}
|
|
@ -100,6 +100,10 @@ export interface DiscoverSidebarProps {
|
|||
* Callback function to select another index pattern
|
||||
*/
|
||||
setIndexPattern: (id: string) => void;
|
||||
/**
|
||||
* If on, fields are read from the fields API, not from source
|
||||
*/
|
||||
useNewFieldsApi?: boolean;
|
||||
/**
|
||||
* Metric tracking function
|
||||
* @param metricType
|
||||
|
@ -127,9 +131,11 @@ export function DiscoverSidebar({
|
|||
setFieldFilter,
|
||||
setIndexPattern,
|
||||
trackUiMetric,
|
||||
useNewFieldsApi = false,
|
||||
useFlyout = false,
|
||||
}: DiscoverSidebarProps) {
|
||||
const [fields, setFields] = useState<IndexPatternField[] | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const newFields = getIndexPatternFieldList(selectedIndexPattern, fieldCounts);
|
||||
setFields(newFields);
|
||||
|
@ -154,13 +160,10 @@ export function DiscoverSidebar({
|
|||
selected: selectedFields,
|
||||
popular: popularFields,
|
||||
unpopular: unpopularFields,
|
||||
} = useMemo(() => groupFields(fields, columns, popularLimit, fieldCounts, fieldFilter), [
|
||||
fields,
|
||||
columns,
|
||||
popularLimit,
|
||||
fieldCounts,
|
||||
fieldFilter,
|
||||
]);
|
||||
} = useMemo(
|
||||
() => groupFields(fields, columns, popularLimit, fieldCounts, fieldFilter, useNewFieldsApi),
|
||||
[fields, columns, popularLimit, fieldCounts, fieldFilter, useNewFieldsApi]
|
||||
);
|
||||
|
||||
const fieldTypes = useMemo(() => {
|
||||
const result = ['any'];
|
||||
|
@ -174,6 +177,27 @@ export function DiscoverSidebar({
|
|||
return result;
|
||||
}, [fields]);
|
||||
|
||||
const multiFields = useMemo(() => {
|
||||
if (!useNewFieldsApi || !fields) {
|
||||
return undefined;
|
||||
}
|
||||
const map = new Map<string, Array<{ field: IndexPatternField; isSelected: boolean }>>();
|
||||
fields.forEach((field) => {
|
||||
const parent = field.spec?.subType?.multi?.parent;
|
||||
if (!parent) {
|
||||
return;
|
||||
}
|
||||
const multiField = {
|
||||
field,
|
||||
isSelected: selectedFields.includes(field),
|
||||
};
|
||||
const value = map.get(parent) ?? [];
|
||||
value.push(multiField);
|
||||
map.set(parent, value);
|
||||
});
|
||||
return map;
|
||||
}, [fields, useNewFieldsApi, selectedFields]);
|
||||
|
||||
if (!selectedIndexPattern || !fields) {
|
||||
return null;
|
||||
}
|
||||
|
@ -278,6 +302,7 @@ export function DiscoverSidebar({
|
|||
getDetails={getDetailsByField}
|
||||
selected={true}
|
||||
trackUiMetric={trackUiMetric}
|
||||
multiFields={multiFields?.get(field.name)}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
|
@ -338,6 +363,7 @@ export function DiscoverSidebar({
|
|||
onAddFilter={onAddFilter}
|
||||
getDetails={getDetailsByField}
|
||||
trackUiMetric={trackUiMetric}
|
||||
multiFields={multiFields?.get(field.name)}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
|
@ -366,6 +392,7 @@ export function DiscoverSidebar({
|
|||
onAddFilter={onAddFilter}
|
||||
getDetails={getDetailsByField}
|
||||
trackUiMetric={trackUiMetric}
|
||||
multiFields={multiFields?.get(field.name)}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
|
|
|
@ -103,6 +103,10 @@ export interface DiscoverSidebarResponsiveProps {
|
|||
* Shows index pattern and a button that displays the sidebar in a flyout
|
||||
*/
|
||||
useFlyout?: boolean;
|
||||
/**
|
||||
* Read from the Fields API
|
||||
*/
|
||||
useNewFieldsApi?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -69,7 +69,8 @@ describe('group_fields', function () {
|
|||
['currency'],
|
||||
5,
|
||||
fieldCounts,
|
||||
fieldFilterState
|
||||
fieldFilterState,
|
||||
false
|
||||
);
|
||||
expect(actual).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
|
@ -118,6 +119,80 @@ describe('group_fields', function () {
|
|||
}
|
||||
`);
|
||||
});
|
||||
it('should group fields in selected, popular, unpopular group if they contain multifields', function () {
|
||||
const category = {
|
||||
name: 'category',
|
||||
type: 'string',
|
||||
esTypes: ['text'],
|
||||
count: 1,
|
||||
scripted: false,
|
||||
searchable: true,
|
||||
aggregatable: true,
|
||||
readFromDocValues: true,
|
||||
};
|
||||
const currency = {
|
||||
name: 'currency',
|
||||
displayName: 'currency',
|
||||
kbnFieldType: {
|
||||
esTypes: ['string', 'text', 'keyword', '_type', '_id'],
|
||||
filterable: true,
|
||||
name: 'string',
|
||||
sortable: true,
|
||||
},
|
||||
spec: {
|
||||
esTypes: ['text'],
|
||||
name: 'category',
|
||||
},
|
||||
scripted: false,
|
||||
searchable: true,
|
||||
aggregatable: true,
|
||||
readFromDocValues: true,
|
||||
};
|
||||
const currencyKeyword = {
|
||||
name: 'currency.keyword',
|
||||
displayName: 'currency.keyword',
|
||||
type: 'string',
|
||||
esTypes: ['keyword'],
|
||||
kbnFieldType: {
|
||||
esTypes: ['string', 'text', 'keyword', '_type', '_id'],
|
||||
filterable: true,
|
||||
name: 'string',
|
||||
sortable: true,
|
||||
},
|
||||
spec: {
|
||||
aggregatable: true,
|
||||
esTypes: ['keyword'],
|
||||
name: 'category.keyword',
|
||||
readFromDocValues: true,
|
||||
searchable: true,
|
||||
shortDotsEnable: false,
|
||||
subType: {
|
||||
multi: {
|
||||
parent: 'currency',
|
||||
},
|
||||
},
|
||||
},
|
||||
scripted: false,
|
||||
searchable: true,
|
||||
aggregatable: true,
|
||||
readFromDocValues: false,
|
||||
};
|
||||
const fieldsToGroup = [category, currency, currencyKeyword];
|
||||
|
||||
const fieldFilterState = getDefaultFieldFilter();
|
||||
|
||||
const actual = groupFields(
|
||||
fieldsToGroup as any,
|
||||
['currency'],
|
||||
5,
|
||||
fieldCounts,
|
||||
fieldFilterState,
|
||||
true
|
||||
);
|
||||
expect(actual.popular).toEqual([category]);
|
||||
expect(actual.selected).toEqual([currency]);
|
||||
expect(actual.unpopular).toEqual([]);
|
||||
});
|
||||
|
||||
it('should sort selected fields by columns order ', function () {
|
||||
const fieldFilterState = getDefaultFieldFilter();
|
||||
|
@ -127,7 +202,8 @@ describe('group_fields', function () {
|
|||
['customer_birth_date', 'currency', 'unknown'],
|
||||
5,
|
||||
fieldCounts,
|
||||
fieldFilterState
|
||||
fieldFilterState,
|
||||
false
|
||||
);
|
||||
expect(actual1.selected.map((field) => field.name)).toEqual([
|
||||
'customer_birth_date',
|
||||
|
@ -140,7 +216,8 @@ describe('group_fields', function () {
|
|||
['currency', 'customer_birth_date', 'unknown'],
|
||||
5,
|
||||
fieldCounts,
|
||||
fieldFilterState
|
||||
fieldFilterState,
|
||||
false
|
||||
);
|
||||
expect(actual2.selected.map((field) => field.name)).toEqual([
|
||||
'currency',
|
||||
|
|
|
@ -33,7 +33,8 @@ export function groupFields(
|
|||
columns: string[],
|
||||
popularLimit: number,
|
||||
fieldCounts: Record<string, number>,
|
||||
fieldFilterState: FieldFilterState
|
||||
fieldFilterState: FieldFilterState,
|
||||
useNewFieldsApi: boolean
|
||||
): GroupedFields {
|
||||
const result: GroupedFields = {
|
||||
selected: [],
|
||||
|
@ -62,12 +63,17 @@ export function groupFields(
|
|||
if (!isFieldFiltered(field, fieldFilterState, fieldCounts)) {
|
||||
continue;
|
||||
}
|
||||
const isSubfield = useNewFieldsApi && field.spec?.subType?.multi?.parent;
|
||||
if (columns.includes(field.name)) {
|
||||
result.selected.push(field);
|
||||
} else if (popular.includes(field.name) && field.type !== '_source') {
|
||||
result.popular.push(field);
|
||||
if (!isSubfield) {
|
||||
result.popular.push(field);
|
||||
}
|
||||
} else if (field.type !== '_source') {
|
||||
result.unpopular.push(field);
|
||||
if (!isSubfield) {
|
||||
result.unpopular.push(field);
|
||||
}
|
||||
}
|
||||
}
|
||||
// add columns, that are not part of the index pattern, to be removeable
|
||||
|
|
|
@ -25,7 +25,7 @@ export interface IndexPatternRef {
|
|||
export interface FieldDetails {
|
||||
error: string;
|
||||
exists: number;
|
||||
total: boolean;
|
||||
total: number;
|
||||
buckets: Bucket[];
|
||||
columns: string[];
|
||||
}
|
||||
|
|
|
@ -29,7 +29,8 @@ export function getSwitchIndexPatternAppState(
|
|||
nextIndexPattern: IndexPattern,
|
||||
currentColumns: string[],
|
||||
currentSort: SortPairArr[],
|
||||
modifyColumns: boolean = true
|
||||
modifyColumns: boolean = true,
|
||||
useNewFieldsApi: boolean = false
|
||||
) {
|
||||
const nextColumns = modifyColumns
|
||||
? currentColumns.filter(
|
||||
|
@ -38,9 +39,11 @@ export function getSwitchIndexPatternAppState(
|
|||
)
|
||||
: currentColumns;
|
||||
const nextSort = getSortArray(currentSort, nextIndexPattern);
|
||||
const defaultColumns = useNewFieldsApi ? [] : ['_source'];
|
||||
const columns = nextColumns.length ? nextColumns : defaultColumns;
|
||||
return {
|
||||
index: nextIndexPattern.id,
|
||||
columns: nextColumns.length ? nextColumns : ['_source'],
|
||||
columns,
|
||||
sort: nextSort,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -49,6 +49,8 @@ export async function persistSavedSearch(
|
|||
indexPattern,
|
||||
services,
|
||||
sort: state.sort as SortOrder[],
|
||||
columns: state.columns || [],
|
||||
useNewFieldsApi: false,
|
||||
});
|
||||
|
||||
savedSearch.columns = state.columns || [];
|
||||
|
|
|
@ -44,8 +44,37 @@ describe('updateSearchSource', () => {
|
|||
} as unknown) as IUiSettingsClient,
|
||||
} as unknown) as DiscoverServices,
|
||||
sort: [] as SortOrder[],
|
||||
columns: [],
|
||||
useNewFieldsApi: false,
|
||||
});
|
||||
expect(result.getField('index')).toEqual(indexPatternMock);
|
||||
expect(result.getField('size')).toEqual(sampleSize);
|
||||
expect(result.getField('fields')).toBe(undefined);
|
||||
});
|
||||
|
||||
test('updates a given search source with the usage of the new fields api', async () => {
|
||||
const searchSourceMock = createSearchSourceMock({});
|
||||
const sampleSize = 250;
|
||||
const result = updateSearchSource(searchSourceMock, {
|
||||
indexPattern: indexPatternMock,
|
||||
services: ({
|
||||
data: dataPluginMock.createStartContract(),
|
||||
uiSettings: ({
|
||||
get: (key: string) => {
|
||||
if (key === SAMPLE_SIZE_SETTING) {
|
||||
return sampleSize;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
} as unknown) as IUiSettingsClient,
|
||||
} as unknown) as DiscoverServices,
|
||||
sort: [] as SortOrder[],
|
||||
columns: [],
|
||||
useNewFieldsApi: true,
|
||||
});
|
||||
expect(result.getField('index')).toEqual(indexPatternMock);
|
||||
expect(result.getField('size')).toEqual(sampleSize);
|
||||
expect(result.getField('fields')).toEqual(['*']);
|
||||
expect(result.getField('fieldsFromSource')).toBe(undefined);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -31,10 +31,14 @@ export function updateSearchSource(
|
|||
indexPattern,
|
||||
services,
|
||||
sort,
|
||||
columns,
|
||||
useNewFieldsApi,
|
||||
}: {
|
||||
indexPattern: IndexPattern;
|
||||
services: DiscoverServices;
|
||||
sort: SortOrder[];
|
||||
columns: string[];
|
||||
useNewFieldsApi: boolean;
|
||||
}
|
||||
) {
|
||||
const { uiSettings, data } = services;
|
||||
|
@ -50,5 +54,13 @@ export function updateSearchSource(
|
|||
.setField('sort', usedSort)
|
||||
.setField('query', data.query.queryString.getQuery() || null)
|
||||
.setField('filter', data.query.filterManager.getFilters());
|
||||
if (useNewFieldsApi) {
|
||||
searchSource.removeField('fieldsFromSource');
|
||||
searchSource.setField('fields', ['*']);
|
||||
} else {
|
||||
searchSource.removeField('fields');
|
||||
const fieldNames = indexPattern.fields.map((field) => field.name);
|
||||
searchSource.setField('fieldsFromSource', fieldNames);
|
||||
}
|
||||
return searchSource;
|
||||
}
|
||||
|
|
|
@ -35,6 +35,7 @@ import {
|
|||
CONTEXT_TIE_BREAKER_FIELDS_SETTING,
|
||||
DOC_TABLE_LEGACY,
|
||||
MODIFY_COLUMNS_ON_SWITCH,
|
||||
SEARCH_FIELDS_FROM_SOURCE,
|
||||
} from '../common';
|
||||
|
||||
export const uiSettings: Record<string, UiSettingsParams> = {
|
||||
|
@ -198,4 +199,11 @@ export const uiSettings: Record<string, UiSettingsParams> = {
|
|||
name: 'discover:modifyColumnsOnSwitchTitle',
|
||||
},
|
||||
},
|
||||
[SEARCH_FIELDS_FROM_SOURCE]: {
|
||||
name: 'Read fields from _source',
|
||||
description: `When enabled will load documents directly from \`_source\`. This is soon going to be deprecated. When disabled, will retrieve fields via the new Fields API in the high-level search service.`,
|
||||
value: false,
|
||||
category: ['discover'],
|
||||
schema: schema.boolean(),
|
||||
},
|
||||
};
|
||||
|
|
|
@ -38,6 +38,7 @@ export default function ({ getService, getPageObjects }) {
|
|||
await kibanaServer.uiSettings.update({
|
||||
'context:defaultSize': `${TEST_DEFAULT_CONTEXT_SIZE}`,
|
||||
'context:step': `${TEST_STEP_SIZE}`,
|
||||
'discover:searchFieldsFromSource': true,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -56,7 +56,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
expect(hasDocHit).to.be(true);
|
||||
});
|
||||
|
||||
it('add filter should create an exists filter if value is null (#7189)', async function () {
|
||||
// no longer relevant as null field won't be returned in the Fields API response
|
||||
xit('add filter should create an exists filter if value is null (#7189)', async function () {
|
||||
await PageObjects.discover.waitUntilSearchingHasFinished();
|
||||
// Filter special document
|
||||
await filterBar.addFilter('agent', 'is', 'Missing/Fields');
|
||||
|
|
|
@ -53,7 +53,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
it('the search term should be highlighted in the field data', async function () {
|
||||
// marks is the style that highlights the text in yellow
|
||||
const marks = await PageObjects.discover.getMarks();
|
||||
expect(marks.length).to.be(25);
|
||||
expect(marks.length).to.be(50);
|
||||
expect(marks.indexOf('php')).to.be(0);
|
||||
});
|
||||
|
||||
|
|
71
test/functional/apps/discover/_discover_fields_api.ts
Normal file
71
test/functional/apps/discover/_discover_fields_api.ts
Normal file
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from './ftr_provider_context';
|
||||
|
||||
export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||
const log = getService('log');
|
||||
const retry = getService('retry');
|
||||
const esArchiver = getService('esArchiver');
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
const PageObjects = getPageObjects(['common', 'discover', 'header', 'timePicker']);
|
||||
const defaultSettings = {
|
||||
defaultIndex: 'logstash-*',
|
||||
'discover:searchFieldsFromSource': false,
|
||||
};
|
||||
describe('discover uses fields API test', function describeIndexTests() {
|
||||
before(async function () {
|
||||
log.debug('load kibana index with default index pattern');
|
||||
await esArchiver.load('discover');
|
||||
await esArchiver.loadIfNeeded('logstash_functional');
|
||||
await kibanaServer.uiSettings.replace(defaultSettings);
|
||||
log.debug('discover');
|
||||
await PageObjects.common.navigateToApp('discover');
|
||||
await PageObjects.timePicker.setDefaultAbsoluteRange();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await kibanaServer.uiSettings.replace({ 'discover:searchFieldsFromSource': true });
|
||||
});
|
||||
|
||||
it('should correctly display documents', async function () {
|
||||
log.debug('check if Document title exists in the grid');
|
||||
expect(await PageObjects.discover.getDocHeader()).to.have.string('Document');
|
||||
const rowData = await PageObjects.discover.getDocTableIndex(1);
|
||||
log.debug('check the newest doc timestamp in UTC (check diff timezone in last test)');
|
||||
expect(rowData.startsWith('Sep 22, 2015 @ 23:50:13.253')).to.be.ok();
|
||||
const expectedHitCount = '14,004';
|
||||
await retry.try(async function () {
|
||||
expect(await PageObjects.discover.getHitCount()).to.be(expectedHitCount);
|
||||
});
|
||||
});
|
||||
|
||||
it('adding a column removes a default column', async function () {
|
||||
await PageObjects.discover.clickFieldListItemAdd('_score');
|
||||
expect(await PageObjects.discover.getDocHeader()).to.have.string('_score');
|
||||
expect(await PageObjects.discover.getDocHeader()).not.to.have.string('Document');
|
||||
});
|
||||
|
||||
it('removing a column adds a default column', async function () {
|
||||
await PageObjects.discover.clickFieldListItemRemove('_score');
|
||||
expect(await PageObjects.discover.getDocHeader()).not.to.have.string('_score');
|
||||
expect(await PageObjects.discover.getDocHeader()).to.have.string('Document');
|
||||
});
|
||||
});
|
||||
}
|
|
@ -55,7 +55,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
expect(hasDocHit).to.be(true);
|
||||
});
|
||||
|
||||
it('add filter should create an exists filter if value is null (#7189)', async function () {
|
||||
// no longer relevant as null field won't be returned in the Fields API response
|
||||
xit('add filter should create an exists filter if value is null (#7189)', async function () {
|
||||
await PageObjects.discover.waitUntilSearchingHasFinished();
|
||||
// Filter special document
|
||||
await filterBar.addFilter('agent', 'is', 'Missing/Fields');
|
||||
|
|
|
@ -36,6 +36,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
await esArchiver.load('discover');
|
||||
await kibanaServer.uiSettings.replace({
|
||||
defaultIndex: 'logstash-*',
|
||||
'discover:searchFieldsFromSource': true,
|
||||
});
|
||||
await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings();
|
||||
await PageObjects.common.navigateToApp('discover');
|
||||
|
|
105
test/functional/apps/discover/_field_data_with_fields_api.ts
Normal file
105
test/functional/apps/discover/_field_data_with_fields_api.ts
Normal file
|
@ -0,0 +1,105 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
|
||||
export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||
const retry = getService('retry');
|
||||
const esArchiver = getService('esArchiver');
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
const toasts = getService('toasts');
|
||||
const queryBar = getService('queryBar');
|
||||
const PageObjects = getPageObjects(['common', 'header', 'discover', 'visualize', 'timePicker']);
|
||||
|
||||
describe('discover tab with new fields API', function describeIndexTests() {
|
||||
this.tags('includeFirefox');
|
||||
before(async function () {
|
||||
await esArchiver.loadIfNeeded('logstash_functional');
|
||||
await esArchiver.load('discover');
|
||||
await kibanaServer.uiSettings.replace({
|
||||
defaultIndex: 'logstash-*',
|
||||
'discover:searchFieldsFromSource': false,
|
||||
});
|
||||
await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings();
|
||||
await PageObjects.common.navigateToApp('discover');
|
||||
});
|
||||
describe('field data', function () {
|
||||
it('search php should show the correct hit count', async function () {
|
||||
const expectedHitCount = '445';
|
||||
await retry.try(async function () {
|
||||
await queryBar.setQuery('php');
|
||||
await queryBar.submitQuery();
|
||||
const hitCount = await PageObjects.discover.getHitCount();
|
||||
expect(hitCount).to.be(expectedHitCount);
|
||||
});
|
||||
});
|
||||
|
||||
it('the search term should be highlighted in the field data', async function () {
|
||||
// marks is the style that highlights the text in yellow
|
||||
const marks = await PageObjects.discover.getMarks();
|
||||
expect(marks.length).to.be(100);
|
||||
expect(marks.indexOf('php')).to.be(0);
|
||||
});
|
||||
|
||||
it('search type:apache should show the correct hit count', async function () {
|
||||
const expectedHitCount = '11,156';
|
||||
await queryBar.setQuery('type:apache');
|
||||
await queryBar.submitQuery();
|
||||
await retry.try(async function tryingForTime() {
|
||||
const hitCount = await PageObjects.discover.getHitCount();
|
||||
expect(hitCount).to.be(expectedHitCount);
|
||||
});
|
||||
});
|
||||
|
||||
it('doc view should show Time and Document columns', async function () {
|
||||
const expectedHeader = 'Time Document';
|
||||
const Docheader = await PageObjects.discover.getDocHeader();
|
||||
expect(Docheader).to.be(expectedHeader);
|
||||
});
|
||||
|
||||
it('doc view should sort ascending', async function () {
|
||||
const expectedTimeStamp = 'Sep 20, 2015 @ 00:00:00.000';
|
||||
await PageObjects.discover.clickDocSortDown();
|
||||
|
||||
// we don't technically need this sleep here because the tryForTime will retry and the
|
||||
// results will match on the 2nd or 3rd attempt, but that debug output is huge in this
|
||||
// case and it can be avoided with just a few seconds sleep.
|
||||
await PageObjects.common.sleep(2000);
|
||||
await retry.try(async function tryingForTime() {
|
||||
const rowData = await PageObjects.discover.getDocTableIndex(1);
|
||||
|
||||
expect(rowData.startsWith(expectedTimeStamp)).to.be.ok();
|
||||
});
|
||||
});
|
||||
|
||||
it('a bad syntax query should show an error message', async function () {
|
||||
const expectedError =
|
||||
'Expected ":", "<", "<=", ">", ">=", AND, OR, end of input, ' +
|
||||
'whitespace but "(" found.';
|
||||
await queryBar.setQuery('xxx(yyy))');
|
||||
await queryBar.submitQuery();
|
||||
const { message } = await toasts.getErrorToast();
|
||||
expect(message).to.contain(expectedError);
|
||||
await toasts.dismissToast();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -40,7 +40,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
|
||||
it('verify the large string book present', async function () {
|
||||
const ExpectedDoc =
|
||||
'mybook:Project Gutenberg EBook of Hamlet, by William Shakespeare' +
|
||||
'_id:1 _type: - _index:testlargestring _score:0' +
|
||||
' mybook:Project Gutenberg EBook of Hamlet, by William Shakespeare' +
|
||||
' This eBook is for the use of anyone anywhere in the United States' +
|
||||
' and most other parts of the world at no cost and with almost no restrictions whatsoever.' +
|
||||
' You may copy it, give it away or re-use it under the terms of the' +
|
||||
|
|
|
@ -88,7 +88,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
'/app/discover?_t=1453775307251#' +
|
||||
'/?_g=(filters:!(),refreshInterval:(pause:!t,value:0),time' +
|
||||
":(from:'2015-09-19T06:31:44.000Z',to:'2015-09" +
|
||||
"-23T18:31:44.000Z'))&_a=(columns:!(_source),filters:!(),index:'logstash-" +
|
||||
"-23T18:31:44.000Z'))&_a=(columns:!(),filters:!(),index:'logstash-" +
|
||||
"*',interval:auto,query:(language:kuery,query:'')" +
|
||||
",sort:!(!('@timestamp',desc)))";
|
||||
const actualUrl = await PageObjects.share.getSharedUrl();
|
||||
|
|
|
@ -40,6 +40,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
// and load a set of makelogs data
|
||||
await esArchiver.loadIfNeeded('logstash_functional');
|
||||
|
||||
await kibanaServer.uiSettings.update({
|
||||
'discover:searchFieldsFromSource': true,
|
||||
});
|
||||
|
||||
log.debug('discover');
|
||||
await PageObjects.common.navigateToApp('discover');
|
||||
|
||||
|
|
23
test/functional/apps/discover/ftr_provider_context.d.ts
vendored
Normal file
23
test/functional/apps/discover/ftr_provider_context.d.ts
vendored
Normal file
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* 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 { GenericFtrProviderContext } from '@kbn/test/types/ftr';
|
||||
import { services } from '../../services';
|
||||
import { pageObjects } from '../../page_objects';
|
||||
|
||||
export type FtrProviderContext = GenericFtrProviderContext<typeof services, typeof pageObjects>;
|
|
@ -42,6 +42,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) {
|
|||
loadTestFile(require.resolve('./_filter_editor'));
|
||||
loadTestFile(require.resolve('./_errors'));
|
||||
loadTestFile(require.resolve('./_field_data'));
|
||||
loadTestFile(require.resolve('./_field_data_with_fields_api'));
|
||||
loadTestFile(require.resolve('./_shared_links'));
|
||||
loadTestFile(require.resolve('./_sidebar'));
|
||||
loadTestFile(require.resolve('./_source_filters'));
|
||||
|
@ -51,6 +52,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) {
|
|||
loadTestFile(require.resolve('./_date_nanos'));
|
||||
loadTestFile(require.resolve('./_date_nanos_mixed'));
|
||||
loadTestFile(require.resolve('./_indexpattern_without_timefield'));
|
||||
loadTestFile(require.resolve('./_discover_fields_api'));
|
||||
loadTestFile(require.resolve('./_data_grid'));
|
||||
loadTestFile(require.resolve('./_data_grid_context'));
|
||||
loadTestFile(require.resolve('./_data_grid_field_data'));
|
||||
|
|
|
@ -5,7 +5,8 @@
|
|||
"mappings": {
|
||||
"properties": {
|
||||
"@timestamp": {
|
||||
"type": "date_nanos"
|
||||
"type": "date_nanos",
|
||||
"format": "strict_date_optional_time_nanos"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -29,7 +29,8 @@
|
|||
"mappings": {
|
||||
"properties": {
|
||||
"timestamp": {
|
||||
"type": "date_nanos"
|
||||
"type": "date_nanos",
|
||||
"format": "strict_date_optional_time_nanos"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -1510,10 +1510,8 @@
|
|||
"discover.embeddable.inspectorRequestDescription": "このリクエストはElasticsearchにクエリをかけ、検索データを取得します。",
|
||||
"discover.embeddable.search.displayName": "検索",
|
||||
"discover.fieldChooser.detailViews.emptyStringText": "空の文字列",
|
||||
"discover.fieldChooser.detailViews.existsText": "存在する",
|
||||
"discover.fieldChooser.detailViews.filterOutValueButtonAriaLabel": "{field}を除外:\"{value}\"",
|
||||
"discover.fieldChooser.detailViews.filterValueButtonAriaLabel": "{field}を除外:\"{value}\"",
|
||||
"discover.fieldChooser.detailViews.recordsText": "記録",
|
||||
"discover.fieldChooser.detailViews.visualizeLinkText": "可視化",
|
||||
"discover.fieldChooser.discoverField.addButtonAriaLabel": "{field}を表に追加",
|
||||
"discover.fieldChooser.discoverField.addFieldTooltip": "フィールドを列として追加",
|
||||
|
|
|
@ -1510,10 +1510,8 @@
|
|||
"discover.embeddable.inspectorRequestDescription": "此请求将查询 Elasticsearch 以获取搜索的数据。",
|
||||
"discover.embeddable.search.displayName": "搜索",
|
||||
"discover.fieldChooser.detailViews.emptyStringText": "空字符串",
|
||||
"discover.fieldChooser.detailViews.existsText": "存在于",
|
||||
"discover.fieldChooser.detailViews.filterOutValueButtonAriaLabel": "筛除 {field}:“{value}”",
|
||||
"discover.fieldChooser.detailViews.filterValueButtonAriaLabel": "筛留 {field}:“{value}”",
|
||||
"discover.fieldChooser.detailViews.recordsText": "个记录",
|
||||
"discover.fieldChooser.detailViews.visualizeLinkText": "可视化",
|
||||
"discover.fieldChooser.discoverField.addButtonAriaLabel": "将 {field} 添加到表中",
|
||||
"discover.fieldChooser.discoverField.addFieldTooltip": "将字段添加为列",
|
||||
|
|
|
@ -77,7 +77,7 @@ export default function ({ getService, getPageObjects }) {
|
|||
});
|
||||
const rowData = await PageObjects.discover.getDocTableIndex(1);
|
||||
expect(rowData).to.be(
|
||||
'name:ABC Company region:EAST _id:doc1 _type: - _index:dlstest _score:0'
|
||||
'_id:doc1 _type: - _index:dlstest _score:0 region.keyword:EAST name:ABC Company name.keyword:ABC Company region:EAST'
|
||||
);
|
||||
});
|
||||
after('logout', async () => {
|
||||
|
|
|
@ -112,7 +112,7 @@ export default function ({ getService, getPageObjects }) {
|
|||
});
|
||||
const rowData = await PageObjects.discover.getDocTableIndex(1);
|
||||
expect(rowData).to.be(
|
||||
'customer_ssn:444.555.6666 customer_name:ABC Company customer_region:WEST _id:2 _type: - _index:flstest _score:0'
|
||||
'_id:2 _type: - _index:flstest _score:0 customer_name.keyword:ABC Company customer_ssn:444.555.6666 customer_region.keyword:WEST runtime_customer_ssn:444.555.6666 calculated at runtime customer_region:WEST customer_name:ABC Company customer_ssn.keyword:444.555.6666'
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -126,7 +126,7 @@ export default function ({ getService, getPageObjects }) {
|
|||
});
|
||||
const rowData = await PageObjects.discover.getDocTableIndex(1);
|
||||
expect(rowData).to.be(
|
||||
'customer_name:ABC Company customer_region:WEST _id:2 _type: - _index:flstest _score:0'
|
||||
'_id:2 _type: - _index:flstest _score:0 customer_name.keyword:ABC Company customer_region.keyword:WEST customer_region:WEST customer_name:ABC Company'
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -7,7 +7,8 @@
|
|||
"runtime_customer_ssn": {
|
||||
"type": "keyword",
|
||||
"script": {
|
||||
"source": "emit(doc['customer_ssn'].value + ' calculated at runtime')"
|
||||
"lang": "painless",
|
||||
"source": "if (doc['customer_ssn'].size() !== 0) { return emit(doc['customer_ssn'].value + ' calculated at runtime') }"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -37,7 +38,8 @@
|
|||
"type": "keyword"
|
||||
}
|
||||
},
|
||||
"type": "text"
|
||||
"type": "text",
|
||||
"fielddata": true
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue