[UnifiedFieldList] Migrate field list components from Lens to UnifiedFieldList (#142758)

* [UnifiedFieldList] Extract FieldsAccordion component from Lens

* [UnifiedFieldList] Extract FieldList component from Lens

* [CI] Auto-commit changed files from 'node scripts/build_plugin_list_docs'

* [UnifiedFieldList] Rename component

* [UnifiedFieldList] Start extracting logic for fetching fields existence info

* [UnifiedFieldList] Start extracting logic for fetching fields existence info

* [UnifiedFieldList] Fix special and runtime fields

* [UnifiedFieldList] Start extracting logic for fetching fields existence info

* [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix'

* [UnifiedFieldList] Make API stricter

* [UnifiedFieldList] Make sure that key is specified for items

* [UnifiedFieldList] Fetch data for all active data views

* [UnifiedFieldList] Refactor some other occurances

* [UnifiedFieldList] Update more tests

* [UnifiedFieldList] Fix some checks

* [UnifiedFieldList] Some progress on updating tests

* [UnifiedFieldList] Update more tests

* [UnifiedFieldList] Skip redundant request's results

* [UnifiedFieldList] Update more tests

* [UnifiedFieldList] Improve tests

* [UnifiedFieldList] Improve tests

* [UnifiedFieldList] Improve tests

* [UnifiedFieldList] Move grouping into a customizable hook

* [UnifiedFieldList] Fix after the merge

* [UnifiedFieldList] Fix checks

* Revert "[UnifiedFieldList] Fix after the merge"

This reverts commit 500db7ed89.

* [UnifiedFieldList] Handle merge better

* [UnifiedFieldList] Update the naming

* [UnifiedFieldList] Support Selected fields

* [UnifiedFieldList] Update tests

* [UnifiedFieldList] Fix grouping

* [UnifiedFieldList] Update more tests

* [UnifiedFieldList] Fix refetch after adding a field

* [UnifiedFieldList] Load es query builder in async way

* [UnifiedFieldList] Fix a bug in case of renaming a field

* [UnifiedFieldList] Small refactoring

* [UnifiedFieldList] Refactor text based view

* [UnifiedFieldList] Better types support

* [UnifiedFieldList] Simplify props

* [UnifiedFieldList] Fix types

* [UnifiedFieldList] Async loading for FieldListGrouped code

* [UnifiedFieldList] Add more tests

* [UnifiedFieldList] Add more tests

* [UnifiedFieldList] Add more tests

* [UnifiedFieldList] Add more tests

* [UnifiedFieldList] Add more tests

* [UnifiedFieldList] Add more tests

* [UnifiedFieldList] Add more tests

* [UnifiedFieldList] Add docs

* [UnifiedFieldList] Clean up

* [UnifiedFieldList] Fix onNoData callback

* [UnifiedFieldList] Address PR comments

* [UnifiedFieldList] Address PR comments

* [UnifiedFieldList] Support a custom data-test-subj

* [UnifiedFieldList] Fix concurrency handling logic

* [UnifiedFieldList] Remove a generic tooltip message. Lens and Discover will have their own tooltips.

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Stratoula Kalafateli <efstratia.kalafateli@elastic.co>
This commit is contained in:
Julia Rechkunova 2022-11-02 13:18:21 +01:00 committed by GitHub
parent 096d61c1f1
commit ca1c58d3df
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
69 changed files with 3266 additions and 2077 deletions

View file

@ -5,22 +5,10 @@
* 2.0.
*/
import { DataViewsContract, DataViewSpec, FieldSpec } from '@kbn/data-views-plugin/public';
import { IndexPattern, IndexPatternField } from '../types';
import {
ensureIndexPattern,
loadIndexPatternRefs,
loadIndexPatterns,
syncExistingFields,
} from './loader';
import { DataViewsContract } from '@kbn/data-views-plugin/public';
import { ensureIndexPattern, loadIndexPatternRefs, loadIndexPatterns } from './loader';
import { sampleIndexPatterns, mockDataViewsService } from './mocks';
import { documentField } from '../datasources/form_based/document_field';
import { coreMock } from '@kbn/core/public/mocks';
import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks';
import type { DataView } from '@kbn/data-views-plugin/public';
import { dataPluginMock } from '@kbn/data-plugin/public/mocks';
import { UI_SETTINGS } from '@kbn/data-plugin/public';
import { createHttpFetchError } from '@kbn/core-http-browser-mocks';
describe('loader', () => {
describe('loadIndexPatternRefs', () => {
@ -266,218 +254,4 @@ describe('loader', () => {
expect(onError).not.toHaveBeenCalled();
});
});
describe('syncExistingFields', () => {
const core = coreMock.createStart();
const dataViews = dataViewPluginMocks.createStartContract();
const data = dataPluginMock.createStartContract();
const dslQuery = {
bool: {
must: [],
filter: [{ match_all: {} }],
should: [],
must_not: [],
},
};
function getIndexPatternList() {
return [
{
id: '1',
title: '1',
fields: [{ name: 'ip1_field_1' }, { name: 'ip1_field_2' }],
hasRestrictions: false,
},
{
id: '2',
title: '2',
fields: [{ name: 'ip2_field_1' }, { name: 'ip2_field_2' }],
hasRestrictions: false,
},
{
id: '3',
title: '3',
fields: [{ name: 'ip3_field_1' }, { name: 'ip3_field_2' }],
hasRestrictions: false,
},
] as unknown as IndexPattern[];
}
beforeEach(() => {
core.uiSettings.get.mockImplementation((key: string) => {
if (key === UI_SETTINGS.META_FIELDS) {
return [];
}
});
dataViews.get.mockImplementation((id: string) =>
Promise.resolve(
getIndexPatternList().find(
(indexPattern) => indexPattern.id === id
) as unknown as DataView
)
);
});
it('should call once for each index pattern', async () => {
const updateIndexPatterns = jest.fn();
dataViews.getFieldsForIndexPattern.mockImplementation(
(dataView: DataViewSpec | DataView) =>
Promise.resolve(dataView.fields) as Promise<FieldSpec[]>
);
await syncExistingFields({
dateRange: { fromDate: '1900-01-01', toDate: '2000-01-01' },
indexPatternList: getIndexPatternList(),
updateIndexPatterns,
dslQuery,
onNoData: jest.fn(),
currentIndexPatternTitle: 'abc',
isFirstExistenceFetch: false,
existingFields: {},
core,
data,
dataViews,
});
expect(dataViews.get).toHaveBeenCalledTimes(3);
expect(dataViews.getFieldsForIndexPattern).toHaveBeenCalledTimes(3);
expect(updateIndexPatterns).toHaveBeenCalledTimes(1);
const [newState, options] = updateIndexPatterns.mock.calls[0];
expect(options).toEqual({ applyImmediately: true });
expect(newState).toEqual({
isFirstExistenceFetch: false,
existingFields: {
'1': { ip1_field_1: true, ip1_field_2: true },
'2': { ip2_field_1: true, ip2_field_2: true },
'3': { ip3_field_1: true, ip3_field_2: true },
},
});
});
it('should call onNoData callback if current index pattern returns no fields', async () => {
const updateIndexPatterns = jest.fn();
const onNoData = jest.fn();
dataViews.getFieldsForIndexPattern.mockImplementation(
async (dataView: DataViewSpec | DataView) => {
return (dataView.title === '1'
? [{ name: `${dataView.title}_field_1` }, { name: `${dataView.title}_field_2` }]
: []) as unknown as Promise<FieldSpec[]>;
}
);
const args = {
dateRange: { fromDate: '1900-01-01', toDate: '2000-01-01' },
indexPatternList: getIndexPatternList(),
updateIndexPatterns,
dslQuery,
onNoData,
currentIndexPatternTitle: 'abc',
isFirstExistenceFetch: false,
existingFields: {},
core,
data,
dataViews,
};
await syncExistingFields(args);
expect(onNoData).not.toHaveBeenCalled();
await syncExistingFields({ ...args, isFirstExistenceFetch: true });
expect(onNoData).not.toHaveBeenCalled();
});
it('should set all fields to available and existence error flag if the request fails', async () => {
const updateIndexPatterns = jest.fn();
dataViews.getFieldsForIndexPattern.mockImplementation(() => {
return new Promise((_, reject) => {
reject(new Error());
});
});
const args = {
dateRange: { fromDate: '1900-01-01', toDate: '2000-01-01' },
indexPatternList: [
{
id: '1',
title: '1',
hasRestrictions: false,
fields: [{ name: 'field1' }, { name: 'field2' }] as IndexPatternField[],
},
] as IndexPattern[],
updateIndexPatterns,
dslQuery,
onNoData: jest.fn(),
currentIndexPatternTitle: 'abc',
isFirstExistenceFetch: false,
existingFields: {},
core,
data,
dataViews,
};
await syncExistingFields(args);
const [newState, options] = updateIndexPatterns.mock.calls[0];
expect(options).toEqual({ applyImmediately: true });
expect(newState.existenceFetchFailed).toEqual(true);
expect(newState.existenceFetchTimeout).toEqual(false);
expect(newState.existingFields['1']).toEqual({
field1: true,
field2: true,
});
});
it('should set all fields to available and existence error flag if the request times out', async () => {
const updateIndexPatterns = jest.fn();
dataViews.getFieldsForIndexPattern.mockImplementation(() => {
return new Promise((_, reject) => {
const error = createHttpFetchError(
'timeout',
'error',
{} as Request,
{ status: 408 } as Response
);
reject(error);
});
});
const args = {
dateRange: { fromDate: '1900-01-01', toDate: '2000-01-01' },
indexPatternList: [
{
id: '1',
title: '1',
hasRestrictions: false,
fields: [{ name: 'field1' }, { name: 'field2' }] as IndexPatternField[],
},
] as IndexPattern[],
updateIndexPatterns,
dslQuery,
onNoData: jest.fn(),
currentIndexPatternTitle: 'abc',
isFirstExistenceFetch: false,
existingFields: {},
core,
data,
dataViews,
};
await syncExistingFields(args);
const [newState, options] = updateIndexPatterns.mock.calls[0];
expect(options).toEqual({ applyImmediately: true });
expect(newState.existenceFetchFailed).toEqual(false);
expect(newState.existenceFetchTimeout).toEqual(true);
expect(newState.existingFields['1']).toEqual({
field1: true,
field2: true,
});
});
});
});

View file

@ -8,13 +8,8 @@
import { isNestedField } from '@kbn/data-views-plugin/common';
import type { DataViewsContract, DataView, DataViewSpec } from '@kbn/data-views-plugin/public';
import { keyBy } from 'lodash';
import { CoreStart } from '@kbn/core/public';
import { DataPublicPluginStart } from '@kbn/data-plugin/public';
import { loadFieldExisting } from '@kbn/unified-field-list-plugin/public';
import { IndexPattern, IndexPatternField, IndexPatternMap, IndexPatternRef } from '../types';
import { documentField } from '../datasources/form_based/document_field';
import { DateRange } from '../../common';
import { DataViewsState } from '../state_management';
type ErrorHandler = (err: Error) => void;
type MinimalDataViewsContract = Pick<DataViewsContract, 'get' | 'getIdsWithTitle' | 'create'>;
@ -247,120 +242,3 @@ export async function ensureIndexPattern({
};
return newIndexPatterns;
}
async function refreshExistingFields({
dateRange,
indexPatternList,
dslQuery,
core,
data,
dataViews,
}: {
dateRange: DateRange;
indexPatternList: IndexPattern[];
dslQuery: object;
core: Pick<CoreStart, 'http' | 'notifications' | 'uiSettings'>;
data: DataPublicPluginStart;
dataViews: DataViewsContract;
}) {
try {
const emptinessInfo = await Promise.all(
indexPatternList.map(async (pattern) => {
if (pattern.hasRestrictions) {
return {
indexPatternTitle: pattern.title,
existingFieldNames: pattern.fields.map((field) => field.name),
};
}
const dataView = await dataViews.get(pattern.id);
return await loadFieldExisting({
dslQuery,
fromDate: dateRange.fromDate,
toDate: dateRange.toDate,
timeFieldName: pattern.timeFieldName,
data,
uiSettingsClient: core.uiSettings,
dataViewsService: dataViews,
dataView,
});
})
);
return { result: emptinessInfo, status: 200 };
} catch (e) {
return { result: undefined, status: e.res?.status as number };
}
}
type FieldsPropsFromDataViewsState = Pick<
DataViewsState,
'existingFields' | 'isFirstExistenceFetch' | 'existenceFetchTimeout' | 'existenceFetchFailed'
>;
export async function syncExistingFields({
updateIndexPatterns,
isFirstExistenceFetch,
currentIndexPatternTitle,
onNoData,
existingFields,
...requestOptions
}: {
dateRange: DateRange;
indexPatternList: IndexPattern[];
existingFields: Record<string, Record<string, boolean>>;
updateIndexPatterns: (
newFieldState: FieldsPropsFromDataViewsState,
options: { applyImmediately: boolean }
) => void;
isFirstExistenceFetch: boolean;
currentIndexPatternTitle: string;
dslQuery: object;
onNoData?: () => void;
core: Pick<CoreStart, 'http' | 'notifications' | 'uiSettings'>;
data: DataPublicPluginStart;
dataViews: DataViewsContract;
}) {
const { indexPatternList } = requestOptions;
const newExistingFields = { ...existingFields };
const { result, status } = await refreshExistingFields(requestOptions);
if (result) {
if (isFirstExistenceFetch) {
const fieldsCurrentIndexPattern = result.find(
(info) => info.indexPatternTitle === currentIndexPatternTitle
);
if (fieldsCurrentIndexPattern && fieldsCurrentIndexPattern.existingFieldNames.length === 0) {
onNoData?.();
}
}
for (const { indexPatternTitle, existingFieldNames } of result) {
newExistingFields[indexPatternTitle] = booleanMap(existingFieldNames);
}
} else {
for (const { title, fields } of indexPatternList) {
newExistingFields[title] = booleanMap(fields.map((field) => field.name));
}
}
updateIndexPatterns(
{
existingFields: newExistingFields,
...(result
? { isFirstExistenceFetch: status !== 200 }
: {
isFirstExistenceFetch,
existenceFetchFailed: status !== 408,
existenceFetchTimeout: status === 408,
}),
},
{ applyImmediately: true }
);
}
function booleanMap(keys: string[]) {
return keys.reduce((acc, key) => {
acc[key] = true;
return acc;
}, {} as Record<string, boolean>);
}

View file

@ -12,7 +12,7 @@ import {
createMockedRestrictedIndexPattern,
} from '../datasources/form_based/mocks';
import { DataViewsState } from '../state_management';
import { ExistingFieldsMap, IndexPattern } from '../types';
import { IndexPattern } from '../types';
import { getFieldByNameFactory } from './loader';
/**
@ -22,25 +22,13 @@ import { getFieldByNameFactory } from './loader';
export const createMockDataViewsState = ({
indexPatterns,
indexPatternRefs,
isFirstExistenceFetch,
existingFields,
}: Partial<DataViewsState> = {}): DataViewsState => {
const refs =
indexPatternRefs ??
Object.values(indexPatterns ?? {}).map(({ id, title, name }) => ({ id, title, name }));
const allFields =
existingFields ??
refs.reduce((acc, { id, title }) => {
if (indexPatterns && id in indexPatterns) {
acc[title] = Object.fromEntries(indexPatterns[id].fields.map((f) => [f.displayName, true]));
}
return acc;
}, {} as ExistingFieldsMap);
return {
indexPatterns: indexPatterns ?? {},
indexPatternRefs: refs,
isFirstExistenceFetch: Boolean(isFirstExistenceFetch),
existingFields: allFields,
};
};

View file

@ -14,14 +14,8 @@ import {
UPDATE_FILTER_REFERENCES_ACTION,
UPDATE_FILTER_REFERENCES_TRIGGER,
} from '@kbn/unified-search-plugin/public';
import type { DateRange } from '../../common';
import type { IndexPattern, IndexPatternMap, IndexPatternRef } from '../types';
import {
ensureIndexPattern,
loadIndexPatternRefs,
loadIndexPatterns,
syncExistingFields,
} from './loader';
import { ensureIndexPattern, loadIndexPatternRefs, loadIndexPatterns } from './loader';
import type { DataViewsState } from '../state_management';
import { generateId } from '../id_generator';
@ -71,18 +65,6 @@ export interface IndexPatternServiceAPI {
id: string;
cache: IndexPatternMap;
}) => Promise<IndexPatternMap | undefined>;
/**
* Loads the existingFields map given the current context
*/
refreshExistingFields: (args: {
dateRange: DateRange;
currentIndexPatternTitle: string;
dslQuery: object;
onNoData?: () => void;
existingFields: Record<string, Record<string, boolean>>;
indexPatternList: IndexPattern[];
isFirstExistenceFetch: boolean;
}) => Promise<void>;
replaceDataViewId: (newDataView: DataView) => Promise<void>;
/**
@ -150,14 +132,6 @@ export function createIndexPatternService({
},
ensureIndexPattern: (args) =>
ensureIndexPattern({ onError: onChangeError, dataViews, ...args }),
refreshExistingFields: (args) =>
syncExistingFields({
updateIndexPatterns,
...args,
data,
dataViews,
core,
}),
loadIndexPatternRefs: async ({ isFullEditor }) =>
isFullEditor ? loadIndexPatternRefs(dataViews) : [],
getDefaultIndex: () => core.uiSettings.get('defaultIndex'),

View file

@ -28,11 +28,9 @@ export function loadInitialDataViews() {
const restricted = createMockedRestrictedIndexPattern();
return {
indexPatternRefs: [],
existingFields: {},
indexPatterns: {
[indexPattern.id]: indexPattern,
[restricted.id]: restricted,
},
isFirstExistenceFetch: false,
};
}

View file

@ -14,15 +14,6 @@
margin-bottom: $euiSizeS;
}
.lnsInnerIndexPatternDataPanel__titleTooltip {
margin-right: $euiSizeXS;
}
.lnsInnerIndexPatternDataPanel__fieldItems {
// Quick fix for making sure the shadow and focus rings are visible outside the accordion bounds
padding: $euiSizeXS;
}
.lnsInnerIndexPatternDataPanel__textField {
@include euiFormControlLayoutPadding(1, 'right');
@include euiFormControlLayoutPadding(1, 'left');
@ -60,4 +51,4 @@
.lnsFilterButton .euiFilterButton__textShift {
min-width: 0;
}
}

View file

@ -6,32 +6,38 @@
*/
import './datapanel.scss';
import { uniq, groupBy } from 'lodash';
import React, { useState, memo, useCallback, useMemo, useRef, useEffect } from 'react';
import { uniq } from 'lodash';
import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import {
EuiCallOut,
EuiContextMenuItem,
EuiContextMenuPanel,
EuiFilterButton,
EuiFlexGroup,
EuiFlexItem,
EuiContextMenuPanel,
EuiContextMenuItem,
EuiPopover,
EuiCallOut,
EuiFormControlLayout,
EuiFilterButton,
EuiScreenReaderOnly,
EuiIcon,
EuiPopover,
EuiProgress,
htmlIdGenerator,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import type { EsQueryConfig, Query, Filter } from '@kbn/es-query';
import { FormattedMessage } from '@kbn/i18n-react';
import type { CoreStart } from '@kbn/core/public';
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
import { type DataView } from '@kbn/data-plugin/common';
import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
import { htmlIdGenerator } from '@elastic/eui';
import { buildEsQuery } from '@kbn/es-query';
import { getEsQueryConfig } from '@kbn/data-plugin/public';
import { IndexPatternFieldEditorStart } from '@kbn/data-view-field-editor-plugin/public';
import { VISUALIZE_GEO_FIELD_TRIGGER } from '@kbn/ui-actions-plugin/public';
import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
import {
FieldsGroupNames,
FieldListGrouped,
type FieldListGroupedProps,
useExistingFieldsFetcher,
useGroupedFields,
useExistingFieldsReader,
} from '@kbn/unified-field-list-plugin/public';
import { ChartsPluginSetup } from '@kbn/charts-plugin/public';
import type {
DatasourceDataPanelProps,
@ -42,12 +48,11 @@ import type {
} from '../../types';
import { ChildDragDropProvider, DragContextState } from '../../drag_drop';
import type { FormBasedPrivateState } from './types';
import { Loader } from '../../loader';
import { LensFieldIcon } from '../../shared_components/field_picker/lens_field_icon';
import { getFieldType } from './pure_utils';
import { FieldGroups, FieldList } from './field_list';
import { fieldContainsData, fieldExists } from '../../shared_components';
import { fieldContainsData } from '../../shared_components';
import { IndexPatternServiceAPI } from '../../data_views_service/service';
import { FieldItem } from './field_item';
export type Props = Omit<
DatasourceDataPanelProps<FormBasedPrivateState>,
@ -65,10 +70,6 @@ export type Props = Omit<
layerFields?: string[];
};
function sortFields(fieldA: IndexPatternField, fieldB: IndexPatternField) {
return fieldA.displayName.localeCompare(fieldB.displayName, undefined, { sensitivity: 'base' });
}
const supportedFieldTypes = new Set([
'string',
'number',
@ -104,25 +105,8 @@ const fieldTypeNames: Record<DataType, string> = {
murmur3: i18n.translate('xpack.lens.datatypes.murmur3', { defaultMessage: 'murmur3' }),
};
// Wrapper around buildEsQuery, handling errors (e.g. because a query can't be parsed) by
// returning a query dsl object not matching anything
function buildSafeEsQuery(
indexPattern: IndexPattern,
query: Query,
filters: Filter[],
queryConfig: EsQueryConfig
) {
try {
return buildEsQuery(indexPattern, query, filters, queryConfig);
} catch (e) {
return {
bool: {
must_not: {
match_all: {},
},
},
};
}
function onSupportedFieldFilter(field: IndexPatternField): boolean {
return supportedFieldTypes.has(field.type);
}
export function FormBasedDataPanel({
@ -147,51 +131,22 @@ export function FormBasedDataPanel({
usedIndexPatterns,
layerFields,
}: Props) {
const { indexPatterns, indexPatternRefs, existingFields, isFirstExistenceFetch } =
frame.dataViews;
const { indexPatterns, indexPatternRefs } = frame.dataViews;
const { currentIndexPatternId } = state;
const indexPatternList = uniq(
(
usedIndexPatterns ?? Object.values(state.layers).map(({ indexPatternId }) => indexPatternId)
).concat(currentIndexPatternId)
)
.filter((id) => !!indexPatterns[id])
.sort()
.map((id) => indexPatterns[id]);
const dslQuery = buildSafeEsQuery(
indexPatterns[currentIndexPatternId],
query,
filters,
getEsQueryConfig(core.uiSettings)
);
const activeIndexPatterns = useMemo(() => {
return uniq(
(
usedIndexPatterns ?? Object.values(state.layers).map(({ indexPatternId }) => indexPatternId)
).concat(currentIndexPatternId)
)
.filter((id) => !!indexPatterns[id])
.sort()
.map((id) => indexPatterns[id]);
}, [usedIndexPatterns, indexPatterns, state.layers, currentIndexPatternId]);
return (
<>
<Loader
load={() =>
indexPatternService.refreshExistingFields({
dateRange,
currentIndexPatternTitle: indexPatterns[currentIndexPatternId]?.title || '',
onNoData: showNoDataPopover,
dslQuery,
indexPatternList,
isFirstExistenceFetch,
existingFields,
})
}
loadDeps={[
query,
filters,
dateRange.fromDate,
dateRange.toDate,
indexPatternList.map((x) => `${x.title}:${x.timeFieldName}`).join(','),
// important here to rerun the fields existence on indexPattern change (i.e. add new fields in place)
frame.dataViews.indexPatterns,
]}
/>
{Object.keys(indexPatterns).length === 0 && indexPatternRefs.length === 0 ? (
<EuiFlexGroup
gutterSize="m"
@ -237,6 +192,8 @@ export function FormBasedDataPanel({
onIndexPatternRefresh={onIndexPatternRefresh}
frame={frame}
layerFields={layerFields}
showNoDataPopover={showNoDataPopover}
activeIndexPatterns={activeIndexPatterns}
/>
)}
</>
@ -252,18 +209,6 @@ interface DataPanelState {
isMetaAccordionOpen: boolean;
}
const defaultFieldGroups: {
specialFields: IndexPatternField[];
availableFields: IndexPatternField[];
emptyFields: IndexPatternField[];
metaFields: IndexPatternField[];
} = {
specialFields: [],
availableFields: [],
emptyFields: [],
metaFields: [],
};
const htmlId = htmlIdGenerator('datapanel');
const fieldSearchDescriptionId = htmlId();
@ -286,9 +231,11 @@ export const InnerFormBasedDataPanel = function InnerFormBasedDataPanel({
frame,
onIndexPatternRefresh,
layerFields,
showNoDataPopover,
activeIndexPatterns,
}: Omit<
DatasourceDataPanelProps,
'state' | 'setState' | 'showNoDataPopover' | 'core' | 'onChangeIndexPattern' | 'usedIndexPatterns'
'state' | 'setState' | 'core' | 'onChangeIndexPattern' | 'usedIndexPatterns'
> & {
data: DataPublicPluginStart;
dataViews: DataViewsPublicPluginStart;
@ -301,6 +248,7 @@ export const InnerFormBasedDataPanel = function InnerFormBasedDataPanel({
indexPatternFieldEditor: IndexPatternFieldEditorStart;
onIndexPatternRefresh: () => void;
layerFields?: string[];
activeIndexPatterns: IndexPattern[];
}) {
const [localState, setLocalState] = useState<DataPanelState>({
nameFilter: '',
@ -310,10 +258,30 @@ export const InnerFormBasedDataPanel = function InnerFormBasedDataPanel({
isEmptyAccordionOpen: false,
isMetaAccordionOpen: false,
});
const { existenceFetchFailed, existenceFetchTimeout, indexPatterns, existingFields } =
frame.dataViews;
const { indexPatterns } = frame.dataViews;
const currentIndexPattern = indexPatterns[currentIndexPatternId];
const existingFieldsForIndexPattern = existingFields[currentIndexPattern?.title];
const { refetchFieldsExistenceInfo, isProcessing } = useExistingFieldsFetcher({
dataViews: activeIndexPatterns as unknown as DataView[],
query,
filters,
fromDate: dateRange.fromDate,
toDate: dateRange.toDate,
services: {
data,
dataViews,
core,
},
onNoData: (dataViewId) => {
if (dataViewId === currentIndexPatternId) {
showNoDataPopover();
}
},
});
const fieldsExistenceReader = useExistingFieldsReader();
const fieldsExistenceStatus =
fieldsExistenceReader.getFieldsExistenceStatus(currentIndexPatternId);
const visualizeGeoFieldTrigger = uiActions.getTrigger(VISUALIZE_GEO_FIELD_TRIGGER);
const allFields = useMemo(() => {
if (!currentIndexPattern) return [];
@ -331,186 +299,73 @@ export const InnerFormBasedDataPanel = function InnerFormBasedDataPanel({
...localState.typeFilter,
]);
const fieldInfoUnavailable =
existenceFetchFailed || existenceFetchTimeout || currentIndexPattern?.hasRestrictions;
const editPermission =
indexPatternFieldEditor.userPermissions.editIndexPattern() || !currentIndexPattern.isPersisted;
const unfilteredFieldGroups: FieldGroups = useMemo(() => {
const containsData = (field: IndexPatternField) => {
const overallField = currentIndexPattern?.getFieldByName(field.name);
return (
overallField &&
existingFieldsForIndexPattern &&
fieldExists(existingFieldsForIndexPattern, overallField.name)
);
};
const onSelectedFieldFilter = useCallback(
(field: IndexPatternField): boolean => {
return Boolean(layerFields?.includes(field.name));
},
[layerFields]
);
const allSupportedTypesFields = allFields.filter((field) =>
supportedFieldTypes.has(field.type)
);
const usedByLayersFields = allFields.filter((field) => layerFields?.includes(field.name));
const sorted = allSupportedTypesFields.sort(sortFields);
const groupedFields = {
...defaultFieldGroups,
...groupBy(sorted, (field) => {
if (field.type === 'document') {
return 'specialFields';
} else if (field.meta) {
return 'metaFields';
} else if (containsData(field)) {
return 'availableFields';
} else return 'emptyFields';
}),
};
const onFilterField = useCallback(
(field: IndexPatternField) => {
if (
localState.nameFilter.length &&
!field.name.toLowerCase().includes(localState.nameFilter.toLowerCase()) &&
!field.displayName.toLowerCase().includes(localState.nameFilter.toLowerCase())
) {
return false;
}
if (localState.typeFilter.length > 0) {
return localState.typeFilter.includes(getFieldType(field) as DataType);
}
return true;
},
[localState]
);
const isUsingSampling = core.uiSettings.get('lens:useFieldExistenceSampling');
const hasFilters = Boolean(filters.length);
const onOverrideFieldGroupDetails = useCallback(
(groupName) => {
if (groupName === FieldsGroupNames.AvailableFields) {
const isUsingSampling = core.uiSettings.get('lens:useFieldExistenceSampling');
const fieldGroupDefinitions: FieldGroups = {
SpecialFields: {
fields: groupedFields.specialFields,
fieldCount: 1,
isAffectedByGlobalFilter: false,
isAffectedByTimeFilter: false,
isInitiallyOpen: false,
showInAccordion: false,
title: '',
hideDetails: true,
},
SelectedFields: {
fields: usedByLayersFields,
fieldCount: usedByLayersFields.length,
isInitiallyOpen: true,
showInAccordion: true,
title: i18n.translate('xpack.lens.indexPattern.selectedFieldsLabel', {
defaultMessage: 'Selected fields',
}),
isAffectedByGlobalFilter: !!filters.length,
isAffectedByTimeFilter: true,
hideDetails: false,
hideIfEmpty: true,
},
AvailableFields: {
fields: groupedFields.availableFields,
fieldCount: groupedFields.availableFields.length,
isInitiallyOpen: true,
showInAccordion: true,
title: fieldInfoUnavailable
? i18n.translate('xpack.lens.indexPattern.allFieldsLabel', {
defaultMessage: 'All fields',
})
: i18n.translate('xpack.lens.indexPattern.availableFieldsLabel', {
defaultMessage: 'Available fields',
}),
helpText: isUsingSampling
? i18n.translate('xpack.lens.indexPattern.allFieldsSamplingLabelHelp', {
defaultMessage:
'Available fields contain the data in the first 500 documents that match your filters. To view all fields, expand Empty fields. You are unable to create visualizations with full text, geographic, flattened, and object fields.',
})
: i18n.translate('xpack.lens.indexPattern.allFieldsLabelHelp', {
defaultMessage:
'Drag and drop available fields to the workspace and create visualizations. To change the available fields, select a different data view, edit your queries, or use a different time range. Some field types cannot be visualized in Lens, including full text and geographic fields.',
}),
isAffectedByGlobalFilter: !!filters.length,
isAffectedByTimeFilter: true,
// Show details on timeout but not failure
hideDetails: fieldInfoUnavailable && !existenceFetchTimeout,
defaultNoFieldsMessage: i18n.translate('xpack.lens.indexPatterns.noAvailableDataLabel', {
defaultMessage: `There are no available fields that contain data.`,
}),
},
EmptyFields: {
fields: groupedFields.emptyFields,
fieldCount: groupedFields.emptyFields.length,
isAffectedByGlobalFilter: false,
isAffectedByTimeFilter: false,
isInitiallyOpen: false,
showInAccordion: true,
hideDetails: false,
title: i18n.translate('xpack.lens.indexPattern.emptyFieldsLabel', {
defaultMessage: 'Empty fields',
}),
defaultNoFieldsMessage: i18n.translate('xpack.lens.indexPatterns.noEmptyDataLabel', {
defaultMessage: `There are no empty fields.`,
}),
helpText: i18n.translate('xpack.lens.indexPattern.emptyFieldsLabelHelp', {
defaultMessage:
'Empty fields did not contain any values in the first 500 documents based on your filters.',
}),
},
MetaFields: {
fields: groupedFields.metaFields,
fieldCount: groupedFields.metaFields.length,
isAffectedByGlobalFilter: false,
isAffectedByTimeFilter: false,
isInitiallyOpen: false,
showInAccordion: true,
hideDetails: false,
title: i18n.translate('xpack.lens.indexPattern.metaFieldsLabel', {
defaultMessage: 'Meta fields',
}),
defaultNoFieldsMessage: i18n.translate('xpack.lens.indexPatterns.noMetaDataLabel', {
defaultMessage: `There are no meta fields.`,
}),
},
};
return {
helpText: isUsingSampling
? i18n.translate('xpack.lens.indexPattern.allFieldsSamplingLabelHelp', {
defaultMessage:
'Available fields contain the data in the first 500 documents that match your filters. To view all fields, expand Empty fields. You are unable to create visualizations with full text, geographic, flattened, and object fields.',
})
: i18n.translate('xpack.lens.indexPattern.allFieldsLabelHelp', {
defaultMessage:
'Drag and drop available fields to the workspace and create visualizations. To change the available fields, select a different data view, edit your queries, or use a different time range. Some field types cannot be visualized in Lens, including full text and geographic fields.',
}),
isAffectedByGlobalFilter: hasFilters,
};
}
if (groupName === FieldsGroupNames.SelectedFields) {
return {
isAffectedByGlobalFilter: hasFilters,
};
}
},
[core.uiSettings, hasFilters]
);
// do not show empty field accordion if there is no existence information
if (fieldInfoUnavailable) {
delete fieldGroupDefinitions.EmptyFields;
}
return fieldGroupDefinitions;
}, [
const { fieldGroups } = useGroupedFields<IndexPatternField>({
dataViewId: currentIndexPatternId,
allFields,
core.uiSettings,
fieldInfoUnavailable,
filters.length,
existenceFetchTimeout,
currentIndexPattern,
existingFieldsForIndexPattern,
layerFields,
]);
const fieldGroups: FieldGroups = useMemo(() => {
const filterFieldGroup = (fieldGroup: IndexPatternField[]) =>
fieldGroup.filter((field) => {
if (
localState.nameFilter.length &&
!field.name.toLowerCase().includes(localState.nameFilter.toLowerCase()) &&
!field.displayName.toLowerCase().includes(localState.nameFilter.toLowerCase())
) {
return false;
}
if (localState.typeFilter.length > 0) {
return localState.typeFilter.includes(getFieldType(field) as DataType);
}
return true;
});
return Object.fromEntries(
Object.entries(unfilteredFieldGroups).map(([name, group]) => [
name,
{ ...group, fields: filterFieldGroup(group.fields) },
])
);
}, [unfilteredFieldGroups, localState.nameFilter, localState.typeFilter]);
const checkFieldExists = useCallback(
(field: IndexPatternField) =>
fieldContainsData(field.name, currentIndexPattern, existingFieldsForIndexPattern),
[currentIndexPattern, existingFieldsForIndexPattern]
);
const { nameFilter, typeFilter } = localState;
const filter = useMemo(
() => ({
nameFilter,
typeFilter,
}),
[nameFilter, typeFilter]
);
services: {
dataViews,
},
fieldsExistenceReader,
onFilterField,
onSupportedFieldFilter,
onSelectedFieldFilter,
onOverrideFieldGroupDetails,
});
const closeFieldEditor = useRef<() => void | undefined>();
@ -560,6 +415,7 @@ export const InnerFormBasedDataPanel = function InnerFormBasedDataPanel({
onSave: () => {
if (indexPatternInstance.isPersisted()) {
refreshFieldList();
refetchFieldsExistenceInfo(indexPatternInstance.id);
} else {
indexPatternService.replaceDataViewId(indexPatternInstance);
}
@ -574,6 +430,7 @@ export const InnerFormBasedDataPanel = function InnerFormBasedDataPanel({
indexPatternFieldEditor,
refreshFieldList,
indexPatternService,
refetchFieldsExistenceInfo,
]
);
@ -590,6 +447,7 @@ export const InnerFormBasedDataPanel = function InnerFormBasedDataPanel({
onDelete: () => {
if (indexPatternInstance.isPersisted()) {
refreshFieldList();
refetchFieldsExistenceInfo(indexPatternInstance.id);
} else {
indexPatternService.replaceDataViewId(indexPatternInstance);
}
@ -604,24 +462,39 @@ export const InnerFormBasedDataPanel = function InnerFormBasedDataPanel({
indexPatternFieldEditor,
indexPatternService,
refreshFieldList,
refetchFieldsExistenceInfo,
]
);
const fieldProps = useMemo(
() => ({
core,
data,
fieldFormats,
indexPattern: currentIndexPattern,
highlight: localState.nameFilter.toLowerCase(),
dateRange,
query,
filters,
chartsThemeService: charts.theme,
}),
const renderFieldItem: FieldListGroupedProps<IndexPatternField>['renderFieldItem'] = useCallback(
({ field, itemIndex, groupIndex, hideDetails }) => (
<FieldItem
field={field}
exists={fieldContainsData(
field.name,
currentIndexPattern,
fieldsExistenceReader.hasFieldData
)}
hideDetails={hideDetails || field.type === 'document'}
itemIndex={itemIndex}
groupIndex={groupIndex}
dropOntoWorkspace={dropOntoWorkspace}
hasSuggestionForField={hasSuggestionForField}
editField={editField}
removeField={removeField}
uiActions={uiActions}
core={core}
fieldFormats={fieldFormats}
indexPattern={currentIndexPattern}
highlight={localState.nameFilter.toLowerCase()}
dateRange={dateRange}
query={query}
filters={filters}
chartsThemeService={charts.theme}
/>
),
[
core,
data,
fieldFormats,
currentIndexPattern,
dateRange,
@ -629,6 +502,12 @@ export const InnerFormBasedDataPanel = function InnerFormBasedDataPanel({
filters,
localState.nameFilter,
charts.theme,
fieldsExistenceReader.hasFieldData,
dropOntoWorkspace,
hasSuggestionForField,
editField,
removeField,
uiActions,
]
);
@ -640,6 +519,7 @@ export const InnerFormBasedDataPanel = function InnerFormBasedDataPanel({
direction="column"
responsive={false}
>
{isProcessing && <EuiProgress size="xs" color="accent" position="absolute" />}
<EuiFlexItem grow={false}>
<EuiFormControlLayout
icon="search"
@ -734,36 +614,14 @@ export const InnerFormBasedDataPanel = function InnerFormBasedDataPanel({
/>
</EuiFormControlLayout>
</EuiFlexItem>
<EuiScreenReaderOnly>
<div aria-live="polite" id={fieldSearchDescriptionId}>
{i18n.translate('xpack.lens.indexPatterns.fieldSearchLiveRegion', {
defaultMessage:
'{availableFields} available {availableFields, plural, one {field} other {fields}}. {emptyFields} empty {emptyFields, plural, one {field} other {fields}}. {metaFields} meta {metaFields, plural, one {field} other {fields}}.',
values: {
availableFields: fieldGroups.AvailableFields.fields.length,
// empty fields can be undefined if there is no existence information to be fetched
emptyFields: fieldGroups.EmptyFields?.fields.length || 0,
metaFields: fieldGroups.MetaFields.fields.length,
},
})}
</div>
</EuiScreenReaderOnly>
<EuiFlexItem>
<FieldList
exists={checkFieldExists}
fieldProps={fieldProps}
<FieldListGrouped<IndexPatternField>
fieldGroups={fieldGroups}
hasSyncedExistingFields={!!existingFieldsForIndexPattern}
filter={filter}
currentIndexPatternId={currentIndexPatternId}
existenceFetchFailed={existenceFetchFailed}
existenceFetchTimeout={existenceFetchTimeout}
existFieldsInIndex={!!allFields.length}
dropOntoWorkspace={dropOntoWorkspace}
hasSuggestionForField={hasSuggestionForField}
editField={editField}
removeField={removeField}
uiActions={uiActions}
fieldsExistenceStatus={fieldsExistenceStatus}
fieldsExistInIndex={!!allFields.length}
renderFieldItem={renderFieldItem}
screenReaderDescriptionForSearchInputId={fieldSearchDescriptionId}
data-test-subj="lnsIndexPattern"
/>
</EuiFlexItem>
</EuiFlexGroup>

View file

@ -606,7 +606,6 @@ export function DimensionEditor(props: DimensionEditorProps) {
setIsCloseable,
paramEditorCustomProps,
ReferenceEditor,
existingFields: props.existingFields,
...services,
};
@ -789,7 +788,6 @@ export function DimensionEditor(props: DimensionEditorProps) {
}}
validation={validation}
currentIndexPattern={currentIndexPattern}
existingFields={props.existingFields}
selectionStyle={selectedOperationDefinition.selectionStyle}
dateRange={dateRange}
labelAppend={selectedOperationDefinition?.getHelpMessage?.({
@ -815,7 +813,6 @@ export function DimensionEditor(props: DimensionEditorProps) {
selectedColumn={selectedColumn as FieldBasedIndexPatternColumn}
columnId={columnId}
indexPattern={currentIndexPattern}
existingFields={props.existingFields}
operationSupportMatrix={operationSupportMatrix}
updateLayer={(newLayer) => {
if (temporaryQuickFunction) {
@ -845,7 +842,6 @@ export function DimensionEditor(props: DimensionEditorProps) {
const customParamEditor = ParamEditor ? (
<>
<ParamEditor
existingFields={props.existingFields}
layer={state.layers[layerId]}
activeData={props.activeData}
paramEditorUpdater={

View file

@ -32,6 +32,7 @@ import {
CoreStart,
} from '@kbn/core/public';
import { IStorageWrapper } from '@kbn/kibana-utils-plugin/public';
import { useExistingFieldsReader } from '@kbn/unified-field-list-plugin/public/hooks/use_existing_fields';
import { generateId } from '../../../id_generator';
import { FormBasedPrivateState } from '../types';
import { LayerTypes } from '@kbn/expression-xy-plugin/public';
@ -78,6 +79,16 @@ jest.mock('../operations/definitions/formula/editor/formula_editor', () => {
};
});
jest.mock('@kbn/unified-field-list-plugin/public/hooks/use_existing_fields', () => ({
useExistingFieldsReader: jest.fn(() => {
return {
hasFieldData: (dataViewId: string, fieldName: string) => {
return ['timestamp', 'bytes', 'memory', 'source'].includes(fieldName);
},
};
}),
}));
const fields = [
{
name: 'timestamp',
@ -197,14 +208,6 @@ describe('FormBasedDimensionEditor', () => {
defaultProps = {
indexPatterns: expectedIndexPatterns,
existingFields: {
'my-fake-index-pattern': {
timestamp: true,
bytes: true,
memory: true,
source: true,
},
},
state,
setState,
dateRange: { fromDate: 'now-1d', toDate: 'now' },
@ -339,16 +342,15 @@ describe('FormBasedDimensionEditor', () => {
});
it('should hide fields that have no data', () => {
const props = {
...defaultProps,
existingFields: {
'my-fake-index-pattern': {
timestamp: true,
source: true,
(useExistingFieldsReader as jest.Mock).mockImplementationOnce(() => {
return {
hasFieldData: (dataViewId: string, fieldName: string) => {
return ['timestamp', 'source'].includes(fieldName);
},
},
};
wrapper = mount(<FormBasedDimensionEditorComponent {...props} />);
};
});
wrapper = mount(<FormBasedDimensionEditorComponent {...defaultProps} />);
const options = wrapper
.find(EuiComboBox)

View file

@ -112,11 +112,7 @@ function getLayer(col1: GenericIndexPatternColumn = getStringBasedOperationColum
},
};
}
function getDefaultOperationSupportMatrix(
layer: FormBasedLayer,
columnId: string,
existingFields: Record<string, Record<string, boolean>>
) {
function getDefaultOperationSupportMatrix(layer: FormBasedLayer, columnId: string) {
return getOperationSupportMatrix({
state: {
layers: { layer1: layer },
@ -130,29 +126,36 @@ function getDefaultOperationSupportMatrix(
});
}
function getExistingFields() {
const fields: Record<string, boolean> = {};
for (const field of defaultProps.indexPattern.fields) {
fields[field.name] = true;
}
return {
[defaultProps.indexPattern.title]: fields,
};
}
const mockedReader = {
hasFieldData: (dataViewId: string, fieldName: string) => {
if (defaultProps.indexPattern.id !== dataViewId) {
return false;
}
const map: Record<string, boolean> = {};
for (const field of defaultProps.indexPattern.fields) {
map[field.name] = true;
}
return map[fieldName];
},
};
jest.mock('@kbn/unified-field-list-plugin/public/hooks/use_existing_fields', () => ({
useExistingFieldsReader: jest.fn(() => mockedReader),
}));
describe('FieldInput', () => {
it('should render a field select box', () => {
const updateLayerSpy = jest.fn();
const layer = getLayer();
const existingFields = getExistingFields();
const operationSupportMatrix = getDefaultOperationSupportMatrix(layer, 'col1', existingFields);
const operationSupportMatrix = getDefaultOperationSupportMatrix(layer, 'col1');
const instance = mount(
<FieldInput
{...defaultProps}
layer={layer}
columnId={'col1'}
updateLayer={updateLayerSpy}
existingFields={existingFields}
operationSupportMatrix={operationSupportMatrix}
/>
);
@ -163,15 +166,13 @@ describe('FieldInput', () => {
it('should render an error message when incomplete operation is on', () => {
const updateLayerSpy = jest.fn();
const layer = getLayer();
const existingFields = getExistingFields();
const operationSupportMatrix = getDefaultOperationSupportMatrix(layer, 'col1', existingFields);
const operationSupportMatrix = getDefaultOperationSupportMatrix(layer, 'col1');
const instance = mount(
<FieldInput
{...defaultProps}
layer={layer}
columnId={'col1'}
updateLayer={updateLayerSpy}
existingFields={existingFields}
operationSupportMatrix={operationSupportMatrix}
incompleteOperation={'terms'}
selectedColumn={getStringBasedOperationColumn()}
@ -195,19 +196,13 @@ describe('FieldInput', () => {
(_, col: ReferenceBasedIndexPatternColumn) => {
const updateLayerSpy = jest.fn();
const layer = getLayer(col);
const existingFields = getExistingFields();
const operationSupportMatrix = getDefaultOperationSupportMatrix(
layer,
'col1',
existingFields
);
const operationSupportMatrix = getDefaultOperationSupportMatrix(layer, 'col1');
const instance = mount(
<FieldInput
{...defaultProps}
layer={layer}
columnId={'col1'}
updateLayer={updateLayerSpy}
existingFields={existingFields}
operationSupportMatrix={operationSupportMatrix}
incompleteOperation={'terms'}
/>
@ -234,19 +229,13 @@ describe('FieldInput', () => {
(_, col: ReferenceBasedIndexPatternColumn) => {
const updateLayerSpy = jest.fn();
const layer = getLayer(col);
const existingFields = getExistingFields();
const operationSupportMatrix = getDefaultOperationSupportMatrix(
layer,
'col1',
existingFields
);
const operationSupportMatrix = getDefaultOperationSupportMatrix(layer, 'col1');
const instance = mount(
<FieldInput
{...defaultProps}
layer={layer}
columnId={'col1'}
updateLayer={updateLayerSpy}
existingFields={existingFields}
operationSupportMatrix={operationSupportMatrix}
selectedColumn={getStringBasedOperationColumn()}
incompleteOperation={'terms'}
@ -269,15 +258,13 @@ describe('FieldInput', () => {
it('should render an error message for invalid fields', () => {
const updateLayerSpy = jest.fn();
const layer = getLayer();
const existingFields = getExistingFields();
const operationSupportMatrix = getDefaultOperationSupportMatrix(layer, 'col1', existingFields);
const operationSupportMatrix = getDefaultOperationSupportMatrix(layer, 'col1');
const instance = mount(
<FieldInput
{...defaultProps}
layer={layer}
columnId={'col1'}
updateLayer={updateLayerSpy}
existingFields={existingFields}
operationSupportMatrix={operationSupportMatrix}
currentFieldIsInvalid
/>
@ -295,15 +282,13 @@ describe('FieldInput', () => {
it('should render a help message when passed and no errors are found', () => {
const updateLayerSpy = jest.fn();
const layer = getLayer();
const existingFields = getExistingFields();
const operationSupportMatrix = getDefaultOperationSupportMatrix(layer, 'col1', existingFields);
const operationSupportMatrix = getDefaultOperationSupportMatrix(layer, 'col1');
const instance = mount(
<FieldInput
{...defaultProps}
layer={layer}
columnId={'col1'}
updateLayer={updateLayerSpy}
existingFields={existingFields}
operationSupportMatrix={operationSupportMatrix}
helpMessage={'My help message'}
/>
@ -320,15 +305,13 @@ describe('FieldInput', () => {
it('should prioritize errors over help messages', () => {
const updateLayerSpy = jest.fn();
const layer = getLayer();
const existingFields = getExistingFields();
const operationSupportMatrix = getDefaultOperationSupportMatrix(layer, 'col1', existingFields);
const operationSupportMatrix = getDefaultOperationSupportMatrix(layer, 'col1');
const instance = mount(
<FieldInput
{...defaultProps}
layer={layer}
columnId={'col1'}
updateLayer={updateLayerSpy}
existingFields={existingFields}
operationSupportMatrix={operationSupportMatrix}
currentFieldIsInvalid
helpMessage={'My help message'}
@ -346,15 +329,13 @@ describe('FieldInput', () => {
it('should update the layer on field selection', () => {
const updateLayerSpy = jest.fn();
const layer = getLayer();
const existingFields = getExistingFields();
const operationSupportMatrix = getDefaultOperationSupportMatrix(layer, 'col1', existingFields);
const operationSupportMatrix = getDefaultOperationSupportMatrix(layer, 'col1');
const instance = mount(
<FieldInput
{...defaultProps}
layer={layer}
columnId={'col1'}
updateLayer={updateLayerSpy}
existingFields={existingFields}
operationSupportMatrix={operationSupportMatrix}
selectedColumn={getStringBasedOperationColumn()}
/>
@ -372,15 +353,13 @@ describe('FieldInput', () => {
it('should not trigger when the same selected field is selected again', () => {
const updateLayerSpy = jest.fn();
const layer = getLayer();
const existingFields = getExistingFields();
const operationSupportMatrix = getDefaultOperationSupportMatrix(layer, 'col1', existingFields);
const operationSupportMatrix = getDefaultOperationSupportMatrix(layer, 'col1');
const instance = mount(
<FieldInput
{...defaultProps}
layer={layer}
columnId={'col1'}
updateLayer={updateLayerSpy}
existingFields={existingFields}
operationSupportMatrix={operationSupportMatrix}
selectedColumn={getStringBasedOperationColumn()}
/>
@ -398,15 +377,13 @@ describe('FieldInput', () => {
it('should prioritize incomplete fields over selected column field to display', () => {
const updateLayerSpy = jest.fn();
const layer = getLayer();
const existingFields = getExistingFields();
const operationSupportMatrix = getDefaultOperationSupportMatrix(layer, 'col1', existingFields);
const operationSupportMatrix = getDefaultOperationSupportMatrix(layer, 'col1');
const instance = mount(
<FieldInput
{...defaultProps}
layer={layer}
columnId={'col1'}
updateLayer={updateLayerSpy}
existingFields={existingFields}
operationSupportMatrix={operationSupportMatrix}
incompleteField={'dest'}
selectedColumn={getStringBasedOperationColumn()}
@ -425,15 +402,13 @@ describe('FieldInput', () => {
const updateLayerSpy = jest.fn();
const onDeleteColumn = jest.fn();
const layer = getLayer();
const existingFields = getExistingFields();
const operationSupportMatrix = getDefaultOperationSupportMatrix(layer, 'col1', existingFields);
const operationSupportMatrix = getDefaultOperationSupportMatrix(layer, 'col1');
const instance = mount(
<FieldInput
{...defaultProps}
layer={layer}
columnId={'col1'}
updateLayer={updateLayerSpy}
existingFields={existingFields}
operationSupportMatrix={operationSupportMatrix}
onDeleteColumn={onDeleteColumn}
/>

View file

@ -22,7 +22,6 @@ export function FieldInput({
selectedColumn,
columnId,
indexPattern,
existingFields,
operationSupportMatrix,
updateLayer,
onDeleteColumn,
@ -62,7 +61,6 @@ export function FieldInput({
<FieldSelect
fieldIsInvalid={currentFieldIsInvalid}
currentIndexPattern={indexPattern}
existingFields={existingFields[indexPattern.title]}
operationByField={operationSupportMatrix.operationByField}
selectedOperationType={
// Allows operation to be selected before creating a valid column

View file

@ -10,6 +10,7 @@ import { partition } from 'lodash';
import React, { useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiComboBoxOptionOption, EuiComboBoxProps } from '@elastic/eui';
import { useExistingFieldsReader } from '@kbn/unified-field-list-plugin/public';
import type { OperationType } from '../form_based';
import type { OperationSupportMatrix } from './operation_support';
import {
@ -18,7 +19,7 @@ import {
FieldPicker,
} from '../../../shared_components/field_picker';
import { fieldContainsData } from '../../../shared_components';
import type { ExistingFieldsMap, IndexPattern } from '../../../types';
import type { IndexPattern } from '../../../types';
import { getFieldType } from '../pure_utils';
export type FieldChoiceWithOperationType = FieldOptionValue & {
@ -33,7 +34,6 @@ export interface FieldSelectProps extends EuiComboBoxProps<EuiComboBoxOptionOpti
operationByField: OperationSupportMatrix['operationByField'];
onChoose: (choice: FieldChoiceWithOperationType) => void;
onDeleteColumn?: () => void;
existingFields: ExistingFieldsMap[string];
fieldIsInvalid: boolean;
markAllFieldsCompatible?: boolean;
'data-test-subj'?: string;
@ -47,12 +47,12 @@ export function FieldSelect({
operationByField,
onChoose,
onDeleteColumn,
existingFields,
fieldIsInvalid,
markAllFieldsCompatible,
['data-test-subj']: dataTestSub,
...rest
}: FieldSelectProps) {
const { hasFieldData } = useExistingFieldsReader();
const memoizedFieldOptions = useMemo(() => {
const fields = Object.keys(operationByField).sort();
@ -67,8 +67,8 @@ export function FieldSelect({
(field) => currentIndexPattern.getFieldByName(field)?.type === 'document'
);
function containsData(field: string) {
return fieldContainsData(field, currentIndexPattern, existingFields);
function containsData(fieldName: string) {
return fieldContainsData(fieldName, currentIndexPattern, hasFieldData);
}
function fieldNamesToOptions(items: string[]) {
@ -145,7 +145,7 @@ export function FieldSelect({
selectedOperationType,
currentIndexPattern,
operationByField,
existingFields,
hasFieldData,
markAllFieldsCompatible,
]);

View file

@ -28,6 +28,16 @@ import {
import { FieldSelect } from './field_select';
import { FormBasedLayer } from '../types';
jest.mock('@kbn/unified-field-list-plugin/public/hooks/use_existing_fields', () => ({
useExistingFieldsReader: jest.fn(() => {
return {
hasFieldData: (dataViewId: string, fieldName: string) => {
return ['timestamp', 'bytes', 'memory', 'source'].includes(fieldName);
},
};
}),
}));
jest.mock('../operations');
describe('reference editor', () => {
@ -59,14 +69,6 @@ describe('reference editor', () => {
paramEditorUpdater,
selectionStyle: 'full' as const,
currentIndexPattern: createMockedIndexPattern(),
existingFields: {
'my-fake-index-pattern': {
timestamp: true,
bytes: true,
memory: true,
source: true,
},
},
dateRange: { fromDate: 'now-1d', toDate: 'now' },
storage: {} as IStorageWrapper,
uiSettings: {} as IUiSettingsClient,

View file

@ -29,12 +29,7 @@ import {
import { FieldChoiceWithOperationType, FieldSelect } from './field_select';
import { hasField } from '../pure_utils';
import type { FormBasedLayer } from '../types';
import type {
ExistingFieldsMap,
IndexPattern,
IndexPatternField,
ParamEditorCustomProps,
} from '../../../types';
import type { IndexPattern, IndexPatternField, ParamEditorCustomProps } from '../../../types';
import type { FormBasedDimensionEditorProps } from './dimension_panel';
import { FormRow } from '../operations/definitions/shared_components';
@ -83,7 +78,6 @@ export interface ReferenceEditorProps {
fieldLabel?: string;
operationDefinitionMap: Record<string, GenericOperationDefinition>;
isInline?: boolean;
existingFields: ExistingFieldsMap;
dateRange: DateRange;
labelAppend?: EuiFormRowProps['labelAppend'];
isFullscreen: boolean;
@ -114,7 +108,6 @@ export interface ReferenceEditorProps {
export const ReferenceEditor = (props: ReferenceEditorProps) => {
const {
currentIndexPattern,
existingFields,
validation,
selectionStyle,
labelAppend,
@ -307,7 +300,6 @@ export const ReferenceEditor = (props: ReferenceEditorProps) => {
<FieldSelect
fieldIsInvalid={showFieldInvalid || showFieldMissingInvalid}
currentIndexPattern={currentIndexPattern}
existingFields={existingFields[currentIndexPattern.title]}
operationByField={operationSupportMatrix.operationByField}
selectedOperationType={
// Allows operation to be selected before creating a valid column

View file

@ -1,20 +0,0 @@
/**
* 1. Don't cut off the shadow of the field items
*/
.lnsIndexPatternFieldList {
@include euiOverflowShadow;
@include euiScrollBar;
margin-left: -$euiSize; /* 1 */
position: relative;
flex-grow: 1;
overflow: auto;
}
.lnsIndexPatternFieldList__accordionContainer {
padding-top: $euiSizeS;
position: absolute;
top: 0;
left: $euiSize; /* 1 */
right: $euiSizeXS; /* 1 */
}

View file

@ -1,220 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import './field_list.scss';
import { partition, throttle } from 'lodash';
import React, { useState, Fragment, useCallback, useMemo, useEffect } from 'react';
import { EuiSpacer } from '@elastic/eui';
import { UiActionsStart } from '@kbn/ui-actions-plugin/public';
import { FieldItem } from './field_item';
import { NoFieldsCallout } from './no_fields_callout';
import { FieldItemSharedProps, FieldsAccordion } from './fields_accordion';
import type { DatasourceDataPanelProps, IndexPatternField } from '../../types';
const PAGINATION_SIZE = 50;
export type FieldGroups = Record<
string,
{
fields: IndexPatternField[];
fieldCount: number;
showInAccordion: boolean;
isInitiallyOpen: boolean;
title: string;
helpText?: string;
isAffectedByGlobalFilter: boolean;
isAffectedByTimeFilter: boolean;
hideDetails?: boolean;
defaultNoFieldsMessage?: string;
hideIfEmpty?: boolean;
}
>;
function getDisplayedFieldsLength(
fieldGroups: FieldGroups,
accordionState: Partial<Record<string, boolean>>
) {
return Object.entries(fieldGroups)
.filter(([key]) => accordionState[key])
.reduce((allFieldCount, [, { fields }]) => allFieldCount + fields.length, 0);
}
export const FieldList = React.memo(function FieldList({
exists,
fieldGroups,
existenceFetchFailed,
existenceFetchTimeout,
fieldProps,
hasSyncedExistingFields,
filter,
currentIndexPatternId,
existFieldsInIndex,
dropOntoWorkspace,
hasSuggestionForField,
editField,
removeField,
uiActions,
}: {
exists: (field: IndexPatternField) => boolean;
fieldGroups: FieldGroups;
fieldProps: FieldItemSharedProps;
hasSyncedExistingFields: boolean;
existenceFetchFailed?: boolean;
existenceFetchTimeout?: boolean;
filter: {
nameFilter: string;
typeFilter: string[];
};
currentIndexPatternId: string;
existFieldsInIndex: boolean;
dropOntoWorkspace: DatasourceDataPanelProps['dropOntoWorkspace'];
hasSuggestionForField: DatasourceDataPanelProps['hasSuggestionForField'];
editField?: (name: string) => void;
removeField?: (name: string) => void;
uiActions: UiActionsStart;
}) {
const [fieldGroupsToShow, fieldFroupsToCollapse] = partition(
Object.entries(fieldGroups),
([, { showInAccordion }]) => showInAccordion
);
const [pageSize, setPageSize] = useState(PAGINATION_SIZE);
const [scrollContainer, setScrollContainer] = useState<Element | undefined>(undefined);
const [accordionState, setAccordionState] = useState<Partial<Record<string, boolean>>>(() =>
Object.fromEntries(
fieldGroupsToShow.map(([key, { isInitiallyOpen }]) => [key, isInitiallyOpen])
)
);
useEffect(() => {
// Reset the scroll if we have made material changes to the field list
if (scrollContainer) {
scrollContainer.scrollTop = 0;
setPageSize(PAGINATION_SIZE);
}
}, [filter.nameFilter, filter.typeFilter, currentIndexPatternId, scrollContainer]);
const lazyScroll = useCallback(() => {
if (scrollContainer) {
const nearBottom =
scrollContainer.scrollTop + scrollContainer.clientHeight >
scrollContainer.scrollHeight * 0.9;
if (nearBottom) {
setPageSize(
Math.max(
PAGINATION_SIZE,
Math.min(
pageSize + PAGINATION_SIZE * 0.5,
getDisplayedFieldsLength(fieldGroups, accordionState)
)
)
);
}
}
}, [scrollContainer, pageSize, setPageSize, fieldGroups, accordionState]);
const paginatedFields = useMemo(() => {
let remainingItems = pageSize;
return Object.fromEntries(
fieldGroupsToShow.map(([key, fieldGroup]) => {
if (!accordionState[key] || remainingItems <= 0) {
return [key, []];
}
const slicedFieldList = fieldGroup.fields.slice(0, remainingItems);
remainingItems = remainingItems - slicedFieldList.length;
return [key, slicedFieldList];
})
);
}, [pageSize, fieldGroupsToShow, accordionState]);
return (
<div
className="lnsIndexPatternFieldList"
ref={(el) => {
if (el && !el.dataset.dynamicScroll) {
el.dataset.dynamicScroll = 'true';
setScrollContainer(el);
}
}}
onScroll={throttle(lazyScroll, 100)}
>
<div className="lnsIndexPatternFieldList__accordionContainer">
<ul>
{fieldFroupsToCollapse.flatMap(([, { fields }]) =>
fields.map((field, index) => (
<FieldItem
{...fieldProps}
exists={exists(field)}
field={field}
editField={editField}
removeField={removeField}
hideDetails={true}
key={field.name}
itemIndex={index}
groupIndex={0}
dropOntoWorkspace={dropOntoWorkspace}
hasSuggestionForField={hasSuggestionForField}
uiActions={uiActions}
/>
))
)}
</ul>
<EuiSpacer size="s" />
{fieldGroupsToShow.map(([key, fieldGroup], index) => {
if (Boolean(fieldGroup.hideIfEmpty) && !fieldGroup.fields.length) return null;
return (
<Fragment key={key}>
<FieldsAccordion
dropOntoWorkspace={dropOntoWorkspace}
hasSuggestionForField={hasSuggestionForField}
initialIsOpen={Boolean(accordionState[key])}
key={key}
id={`lnsIndexPattern${key}`}
label={fieldGroup.title}
helpTooltip={fieldGroup.helpText}
exists={exists}
editField={editField}
removeField={removeField}
hideDetails={fieldGroup.hideDetails}
hasLoaded={!!hasSyncedExistingFields}
fieldsCount={fieldGroup.fields.length}
isFiltered={fieldGroup.fieldCount !== fieldGroup.fields.length}
paginatedFields={paginatedFields[key]}
fieldProps={fieldProps}
groupIndex={index + 1}
onToggle={(open) => {
setAccordionState((s) => ({
...s,
[key]: open,
}));
const displayedFieldLength = getDisplayedFieldsLength(fieldGroups, {
...accordionState,
[key]: open,
});
setPageSize(
Math.max(PAGINATION_SIZE, Math.min(pageSize * 1.5, displayedFieldLength))
);
}}
showExistenceFetchError={existenceFetchFailed}
showExistenceFetchTimeout={existenceFetchTimeout}
renderCallout={
<NoFieldsCallout
isAffectedByGlobalFilter={fieldGroup.isAffectedByGlobalFilter}
isAffectedByTimerange={fieldGroup.isAffectedByTimeFilter}
isAffectedByFieldFilter={fieldGroup.fieldCount !== fieldGroup.fields.length}
existFieldsInIndex={!!existFieldsInIndex}
defaultNoFieldsMessage={fieldGroup.defaultNoFieldsMessage}
/>
}
uiActions={uiActions}
/>
<EuiSpacer size="m" />
</Fragment>
);
})}
</div>
</div>
);
});

View file

@ -1,110 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { EuiLoadingSpinner, EuiNotificationBadge } from '@elastic/eui';
import { coreMock } from '@kbn/core/public/mocks';
import { mountWithIntl, shallowWithIntl } from '@kbn/test-jest-helpers';
import { fieldFormatsServiceMock } from '@kbn/field-formats-plugin/public/mocks';
import { IndexPattern } from '../../types';
import { FieldItem } from './field_item';
import { FieldsAccordion, FieldsAccordionProps, FieldItemSharedProps } from './fields_accordion';
import { chartPluginMock } from '@kbn/charts-plugin/public/mocks';
import { uiActionsPluginMock } from '@kbn/ui-actions-plugin/public/mocks';
describe('Fields Accordion', () => {
let defaultProps: FieldsAccordionProps;
let indexPattern: IndexPattern;
let core: ReturnType<typeof coreMock['createStart']>;
let fieldProps: FieldItemSharedProps;
beforeEach(() => {
indexPattern = {
id: '1',
title: 'my-fake-index-pattern',
timeFieldName: 'timestamp',
fields: [
{
name: 'timestamp',
type: 'date',
aggregatable: true,
searchable: true,
},
{
name: 'bytes',
type: 'number',
aggregatable: true,
searchable: true,
},
],
} as IndexPattern;
core = coreMock.createStart();
core.http.post.mockClear();
fieldProps = {
indexPattern,
fieldFormats: fieldFormatsServiceMock.createStartContract(),
core,
highlight: '',
dateRange: {
fromDate: 'now-7d',
toDate: 'now',
},
query: { query: '', language: 'lucene' },
filters: [],
chartsThemeService: chartPluginMock.createSetupContract().theme,
};
defaultProps = {
initialIsOpen: true,
onToggle: jest.fn(),
id: 'id',
label: 'label',
hasLoaded: true,
fieldsCount: 2,
isFiltered: false,
paginatedFields: indexPattern.fields,
fieldProps,
renderCallout: <div id="lens-test-callout">Callout</div>,
exists: () => true,
groupIndex: 0,
dropOntoWorkspace: () => {},
hasSuggestionForField: () => false,
uiActions: uiActionsPluginMock.createStartContract(),
};
});
it('renders correct number of Field Items', () => {
const wrapper = mountWithIntl(
<FieldsAccordion {...defaultProps} exists={(field) => field.name === 'timestamp'} />
);
expect(wrapper.find(FieldItem).at(0).prop('exists')).toEqual(true);
expect(wrapper.find(FieldItem).at(1).prop('exists')).toEqual(false);
});
it('passed correct exists flag to each field', () => {
const wrapper = mountWithIntl(<FieldsAccordion {...defaultProps} />);
expect(wrapper.find(FieldItem).length).toEqual(2);
});
it('renders callout if no fields', () => {
const wrapper = shallowWithIntl(
<FieldsAccordion {...defaultProps} fieldsCount={0} paginatedFields={[]} />
);
expect(wrapper.find('#lens-test-callout').length).toEqual(1);
});
it('renders accented notificationBadge state if isFiltered', () => {
const wrapper = mountWithIntl(<FieldsAccordion {...defaultProps} isFiltered={true} />);
expect(wrapper.find(EuiNotificationBadge).prop('color')).toEqual('accent');
});
it('renders spinner if has not loaded', () => {
const wrapper = mountWithIntl(<FieldsAccordion {...defaultProps} hasLoaded={false} />);
expect(wrapper.find(EuiLoadingSpinner).length).toEqual(1);
});
});

View file

@ -1,205 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import './datapanel.scss';
import React, { memo, useCallback, useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import {
EuiText,
EuiNotificationBadge,
EuiSpacer,
EuiAccordion,
EuiLoadingSpinner,
EuiIconTip,
} from '@elastic/eui';
import classNames from 'classnames';
import { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
import { Filter } from '@kbn/es-query';
import type { Query } from '@kbn/es-query';
import { ChartsPluginSetup } from '@kbn/charts-plugin/public';
import { UiActionsStart } from '@kbn/ui-actions-plugin/public';
import { FieldItem } from './field_item';
import type { DatasourceDataPanelProps, IndexPattern, IndexPatternField } from '../../types';
export interface FieldItemSharedProps {
core: DatasourceDataPanelProps['core'];
fieldFormats: FieldFormatsStart;
chartsThemeService: ChartsPluginSetup['theme'];
indexPattern: IndexPattern;
highlight?: string;
query: Query;
dateRange: DatasourceDataPanelProps['dateRange'];
filters: Filter[];
}
export interface FieldsAccordionProps {
initialIsOpen: boolean;
onToggle: (open: boolean) => void;
id: string;
label: string;
helpTooltip?: string;
hasLoaded: boolean;
fieldsCount: number;
isFiltered: boolean;
paginatedFields: IndexPatternField[];
fieldProps: FieldItemSharedProps;
renderCallout: JSX.Element;
exists: (field: IndexPatternField) => boolean;
showExistenceFetchError?: boolean;
showExistenceFetchTimeout?: boolean;
hideDetails?: boolean;
groupIndex: number;
dropOntoWorkspace: DatasourceDataPanelProps['dropOntoWorkspace'];
hasSuggestionForField: DatasourceDataPanelProps['hasSuggestionForField'];
editField?: (name: string) => void;
removeField?: (name: string) => void;
uiActions: UiActionsStart;
}
export const FieldsAccordion = memo(function InnerFieldsAccordion({
initialIsOpen,
onToggle,
id,
label,
helpTooltip,
hasLoaded,
fieldsCount,
isFiltered,
paginatedFields,
fieldProps,
renderCallout,
exists,
hideDetails,
showExistenceFetchError,
showExistenceFetchTimeout,
groupIndex,
dropOntoWorkspace,
hasSuggestionForField,
editField,
removeField,
uiActions,
}: FieldsAccordionProps) {
const renderField = useCallback(
(field: IndexPatternField, index) => (
<FieldItem
{...fieldProps}
key={field.name}
field={field}
exists={exists(field)}
hideDetails={hideDetails}
itemIndex={index}
groupIndex={groupIndex}
dropOntoWorkspace={dropOntoWorkspace}
hasSuggestionForField={hasSuggestionForField}
editField={editField}
removeField={removeField}
uiActions={uiActions}
/>
),
[
fieldProps,
exists,
hideDetails,
dropOntoWorkspace,
hasSuggestionForField,
groupIndex,
editField,
removeField,
uiActions,
]
);
const renderButton = useMemo(() => {
const titleClassname = classNames({
// eslint-disable-next-line @typescript-eslint/naming-convention
lnsInnerIndexPatternDataPanel__titleTooltip: !!helpTooltip,
});
return (
<EuiText size="xs">
<strong className={titleClassname}>{label}</strong>
{!!helpTooltip && (
<EuiIconTip
aria-label={helpTooltip}
type="questionInCircle"
color="subdued"
size="s"
position="right"
content={helpTooltip}
iconProps={{
className: 'eui-alignTop',
}}
/>
)}
</EuiText>
);
}, [label, helpTooltip]);
const extraAction = useMemo(() => {
if (showExistenceFetchError) {
return (
<EuiIconTip
aria-label={i18n.translate('xpack.lens.indexPattern.existenceErrorAriaLabel', {
defaultMessage: 'Existence fetch failed',
})}
type="alert"
color="warning"
content={i18n.translate('xpack.lens.indexPattern.existenceErrorLabel', {
defaultMessage: "Field information can't be loaded",
})}
/>
);
}
if (showExistenceFetchTimeout) {
return (
<EuiIconTip
aria-label={i18n.translate('xpack.lens.indexPattern.existenceTimeoutAriaLabel', {
defaultMessage: 'Existence fetch timed out',
})}
type="clock"
color="warning"
content={i18n.translate('xpack.lens.indexPattern.existenceTimeoutLabel', {
defaultMessage: 'Field information took too long',
})}
/>
);
}
if (hasLoaded) {
return (
<EuiNotificationBadge
size="m"
color={isFiltered ? 'accent' : 'subdued'}
data-test-subj={`${id}-count`}
>
{fieldsCount}
</EuiNotificationBadge>
);
}
return <EuiLoadingSpinner size="m" />;
}, [showExistenceFetchError, showExistenceFetchTimeout, hasLoaded, isFiltered, id, fieldsCount]);
return (
<EuiAccordion
initialIsOpen={initialIsOpen}
onToggle={onToggle}
data-test-subj={id}
id={id}
buttonContent={renderButton}
extraAction={extraAction}
>
<EuiSpacer size="s" />
{hasLoaded &&
(!!fieldsCount ? (
<ul className="lnsInnerIndexPatternDataPanel__fieldItems">
{paginatedFields && paginatedFields.map(renderField)}
</ul>
) : (
renderCallout
))}
</EuiAccordion>
);
});

View file

@ -181,8 +181,6 @@ describe('Layer Data Panel', () => {
{ id: '2', title: 'my-fake-restricted-pattern' },
{ id: '3', title: 'my-compatible-pattern' },
],
existingFields: {},
isFirstExistenceFetch: false,
indexPatterns: {
'1': {
id: '1',

View file

@ -1,161 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { shallow } from 'enzyme';
import { NoFieldsCallout } from './no_fields_callout';
describe('NoFieldCallout', () => {
it('renders correctly for index with no fields', () => {
const component = shallow(<NoFieldsCallout existFieldsInIndex={false} />);
expect(component).toMatchInlineSnapshot(`
<EuiCallOut
color="warning"
size="s"
title="No fields exist in this data view."
/>
`);
});
it('renders correctly when empty with no filters/timerange reasons', () => {
const component = shallow(<NoFieldsCallout existFieldsInIndex={true} />);
expect(component).toMatchInlineSnapshot(`
<EuiCallOut
color="warning"
size="s"
title="There are no fields."
/>
`);
});
it('renders correctly with passed defaultNoFieldsMessage', () => {
const component = shallow(
<NoFieldsCallout existFieldsInIndex={true} defaultNoFieldsMessage="No empty fields" />
);
expect(component).toMatchInlineSnapshot(`
<EuiCallOut
color="warning"
size="s"
title="No empty fields"
/>
`);
});
it('renders properly when affected by field filter', () => {
const component = shallow(
<NoFieldsCallout existFieldsInIndex={true} isAffectedByFieldFilter={true} />
);
expect(component).toMatchInlineSnapshot(`
<EuiCallOut
color="warning"
size="s"
title="No fields match the selected filters."
>
<strong>
Try:
</strong>
<ul>
<li>
Using different field filters
</li>
</ul>
</EuiCallOut>
`);
});
it('renders correctly when affected by global filters and timerange', () => {
const component = shallow(
<NoFieldsCallout
existFieldsInIndex={true}
isAffectedByTimerange={true}
isAffectedByGlobalFilter={true}
defaultNoFieldsMessage="There are no available fields that contain data."
/>
);
expect(component).toMatchInlineSnapshot(`
<EuiCallOut
color="warning"
size="s"
title="There are no available fields that contain data."
>
<strong>
Try:
</strong>
<ul>
<li>
Extending the time range
</li>
<li>
Changing the global filters
</li>
</ul>
</EuiCallOut>
`);
});
it('renders correctly when affected by global filters and field filters', () => {
const component = shallow(
<NoFieldsCallout
existFieldsInIndex={true}
isAffectedByTimerange={true}
isAffectedByFieldFilter={true}
defaultNoFieldsMessage="There are no available fields that contain data."
/>
);
expect(component).toMatchInlineSnapshot(`
<EuiCallOut
color="warning"
size="s"
title="No fields match the selected filters."
>
<strong>
Try:
</strong>
<ul>
<li>
Extending the time range
</li>
<li>
Using different field filters
</li>
</ul>
</EuiCallOut>
`);
});
it('renders correctly when affected by field filters, global filter and timerange', () => {
const component = shallow(
<NoFieldsCallout
existFieldsInIndex={true}
isAffectedByFieldFilter={true}
isAffectedByTimerange={true}
isAffectedByGlobalFilter={true}
defaultNoFieldsMessage={`doesn't exist`}
/>
);
expect(component).toMatchInlineSnapshot(`
<EuiCallOut
color="warning"
size="s"
title="No fields match the selected filters."
>
<strong>
Try:
</strong>
<ul>
<li>
Extending the time range
</li>
<li>
Using different field filters
</li>
<li>
Changing the global filters
</li>
</ul>
</EuiCallOut>
`);
});
});

View file

@ -1,87 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { EuiCallOut } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
const defaultNoFieldsMessageCopy = i18n.translate('xpack.lens.indexPatterns.noDataLabel', {
defaultMessage: 'There are no fields.',
});
export const NoFieldsCallout = ({
existFieldsInIndex,
defaultNoFieldsMessage = defaultNoFieldsMessageCopy,
isAffectedByFieldFilter = false,
isAffectedByTimerange = false,
isAffectedByGlobalFilter = false,
}: {
existFieldsInIndex: boolean;
isAffectedByFieldFilter?: boolean;
defaultNoFieldsMessage?: string;
isAffectedByTimerange?: boolean;
isAffectedByGlobalFilter?: boolean;
}) => {
if (!existFieldsInIndex) {
return (
<EuiCallOut
size="s"
color="warning"
title={i18n.translate('xpack.lens.indexPatterns.noFieldsLabel', {
defaultMessage: 'No fields exist in this data view.',
})}
/>
);
}
return (
<EuiCallOut
size="s"
color="warning"
title={
isAffectedByFieldFilter
? i18n.translate('xpack.lens.indexPatterns.noFilteredFieldsLabel', {
defaultMessage: 'No fields match the selected filters.',
})
: defaultNoFieldsMessage
}
>
{(isAffectedByTimerange || isAffectedByFieldFilter || isAffectedByGlobalFilter) && (
<>
<strong>
{i18n.translate('xpack.lens.indexPatterns.noFields.tryText', {
defaultMessage: 'Try:',
})}
</strong>
<ul>
{isAffectedByTimerange && (
<li>
{i18n.translate('xpack.lens.indexPatterns.noFields.extendTimeBullet', {
defaultMessage: 'Extending the time range',
})}
</li>
)}
{isAffectedByFieldFilter && (
<li>
{i18n.translate('xpack.lens.indexPatterns.noFields.fieldTypeFilterBullet', {
defaultMessage: 'Using different field filters',
})}
</li>
)}
{isAffectedByGlobalFilter && (
<li>
{i18n.translate('xpack.lens.indexPatterns.noFields.globalFiltersBullet', {
defaultMessage: 'Changing the global filters',
})}
</li>
)}
</ul>
</>
)}
</EuiCallOut>
);
};

View file

@ -113,14 +113,6 @@ const defaultOptions = {
isFullscreen: false,
toggleFullscreen: jest.fn(),
setIsCloseable: jest.fn(),
existingFields: {
my_index_pattern: {
timestamp: true,
bytes: true,
memory: true,
source: true,
},
},
};
describe('date_histogram', () => {

View file

@ -38,14 +38,6 @@ const defaultProps = {
toggleFullscreen: jest.fn(),
setIsCloseable: jest.fn(),
layerId: '1',
existingFields: {
my_index_pattern: {
timestamp: true,
bytes: true,
memory: true,
source: true,
},
},
};
// mocking random id generator function

View file

@ -198,7 +198,6 @@ export interface ParamEditorProps<
activeData?: FormBasedDimensionEditorProps['activeData'];
operationDefinitionMap: Record<string, GenericOperationDefinition>;
paramEditorCustomProps?: ParamEditorCustomProps;
existingFields: Record<string, Record<string, boolean>>;
isReferenced?: boolean;
}
@ -215,10 +214,6 @@ export interface FieldInputProps<C> {
incompleteParams: Omit<IncompleteColumn, 'sourceField' | 'operationType'>;
dimensionGroups: FormBasedDimensionEditorProps['dimensionGroups'];
groupId: FormBasedDimensionEditorProps['groupId'];
/**
* indexPatternId -> fieldName -> boolean
*/
existingFields: Record<string, Record<string, boolean>>;
operationSupportMatrix: OperationSupportMatrix;
helpMessage?: React.ReactNode;
operationDefinitionMap: Record<string, GenericOperationDefinition>;

View file

@ -44,14 +44,6 @@ const defaultProps = {
toggleFullscreen: jest.fn(),
setIsCloseable: jest.fn(),
layerId: '1',
existingFields: {
my_index_pattern: {
timestamp: true,
bytes: true,
memory: true,
source: true,
},
},
};
describe('last_value', () => {

View file

@ -60,14 +60,6 @@ const defaultProps = {
toggleFullscreen: jest.fn(),
setIsCloseable: jest.fn(),
layerId: '1',
existingFields: {
my_index_pattern: {
timestamp: true,
bytes: true,
memory: true,
source: true,
},
},
};
describe('percentile', () => {

View file

@ -53,14 +53,6 @@ const defaultProps = {
toggleFullscreen: jest.fn(),
setIsCloseable: jest.fn(),
layerId: '1',
existingFields: {
my_index_pattern: {
timestamp: true,
bytes: true,
memory: true,
source: true,
},
},
};
describe('percentile ranks', () => {

View file

@ -86,14 +86,6 @@ const defaultOptions = {
storage: {} as IStorageWrapper,
uiSettings: uiSettingsMock,
savedObjectsClient: {} as SavedObjectsClientContract,
existingFields: {
my_index_pattern: {
timestamp: true,
bytes: true,
memory: true,
source: true,
},
},
dateRange: {
fromDate: 'now-1y',
toDate: 'now',

View file

@ -52,14 +52,6 @@ const defaultProps = {
toggleFullscreen: jest.fn(),
setIsCloseable: jest.fn(),
layerId: '1',
existingFields: {
my_index_pattern: {
timestamp: true,
bytes: true,
memory: true,
source: true,
},
},
};
describe('static_value', () => {

View file

@ -8,7 +8,7 @@
import React, { useCallback, useMemo } from 'react';
import { htmlIdGenerator } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { ExistingFieldsMap, IndexPattern } from '../../../../../types';
import { IndexPattern } from '../../../../../types';
import {
DragDropBuckets,
FieldsBucketContainer,
@ -27,7 +27,6 @@ export const MAX_MULTI_FIELDS_SIZE = 3;
export interface FieldInputsProps {
column: TermsIndexPatternColumn;
indexPattern: IndexPattern;
existingFields: ExistingFieldsMap;
invalidFields?: string[];
operationSupportMatrix: Pick<OperationSupportMatrix, 'operationByField'>;
onChange: (newValues: string[]) => void;
@ -49,7 +48,6 @@ export function FieldInputs({
column,
onChange,
indexPattern,
existingFields,
operationSupportMatrix,
invalidFields,
}: FieldInputsProps) {
@ -153,7 +151,6 @@ export function FieldInputs({
<FieldSelect
fieldIsInvalid={shouldShowError}
currentIndexPattern={indexPattern}
existingFields={existingFields[indexPattern.title]}
operationByField={filteredOperationByField}
selectedOperationType={column.operationType}
selectedField={value}

View file

@ -426,7 +426,6 @@ export const termsOperation: OperationDefinition<
selectedColumn,
columnId,
indexPattern,
existingFields,
operationSupportMatrix,
updateLayer,
dimensionGroups,
@ -549,7 +548,6 @@ export const termsOperation: OperationDefinition<
<FieldInputs
column={selectedColumn}
indexPattern={indexPattern}
existingFields={existingFields}
operationSupportMatrix={operationSupportMatrix}
onChange={onFieldSelectChange}
invalidFields={invalidFields}
@ -568,7 +566,6 @@ The top values of a specified field ranked by the chosen metric.
currentColumn,
columnId,
indexPattern,
existingFields,
operationDefinitionMap,
ReferenceEditor,
paramEditorCustomProps,
@ -808,7 +805,6 @@ The top values of a specified field ranked by the chosen metric.
}}
column={currentColumn.params.orderAgg}
incompleteColumn={incompleteColumn}
existingFields={existingFields}
onDeleteColumn={() => {
throw new Error('Should not be called');
}}

View file

@ -50,6 +50,16 @@ jest.mock('@kbn/unified-field-list-plugin/public/services/field_stats', () => ({
}),
}));
jest.mock('@kbn/unified-field-list-plugin/public/hooks/use_existing_fields', () => ({
useExistingFieldsReader: jest.fn(() => {
return {
hasFieldData: (dataViewId: string, fieldName: string) => {
return ['timestamp', 'bytes', 'memory', 'source'].includes(fieldName);
},
};
}),
}));
// mocking random id generator function
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
@ -93,14 +103,6 @@ const defaultProps = {
setIsCloseable: jest.fn(),
layerId: '1',
ReferenceEditor,
existingFields: {
'my-fake-index-pattern': {
timestamp: true,
bytes: true,
memory: true,
source: true,
},
},
};
describe('terms', () => {
@ -1170,20 +1172,7 @@ describe('terms', () => {
>,
};
function getExistingFields() {
const fields: Record<string, boolean> = {};
for (const field of defaultProps.indexPattern.fields) {
fields[field.name] = true;
}
return {
[defaultProps.indexPattern.title]: fields,
};
}
function getDefaultOperationSupportMatrix(
columnId: string,
existingFields: Record<string, Record<string, boolean>>
) {
function getDefaultOperationSupportMatrix(columnId: string) {
return getOperationSupportMatrix({
state: {
layers: { layer1: layer },
@ -1199,15 +1188,13 @@ describe('terms', () => {
it('should render the default field input for no field (incomplete operation)', () => {
const updateLayerSpy = jest.fn();
const existingFields = getExistingFields();
const operationSupportMatrix = getDefaultOperationSupportMatrix('col1', existingFields);
const operationSupportMatrix = getDefaultOperationSupportMatrix('col1');
const instance = mount(
<InlineFieldInput
{...defaultFieldInputProps}
layer={layer}
updateLayer={updateLayerSpy}
columnId="col1"
existingFields={existingFields}
operationSupportMatrix={operationSupportMatrix}
incompleteOperation="terms"
/>
@ -1226,8 +1213,7 @@ describe('terms', () => {
it('should show an error message when first field is invalid', () => {
const updateLayerSpy = jest.fn();
const existingFields = getExistingFields();
const operationSupportMatrix = getDefaultOperationSupportMatrix('col1', existingFields);
const operationSupportMatrix = getDefaultOperationSupportMatrix('col1');
layer.columns.col1 = {
label: 'Top value of unsupported',
@ -1247,7 +1233,6 @@ describe('terms', () => {
layer={layer}
updateLayer={updateLayerSpy}
columnId="col1"
existingFields={existingFields}
operationSupportMatrix={operationSupportMatrix}
currentFieldIsInvalid
/>
@ -1259,8 +1244,7 @@ describe('terms', () => {
it('should show an error message when first field is not supported', () => {
const updateLayerSpy = jest.fn();
const existingFields = getExistingFields();
const operationSupportMatrix = getDefaultOperationSupportMatrix('col1', existingFields);
const operationSupportMatrix = getDefaultOperationSupportMatrix('col1');
layer.columns.col1 = {
label: 'Top value of timestamp',
@ -1280,7 +1264,6 @@ describe('terms', () => {
layer={layer}
updateLayer={updateLayerSpy}
columnId="col1"
existingFields={existingFields}
operationSupportMatrix={operationSupportMatrix}
selectedColumn={layer.columns.col1 as TermsIndexPatternColumn}
incompleteOperation="terms"
@ -1293,8 +1276,7 @@ describe('terms', () => {
it('should show an error message when any field but the first is invalid', () => {
const updateLayerSpy = jest.fn();
const existingFields = getExistingFields();
const operationSupportMatrix = getDefaultOperationSupportMatrix('col1', existingFields);
const operationSupportMatrix = getDefaultOperationSupportMatrix('col1');
layer.columns.col1 = {
label: 'Top value of geo.src + 1 other',
@ -1315,7 +1297,6 @@ describe('terms', () => {
layer={layer}
updateLayer={updateLayerSpy}
columnId="col1"
existingFields={existingFields}
operationSupportMatrix={operationSupportMatrix}
selectedColumn={layer.columns.col1 as TermsIndexPatternColumn}
/>
@ -1327,8 +1308,7 @@ describe('terms', () => {
it('should show an error message when any field but the first is not supported', () => {
const updateLayerSpy = jest.fn();
const existingFields = getExistingFields();
const operationSupportMatrix = getDefaultOperationSupportMatrix('col1', existingFields);
const operationSupportMatrix = getDefaultOperationSupportMatrix('col1');
layer.columns.col1 = {
label: 'Top value of geo.src + 1 other',
@ -1349,7 +1329,6 @@ describe('terms', () => {
layer={layer}
updateLayer={updateLayerSpy}
columnId="col1"
existingFields={existingFields}
operationSupportMatrix={operationSupportMatrix}
selectedColumn={layer.columns.col1 as TermsIndexPatternColumn}
/>
@ -1361,15 +1340,13 @@ describe('terms', () => {
it('should render the an add button for single layer and disabled the remove button', () => {
const updateLayerSpy = jest.fn();
const existingFields = getExistingFields();
const operationSupportMatrix = getDefaultOperationSupportMatrix('col1', existingFields);
const operationSupportMatrix = getDefaultOperationSupportMatrix('col1');
const instance = mount(
<InlineFieldInput
{...defaultFieldInputProps}
layer={layer}
updateLayer={updateLayerSpy}
columnId="col1"
existingFields={existingFields}
operationSupportMatrix={operationSupportMatrix}
selectedColumn={layer.columns.col1 as TermsIndexPatternColumn}
/>
@ -1392,15 +1369,13 @@ describe('terms', () => {
it('should switch to the first supported operation when in single term mode and the picked field is not supported', () => {
const updateLayerSpy = jest.fn();
const existingFields = getExistingFields();
const operationSupportMatrix = getDefaultOperationSupportMatrix('col1', existingFields);
const operationSupportMatrix = getDefaultOperationSupportMatrix('col1');
const instance = mount(
<InlineFieldInput
{...defaultFieldInputProps}
layer={layer}
updateLayer={updateLayerSpy}
columnId="col1"
existingFields={existingFields}
operationSupportMatrix={operationSupportMatrix}
selectedColumn={layer.columns.col1 as TermsIndexPatternColumn}
/>
@ -1426,8 +1401,7 @@ describe('terms', () => {
it('should render the multi terms specific UI', () => {
const updateLayerSpy = jest.fn();
const existingFields = getExistingFields();
const operationSupportMatrix = getDefaultOperationSupportMatrix('col1', existingFields);
const operationSupportMatrix = getDefaultOperationSupportMatrix('col1');
(layer.columns.col1 as TermsIndexPatternColumn).params.secondaryFields = ['bytes'];
const instance = mount(
@ -1436,7 +1410,6 @@ describe('terms', () => {
layer={layer}
updateLayer={updateLayerSpy}
columnId="col1"
existingFields={existingFields}
operationSupportMatrix={operationSupportMatrix}
selectedColumn={layer.columns.col1 as TermsIndexPatternColumn}
/>
@ -1457,8 +1430,7 @@ describe('terms', () => {
it('should return to single value UI when removing second item of two', () => {
const updateLayerSpy = jest.fn();
const existingFields = getExistingFields();
const operationSupportMatrix = getDefaultOperationSupportMatrix('col1', existingFields);
const operationSupportMatrix = getDefaultOperationSupportMatrix('col1');
(layer.columns.col1 as TermsIndexPatternColumn).params.secondaryFields = ['memory'];
const instance = mount(
@ -1467,7 +1439,6 @@ describe('terms', () => {
layer={layer}
updateLayer={updateLayerSpy}
columnId="col1"
existingFields={existingFields}
operationSupportMatrix={operationSupportMatrix}
selectedColumn={layer.columns.col1 as TermsIndexPatternColumn}
/>
@ -1489,8 +1460,7 @@ describe('terms', () => {
it('should disable remove button and reorder drag when single value and one temporary new field', () => {
const updateLayerSpy = jest.fn();
const existingFields = getExistingFields();
const operationSupportMatrix = getDefaultOperationSupportMatrix('col1', existingFields);
const operationSupportMatrix = getDefaultOperationSupportMatrix('col1');
let instance = mount(
<InlineFieldInput
@ -1498,7 +1468,6 @@ describe('terms', () => {
layer={layer}
updateLayer={updateLayerSpy}
columnId="col1"
existingFields={existingFields}
operationSupportMatrix={operationSupportMatrix}
selectedColumn={layer.columns.col1 as TermsIndexPatternColumn}
/>
@ -1532,8 +1501,7 @@ describe('terms', () => {
it('should accept scripted fields for single value', () => {
const updateLayerSpy = jest.fn();
const existingFields = getExistingFields();
const operationSupportMatrix = getDefaultOperationSupportMatrix('col1', existingFields);
const operationSupportMatrix = getDefaultOperationSupportMatrix('col1');
(layer.columns.col1 as TermsIndexPatternColumn).sourceField = 'scripted';
const instance = mount(
@ -1542,7 +1510,6 @@ describe('terms', () => {
layer={layer}
updateLayer={updateLayerSpy}
columnId="col1"
existingFields={existingFields}
operationSupportMatrix={operationSupportMatrix}
selectedColumn={layer.columns.col1 as TermsIndexPatternColumn}
/>
@ -1558,8 +1525,7 @@ describe('terms', () => {
it('should mark scripted fields for multiple values', () => {
const updateLayerSpy = jest.fn();
const existingFields = getExistingFields();
const operationSupportMatrix = getDefaultOperationSupportMatrix('col1', existingFields);
const operationSupportMatrix = getDefaultOperationSupportMatrix('col1');
(layer.columns.col1 as TermsIndexPatternColumn).sourceField = 'scripted';
(layer.columns.col1 as TermsIndexPatternColumn).params.secondaryFields = ['memory'];
@ -1569,7 +1535,6 @@ describe('terms', () => {
layer={layer}
updateLayer={updateLayerSpy}
columnId="col1"
existingFields={existingFields}
operationSupportMatrix={operationSupportMatrix}
selectedColumn={layer.columns.col1 as TermsIndexPatternColumn}
/>
@ -1588,8 +1553,7 @@ describe('terms', () => {
it('should not filter scripted fields when in single value', () => {
const updateLayerSpy = jest.fn();
const existingFields = getExistingFields();
const operationSupportMatrix = getDefaultOperationSupportMatrix('col1', existingFields);
const operationSupportMatrix = getDefaultOperationSupportMatrix('col1');
const instance = mount(
<InlineFieldInput
@ -1597,7 +1561,6 @@ describe('terms', () => {
layer={layer}
updateLayer={updateLayerSpy}
columnId="col1"
existingFields={existingFields}
operationSupportMatrix={operationSupportMatrix}
selectedColumn={layer.columns.col1 as TermsIndexPatternColumn}
/>
@ -1618,8 +1581,7 @@ describe('terms', () => {
it('should filter scripted fields when in multi terms mode', () => {
const updateLayerSpy = jest.fn();
const existingFields = getExistingFields();
const operationSupportMatrix = getDefaultOperationSupportMatrix('col1', existingFields);
const operationSupportMatrix = getDefaultOperationSupportMatrix('col1');
(layer.columns.col1 as TermsIndexPatternColumn).params.secondaryFields = ['memory'];
const instance = mount(
@ -1628,7 +1590,6 @@ describe('terms', () => {
layer={layer}
updateLayer={updateLayerSpy}
columnId="col1"
existingFields={existingFields}
operationSupportMatrix={operationSupportMatrix}
selectedColumn={layer.columns.col1 as TermsIndexPatternColumn}
/>
@ -1650,8 +1611,7 @@ describe('terms', () => {
it('should filter already used fields when displaying fields list', () => {
const updateLayerSpy = jest.fn();
const existingFields = getExistingFields();
const operationSupportMatrix = getDefaultOperationSupportMatrix('col1', existingFields);
const operationSupportMatrix = getDefaultOperationSupportMatrix('col1');
(layer.columns.col1 as TermsIndexPatternColumn).params.secondaryFields = ['memory', 'bytes'];
let instance = mount(
@ -1660,7 +1620,6 @@ describe('terms', () => {
layer={layer}
updateLayer={updateLayerSpy}
columnId="col1"
existingFields={existingFields}
operationSupportMatrix={operationSupportMatrix}
selectedColumn={layer.columns.col1 as TermsIndexPatternColumn}
/>
@ -1690,8 +1649,7 @@ describe('terms', () => {
it('should filter fields with unsupported types when in multi terms mode', () => {
const updateLayerSpy = jest.fn();
const existingFields = getExistingFields();
const operationSupportMatrix = getDefaultOperationSupportMatrix('col1', existingFields);
const operationSupportMatrix = getDefaultOperationSupportMatrix('col1');
(layer.columns.col1 as TermsIndexPatternColumn).params.secondaryFields = ['memory'];
const instance = mount(
@ -1700,7 +1658,6 @@ describe('terms', () => {
layer={layer}
updateLayer={updateLayerSpy}
columnId="col1"
existingFields={existingFields}
operationSupportMatrix={operationSupportMatrix}
selectedColumn={layer.columns.col1 as TermsIndexPatternColumn}
/>
@ -1722,8 +1679,7 @@ describe('terms', () => {
it('should limit the number of multiple fields', () => {
const updateLayerSpy = jest.fn();
const existingFields = getExistingFields();
const operationSupportMatrix = getDefaultOperationSupportMatrix('col1', existingFields);
const operationSupportMatrix = getDefaultOperationSupportMatrix('col1');
(layer.columns.col1 as TermsIndexPatternColumn).params.secondaryFields = [
'memory',
@ -1736,7 +1692,6 @@ describe('terms', () => {
layer={layer}
updateLayer={updateLayerSpy}
columnId="col1"
existingFields={existingFields}
operationSupportMatrix={operationSupportMatrix}
selectedColumn={layer.columns.col1 as TermsIndexPatternColumn}
/>
@ -1757,8 +1712,7 @@ describe('terms', () => {
it('should let the user add new empty field up to the limit', () => {
const updateLayerSpy = jest.fn();
const existingFields = getExistingFields();
const operationSupportMatrix = getDefaultOperationSupportMatrix('col1', existingFields);
const operationSupportMatrix = getDefaultOperationSupportMatrix('col1');
let instance = mount(
<InlineFieldInput
@ -1766,7 +1720,6 @@ describe('terms', () => {
layer={layer}
updateLayer={updateLayerSpy}
columnId="col1"
existingFields={existingFields}
operationSupportMatrix={operationSupportMatrix}
selectedColumn={layer.columns.col1 as TermsIndexPatternColumn}
/>
@ -1793,8 +1746,7 @@ describe('terms', () => {
it('should update the parentFormatter on transition between single to multi terms', () => {
const updateLayerSpy = jest.fn();
const existingFields = getExistingFields();
const operationSupportMatrix = getDefaultOperationSupportMatrix('col1', existingFields);
const operationSupportMatrix = getDefaultOperationSupportMatrix('col1');
let instance = mount(
<InlineFieldInput
@ -1802,7 +1754,6 @@ describe('terms', () => {
layer={layer}
updateLayer={updateLayerSpy}
columnId="col1"
existingFields={existingFields}
operationSupportMatrix={operationSupportMatrix}
selectedColumn={layer.columns.col1 as TermsIndexPatternColumn}
/>
@ -1834,8 +1785,7 @@ describe('terms', () => {
it('should preserve custom label when set by the user', () => {
const updateLayerSpy = jest.fn();
const existingFields = getExistingFields();
const operationSupportMatrix = getDefaultOperationSupportMatrix('col1', existingFields);
const operationSupportMatrix = getDefaultOperationSupportMatrix('col1');
layer.columns.col1 = {
label: 'MyCustomLabel',
@ -1857,7 +1807,6 @@ describe('terms', () => {
layer={layer}
updateLayer={updateLayerSpy}
columnId="col1"
existingFields={existingFields}
operationSupportMatrix={operationSupportMatrix}
selectedColumn={layer.columns.col1 as TermsIndexPatternColumn}
/>

View file

@ -8,6 +8,7 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { act } from 'react-dom/test-utils';
import { ReactWrapper } from 'enzyme';
import type { Query } from '@kbn/es-query';
import { dataPluginMock } from '@kbn/data-plugin/public/mocks';
@ -30,7 +31,6 @@ import { createIndexPatternServiceMock } from '../../mocks/data_views_service_mo
import { createMockFramePublicAPI } from '../../mocks';
import { createMockedDragDropContext } from './mocks';
import { DataViewsState } from '../../state_management';
import { ExistingFieldsMap, IndexPattern } from '../../types';
const fieldsFromQuery = [
{
@ -101,18 +101,6 @@ const fieldsOne = [
},
];
function getExistingFields(indexPatterns: Record<string, IndexPattern>) {
const existingFields: ExistingFieldsMap = {};
for (const { title, fields } of Object.values(indexPatterns)) {
const fieldsMap: Record<string, boolean> = {};
for (const { displayName, name } of fields) {
fieldsMap[displayName ?? name] = true;
}
existingFields[title] = fieldsMap;
}
return existingFields;
}
const initialState: TextBasedPrivateState = {
layers: {
first: {
@ -130,8 +118,41 @@ const initialState: TextBasedPrivateState = {
fieldList: fieldsFromQuery,
};
function getFrameAPIMock({ indexPatterns, existingFields, ...rest }: Partial<DataViewsState> = {}) {
function getFrameAPIMock({
indexPatterns,
...rest
}: Partial<DataViewsState> & { indexPatterns: DataViewsState['indexPatterns'] }) {
const frameAPI = createMockFramePublicAPI();
return {
...frameAPI,
dataViews: {
...frameAPI.dataViews,
indexPatterns,
...rest,
},
};
}
// @ts-expect-error Portal mocks are notoriously difficult to type
ReactDOM.createPortal = jest.fn((element) => element);
async function mountAndWaitForLazyModules(component: React.ReactElement): Promise<ReactWrapper> {
let inst: ReactWrapper;
await act(async () => {
inst = await mountWithIntl(component);
// wait for lazy modules
await new Promise((resolve) => setTimeout(resolve, 0));
inst.update();
});
await inst!.update();
return inst!;
}
describe('TextBased Query Languages Data Panel', () => {
let core: ReturnType<typeof coreMock['createStart']>;
let dataViews: DataViewPublicStart;
const defaultIndexPatterns = {
'1': {
id: '1',
@ -144,27 +165,10 @@ function getFrameAPIMock({ indexPatterns, existingFields, ...rest }: Partial<Dat
spec: {},
},
};
return {
...frameAPI,
dataViews: {
...frameAPI.dataViews,
indexPatterns: indexPatterns ?? defaultIndexPatterns,
existingFields: existingFields ?? getExistingFields(indexPatterns ?? defaultIndexPatterns),
isFirstExistenceFetch: false,
...rest,
},
};
}
// @ts-expect-error Portal mocks are notoriously difficult to type
ReactDOM.createPortal = jest.fn((element) => element);
describe('TextBased Query Languages Data Panel', () => {
let core: ReturnType<typeof coreMock['createStart']>;
let dataViews: DataViewPublicStart;
let defaultProps: TextBasedDataPanelProps;
const dataViewsMock = dataViewPluginMocks.createStartContract();
beforeEach(() => {
core = coreMock.createStart();
dataViews = dataViewPluginMocks.createStartContract();
@ -194,7 +198,7 @@ describe('TextBased Query Languages Data Panel', () => {
hasSuggestionForField: jest.fn(() => false),
uiActions: uiActionsPluginMock.createStartContract(),
indexPatternService: createIndexPatternServiceMock({ core, dataViews }),
frame: getFrameAPIMock(),
frame: getFrameAPIMock({ indexPatterns: defaultIndexPatterns }),
state: initialState,
setState: jest.fn(),
onChangeIndexPattern: jest.fn(),
@ -202,23 +206,33 @@ describe('TextBased Query Languages Data Panel', () => {
});
it('should render a search box', async () => {
const wrapper = mountWithIntl(<TextBasedDataPanel {...defaultProps} />);
expect(wrapper.find('[data-test-subj="lnsTextBasedLangugesFieldSearch"]').length).toEqual(1);
const wrapper = await mountAndWaitForLazyModules(<TextBasedDataPanel {...defaultProps} />);
expect(wrapper.find('[data-test-subj="lnsTextBasedLanguagesFieldSearch"]').length).toEqual(1);
});
it('should list all supported fields in the pattern', async () => {
const wrapper = mountWithIntl(<TextBasedDataPanel {...defaultProps} />);
const wrapper = await mountAndWaitForLazyModules(<TextBasedDataPanel {...defaultProps} />);
expect(
wrapper
.find('[data-test-subj="lnsTextBasedLanguagesPanelFields"]')
.find('[data-test-subj="lnsTextBasedLanguagesAvailableFields"]')
.find(FieldButton)
.map((fieldItem) => fieldItem.prop('fieldName'))
).toEqual(['timestamp', 'bytes', 'memory']);
).toEqual(['bytes', 'memory', 'timestamp']);
expect(wrapper.find('[data-test-subj="lnsTextBasedLanguagesEmptyFields"]').exists()).toBe(
false
);
expect(wrapper.find('[data-test-subj="lnsTextBasedLanguagesMetaFields"]').exists()).toBe(false);
});
it('should not display the selected fields accordion if there are no fields displayed', async () => {
const wrapper = mountWithIntl(<TextBasedDataPanel {...defaultProps} />);
expect(wrapper.find('[data-test-subj="lnsSelectedFieldsTextBased"]').length).toEqual(0);
const wrapper = await mountAndWaitForLazyModules(<TextBasedDataPanel {...defaultProps} />);
expect(wrapper.find('[data-test-subj="lnsTextBasedLanguagesSelectedFields"]').length).toEqual(
0
);
});
it('should display the selected fields accordion if there are fields displayed', async () => {
@ -226,13 +240,17 @@ describe('TextBased Query Languages Data Panel', () => {
...defaultProps,
layerFields: ['memory'],
};
const wrapper = mountWithIntl(<TextBasedDataPanel {...props} />);
expect(wrapper.find('[data-test-subj="lnsSelectedFieldsTextBased"]').length).not.toEqual(0);
const wrapper = await mountAndWaitForLazyModules(<TextBasedDataPanel {...props} />);
expect(
wrapper.find('[data-test-subj="lnsTextBasedLanguagesSelectedFields"]').length
).not.toEqual(0);
});
it('should list all supported fields in the pattern that match the search input', async () => {
const wrapper = mountWithIntl(<TextBasedDataPanel {...defaultProps} />);
const searchBox = wrapper.find('[data-test-subj="lnsTextBasedLangugesFieldSearch"]');
const wrapper = await mountAndWaitForLazyModules(<TextBasedDataPanel {...defaultProps} />);
const searchBox = wrapper.find('[data-test-subj="lnsTextBasedLanguagesFieldSearch"]');
act(() => {
searchBox.prop('onChange')!({
@ -240,10 +258,10 @@ describe('TextBased Query Languages Data Panel', () => {
} as React.ChangeEvent<HTMLInputElement>);
});
wrapper.update();
await wrapper.update();
expect(
wrapper
.find('[data-test-subj="lnsTextBasedLanguagesPanelFields"]')
.find('[data-test-subj="lnsTextBasedLanguagesAvailableFields"]')
.find(FieldButton)
.map((fieldItem) => fieldItem.prop('fieldName'))
).toEqual(['memory']);

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React, { useState, useEffect, useMemo } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiFormControlLayout, htmlIdGenerator } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import usePrevious from 'react-use/lib/usePrevious';
@ -14,13 +14,22 @@ import { DataPublicPluginStart } from '@kbn/data-plugin/public';
import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
import { isOfAggregateQueryType } from '@kbn/es-query';
import { ExpressionsStart } from '@kbn/expressions-plugin/public';
import { DatatableColumn, ExpressionsStart } from '@kbn/expressions-plugin/public';
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import {
ExistenceFetchStatus,
FieldListGrouped,
FieldListGroupedProps,
FieldsGroupNames,
useGroupedFields,
} from '@kbn/unified-field-list-plugin/public';
import { FieldButton } from '@kbn/react-field';
import type { DatasourceDataPanelProps } from '../../types';
import type { TextBasedPrivateState } from './types';
import { getStateFromAggregateQuery } from './utils';
import { ChildDragDropProvider } from '../../drag_drop';
import { FieldsAccordion } from './fields_accordion';
import { ChildDragDropProvider, DragDrop } from '../../drag_drop';
import { DataType } from '../../types';
import { LensFieldIcon } from '../../shared_components';
export type TextBasedDataPanelProps = DatasourceDataPanelProps<TextBasedPrivateState> & {
data: DataPublicPluginStart;
@ -67,8 +76,16 @@ export function TextBasedDataPanel({
}, [data, dataViews, expressions, prevQuery, query, setState, state]);
const { fieldList } = state;
const filteredFields = useMemo(() => {
return fieldList.filter((field) => {
const onSelectedFieldFilter = useCallback(
(field: DatatableColumn): boolean => {
return Boolean(layerFields?.includes(field.name));
},
[layerFields]
);
const onFilterField = useCallback(
(field: DatatableColumn) => {
if (
localState.nameFilter &&
!field.name.toLowerCase().includes(localState.nameFilter.toLowerCase())
@ -76,9 +93,57 @@ export function TextBasedDataPanel({
return false;
}
return true;
});
}, [fieldList, localState.nameFilter]);
const usedByLayersFields = fieldList.filter((field) => layerFields?.includes(field.name));
},
[localState]
);
const onOverrideFieldGroupDetails = useCallback((groupName) => {
if (groupName === FieldsGroupNames.AvailableFields) {
return {
helpText: i18n.translate('xpack.lens.indexPattern.allFieldsForTextBasedLabelHelp', {
defaultMessage:
'Drag and drop available fields to the workspace and create visualizations. To change the available fields, edit your query.',
}),
};
}
}, []);
const { fieldGroups } = useGroupedFields<DatatableColumn>({
dataViewId: null,
allFields: fieldList,
services: {
dataViews,
},
onFilterField,
onSelectedFieldFilter,
onOverrideFieldGroupDetails,
});
const renderFieldItem: FieldListGroupedProps<DatatableColumn>['renderFieldItem'] = useCallback(
({ field, itemIndex, groupIndex, hideDetails }) => {
return (
<DragDrop
draggable
order={[itemIndex]}
value={{
field: field?.name,
id: field.id,
humanData: { label: field?.name },
}}
dataTestSubj={`lnsFieldListPanelField-${field.name}`}
>
<FieldButton
className={`lnsFieldItem lnsFieldItem--${field?.meta?.type}`}
isActive={false}
onClick={() => {}}
fieldIcon={<LensFieldIcon type={field?.meta.type as DataType} />}
fieldName={field?.name}
/>
</DragDrop>
);
},
[]
);
return (
<KibanaContextProvider
@ -111,7 +176,7 @@ export function TextBasedDataPanel({
>
<input
className="euiFieldText euiFieldText--fullWidth lnsInnerIndexPatternDataPanel__textField"
data-test-subj="lnsTextBasedLangugesFieldSearch"
data-test-subj="lnsTextBasedLanguagesFieldSearch"
placeholder={i18n.translate('xpack.lens.indexPatterns.filterByNameLabel', {
defaultMessage: 'Search field names',
description: 'Search the list of fields in the data view for the provided text',
@ -129,32 +194,16 @@ export function TextBasedDataPanel({
</EuiFormControlLayout>
</EuiFlexItem>
<EuiFlexItem>
<div className="lnsIndexPatternFieldList">
<div className="lnsIndexPatternFieldList__accordionContainer">
{usedByLayersFields.length > 0 && (
<FieldsAccordion
initialIsOpen={true}
hasLoaded={dataHasLoaded}
isFiltered={Boolean(localState.nameFilter)}
fields={usedByLayersFields}
id="lnsSelectedFieldsTextBased"
label={i18n.translate('xpack.lens.textBased.selectedFieldsLabel', {
defaultMessage: 'Selected fields',
})}
/>
)}
<FieldsAccordion
initialIsOpen={true}
hasLoaded={dataHasLoaded}
isFiltered={Boolean(localState.nameFilter)}
fields={filteredFields}
id="lnsAvailableFieldsTextBased"
label={i18n.translate('xpack.lens.textBased.availableFieldsLabel', {
defaultMessage: 'Available fields',
})}
/>
</div>
</div>
<FieldListGrouped<DatatableColumn>
fieldGroups={fieldGroups}
fieldsExistenceStatus={
dataHasLoaded ? ExistenceFetchStatus.succeeded : ExistenceFetchStatus.unknown
}
fieldsExistInIndex={Boolean(fieldList.length)}
renderFieldItem={renderFieldItem}
screenReaderDescriptionForSearchInputId={fieldSearchDescriptionId}
data-test-subj="lnsTextBasedLanguages"
/>
</EuiFlexItem>
</EuiFlexGroup>
</ChildDragDropProvider>

View file

@ -1,106 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { memo, useMemo } from 'react';
import type { DatatableColumn } from '@kbn/expressions-plugin/public';
import {
EuiText,
EuiNotificationBadge,
EuiAccordion,
EuiLoadingSpinner,
EuiSpacer,
} from '@elastic/eui';
import { FieldButton } from '@kbn/react-field';
import { DragDrop } from '../../drag_drop';
import { LensFieldIcon } from '../../shared_components';
import type { DataType } from '../../types';
export interface FieldsAccordionProps {
initialIsOpen: boolean;
hasLoaded: boolean;
isFiltered: boolean;
// forceState: 'open' | 'closed';
id: string;
label: string;
fields: DatatableColumn[];
}
export const FieldsAccordion = memo(function InnerFieldsAccordion({
initialIsOpen,
hasLoaded,
isFiltered,
id,
label,
fields,
}: FieldsAccordionProps) {
const renderButton = useMemo(() => {
return (
<EuiText size="xs">
<strong>{label}</strong>
</EuiText>
);
}, [label]);
const extraAction = useMemo(() => {
if (hasLoaded) {
return (
<EuiNotificationBadge
size="m"
color={isFiltered ? 'accent' : 'subdued'}
data-test-subj={`${id}-count`}
>
{fields.length}
</EuiNotificationBadge>
);
}
return <EuiLoadingSpinner size="m" />;
}, [fields.length, hasLoaded, id, isFiltered]);
return (
<>
<EuiAccordion
initialIsOpen={initialIsOpen}
id={id}
buttonContent={renderButton}
extraAction={extraAction}
data-test-subj={id}
>
<ul
className="lnsInnerIndexPatternDataPanel__fieldItems"
data-test-subj="lnsTextBasedLanguagesPanelFields"
>
{fields.length > 0 &&
fields.map((field, index) => (
<li key={field?.name}>
<DragDrop
draggable
order={[index]}
value={{
field: field?.name,
id: field.id,
humanData: { label: field?.name },
}}
dataTestSubj={`lnsFieldListPanelField-${field.name}`}
>
<FieldButton
className={`lnsFieldItem lnsFieldItem--${field?.meta?.type}`}
isActive={false}
onClick={() => {}}
fieldIcon={<LensFieldIcon type={field?.meta.type as DataType} />}
fieldName={field?.name}
/>
</DragDrop>
</li>
))}
</ul>
</EuiAccordion>
<EuiSpacer size="m" />
</>
);
});

View file

@ -68,8 +68,6 @@ describe('Layer Data Panel', () => {
{ id: '2', title: 'my-fake-restricted-pattern', name: 'my-fake-restricted-pattern' },
{ id: '3', title: 'my-compatible-pattern', name: 'my-compatible-pattern' },
],
existingFields: {},
isFirstExistenceFetch: false,
indexPatterns: {},
} as DataViewsState,
};

View file

@ -568,7 +568,6 @@ export function LayerPanel(
invalid: group.invalid,
invalidMessage: group.invalidMessage,
indexPatterns: dataViews.indexPatterns,
existingFields: dataViews.existingFields,
}}
/>
) : (
@ -728,7 +727,6 @@ export function LayerPanel(
formatSelectorOptions: activeGroup.formatSelectorOptions,
layerType: activeVisualization.getLayerType(layerId, visualizationState),
indexPatterns: dataViews.indexPatterns,
existingFields: dataViews.existingFields,
activeData: layerVisualizationConfigProps.activeData,
}}
/>

View file

@ -59,8 +59,6 @@ export const defaultState = {
dataViews: {
indexPatterns: {},
indexPatternRefs: [],
existingFields: {},
isFirstExistenceFetch: false,
},
};

View file

@ -5,25 +5,17 @@
* 2.0.
*/
import { type ExistingFieldsReader } from '@kbn/unified-field-list-plugin/public';
import { IndexPattern } from '../../types';
/**
* Checks if the provided field contains data (works for meta field)
*/
export function fieldContainsData(
field: string,
fieldName: string,
indexPattern: IndexPattern,
existingFields: Record<string, boolean>
hasFieldData: ExistingFieldsReader['hasFieldData']
) {
return (
indexPattern.getFieldByName(field)?.type === 'document' || fieldExists(existingFields, field)
);
}
/**
* Performs an existence check on the existingFields data structure for the provided field.
* Does not work for meta fields.
*/
export function fieldExists(existingFields: Record<string, boolean>, fieldName: string) {
return existingFields[fieldName];
const field = indexPattern.getFieldByName(fieldName);
return field?.type === 'document' || hasFieldData(indexPattern.id, fieldName);
}

View file

@ -6,4 +6,4 @@
*/
export { ChangeIndexPattern } from './dataview_picker';
export { fieldExists, fieldContainsData } from './helpers';
export { fieldContainsData } from './helpers';

View file

@ -11,7 +11,7 @@ export { LegendSettingsPopover } from './legend_settings_popover';
export { PalettePicker } from './palette_picker';
export { FieldPicker, LensFieldIcon, TruncatedLabel } from './field_picker';
export type { FieldOption, FieldOptionValue } from './field_picker';
export { ChangeIndexPattern, fieldExists, fieldContainsData } from './dataview_picker';
export { ChangeIndexPattern, fieldContainsData } from './dataview_picker';
export { QueryInput, isQueryValid, validateQuery } from './query_input';
export {
NewBucketButton,

View file

@ -5,10 +5,8 @@ Object {
"lens": Object {
"activeDatasourceId": "testDatasource",
"dataViews": Object {
"existingFields": Object {},
"indexPatternRefs": Array [],
"indexPatterns": Object {},
"isFirstExistenceFetch": true,
},
"datasourceStates": Object {
"testDatasource": Object {

View file

@ -50,8 +50,6 @@ export const initialState: LensAppState = {
dataViews: {
indexPatternRefs: [],
indexPatterns: {},
existingFields: {},
isFirstExistenceFetch: true,
},
};

View file

@ -30,10 +30,6 @@ export interface VisualizationState {
export interface DataViewsState {
indexPatternRefs: IndexPatternRef[];
indexPatterns: Record<string, IndexPattern>;
existingFields: Record<string, Record<string, boolean>>;
isFirstExistenceFetch: boolean;
existenceFetchFailed?: boolean;
existenceFetchTimeout?: boolean;
}
export type DatasourceStates = Record<string, { isLoading: boolean; state: unknown }>;

View file

@ -108,7 +108,6 @@ export interface EditorFrameProps {
export type VisualizationMap = Record<string, Visualization>;
export type DatasourceMap = Record<string, Datasource>;
export type IndexPatternMap = Record<string, IndexPattern>;
export type ExistingFieldsMap = Record<string, Record<string, boolean>>;
export interface EditorFrameInstance {
EditorFrameContainer: (props: EditorFrameProps) => React.ReactElement;
@ -589,7 +588,6 @@ export type DatasourceDimensionProps<T> = SharedDimensionProps & {
state: T;
activeData?: Record<string, Datatable>;
indexPatterns: IndexPatternMap;
existingFields: Record<string, Record<string, boolean>>;
hideTooltip?: boolean;
invalid?: boolean;
invalidMessage?: string;

View file

@ -80,8 +80,6 @@ export function getInitialDataViewsObject(
return {
indexPatterns,
indexPatternRefs,
existingFields: {},
isFirstExistenceFetch: true,
};
}
@ -107,9 +105,6 @@ export async function refreshIndexPatternsList({
onIndexPatternRefresh: () => onRefreshCallbacks.forEach((fn) => fn()),
});
const indexPattern = newlyMappedIndexPattern[indexPatternId];
// But what about existingFields here?
// When the indexPatterns cache object gets updated, the data panel will
// notice it and refetch the fields list existence map
indexPatternService.updateDataViewsState({
indexPatterns: {
...indexPatternsCache,

View file

@ -23,6 +23,7 @@ import {
QueryPointEventAnnotationConfig,
} from '@kbn/event-annotation-plugin/common';
import moment from 'moment';
import { useExistingFieldsReader } from '@kbn/unified-field-list-plugin/public';
import {
FieldOption,
FieldOptionValue,
@ -31,7 +32,6 @@ import {
import { FormatFactory } from '../../../../../common';
import {
DimensionEditorSection,
fieldExists,
NameInput,
useDebouncedValue,
} from '../../../../shared_components';
@ -58,6 +58,7 @@ export const AnnotationsPanel = (
) => {
const { state, setState, layerId, accessor, frame } = props;
const isHorizontal = isHorizontalChart(state.layers);
const { hasFieldData } = useExistingFieldsReader();
const { inputValue: localState, handleInputChange: setLocalState } = useDebouncedValue<XYState>({
value: state,
@ -248,10 +249,7 @@ export const AnnotationsPanel = (
field: field.name,
dataType: field.type,
},
exists: fieldExists(
frame.dataViews.existingFields[currentIndexPattern.title],
field.name
),
exists: hasFieldData(currentIndexPattern.id, field.name),
compatible: true,
'data-test-subj': `lnsXY-annotation-fieldOption-${field.name}`,
} as FieldOption<FieldOptionValue>)
@ -379,7 +377,6 @@ export const AnnotationsPanel = (
currentConfig={currentAnnotation}
setConfig={setAnnotations}
indexPattern={frame.dataViews.indexPatterns[localLayer.indexPatternId]}
existingFields={frame.dataViews.existingFields}
/>
</EuiFormRow>
</DimensionEditorSection>

View file

@ -10,8 +10,8 @@ import type { Query } from '@kbn/data-plugin/common';
import type { QueryPointEventAnnotationConfig } from '@kbn/event-annotation-plugin/common';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { useExistingFieldsReader } from '@kbn/unified-field-list-plugin/public';
import {
fieldExists,
FieldOption,
FieldOptionValue,
FieldPicker,
@ -41,7 +41,7 @@ export const ConfigPanelQueryAnnotation = ({
queryInputShouldOpen?: boolean;
}) => {
const currentIndexPattern = frame.dataViews.indexPatterns[layer.indexPatternId];
const currentExistingFields = frame.dataViews.existingFields[currentIndexPattern.title];
const { hasFieldData } = useExistingFieldsReader();
// list only date fields
const options = currentIndexPattern.fields
.filter((field) => field.type === 'date' && field.displayName)
@ -53,7 +53,7 @@ export const ConfigPanelQueryAnnotation = ({
field: field.name,
dataType: field.type,
},
exists: fieldExists(currentExistingFields, field.name),
exists: hasFieldData(currentIndexPattern.id, field.name),
compatible: true,
'data-test-subj': `lns-fieldOption-${field.name}`,
} as FieldOption<FieldOptionValue>;

View file

@ -9,9 +9,9 @@ import { htmlIdGenerator, EuiFlexItem, EuiPanel, EuiText } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React, { useCallback, useMemo } from 'react';
import { QueryPointEventAnnotationConfig } from '@kbn/event-annotation-plugin/common';
import type { ExistingFieldsMap, IndexPattern } from '../../../../types';
import { useExistingFieldsReader } from '@kbn/unified-field-list-plugin/public';
import type { IndexPattern } from '../../../../types';
import {
fieldExists,
FieldOption,
FieldOptionValue,
FieldPicker,
@ -31,7 +31,6 @@ export interface FieldInputsProps {
currentConfig: QueryPointEventAnnotationConfig;
setConfig: (config: QueryPointEventAnnotationConfig) => void;
indexPattern: IndexPattern;
existingFields: ExistingFieldsMap;
invalidFields?: string[];
}
@ -51,9 +50,9 @@ export function TooltipSection({
currentConfig,
setConfig,
indexPattern,
existingFields,
invalidFields,
}: FieldInputsProps) {
const { hasFieldData } = useExistingFieldsReader();
const onChangeWrapped = useCallback(
(values: WrappedValue[]) => {
setConfig({
@ -124,7 +123,6 @@ export function TooltipSection({
</>
);
}
const currentExistingField = existingFields[indexPattern.title];
const options = indexPattern.fields
.filter(
@ -140,7 +138,7 @@ export function TooltipSection({
field: field.name,
dataType: field.type,
},
exists: fieldExists(currentExistingField, field.name),
exists: hasFieldData(indexPattern.id, field.name),
compatible: true,
'data-test-subj': `lnsXY-annotation-tooltip-fieldOption-${field.name}`,
} as FieldOption<FieldOptionValue>)