[Lens] Use accordion menus in field list for available and empty fields (#68871) (#70095)

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Marta Bondyra 2020-06-27 22:32:09 +02:00 committed by GitHub
parent 8d1ae019ae
commit 39102c33e4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 814 additions and 451 deletions

View file

@ -19,7 +19,6 @@ export function loadInitialState() {
[restricted.id]: restricted,
},
layers: {},
showEmptyFields: false,
};
return result;
}

View file

@ -0,0 +1,49 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`NoFieldCallout renders properly for index with no fields 1`] = `
<EuiCallOut
color="warning"
size="s"
title="No fields exist in this index pattern."
/>
`;
exports[`NoFieldCallout renders properly when affected by field filter 1`] = `
<EuiCallOut
color="warning"
size="s"
title="No fields match the selected filters."
>
<strong>
Try:
</strong>
<ul>
<li>
Using different field filters
</li>
</ul>
</EuiCallOut>
`;
exports[`NoFieldCallout renders properly when affected by field filters, global filter and timerange 1`] = `
<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,2 +1 @@
@import 'datapanel';
@import 'field_item';

View file

@ -16,10 +16,6 @@
line-height: $euiSizeXXL;
}
.lnsInnerIndexPatternDataPanel__filterWrapper {
flex-grow: 0;
}
/**
* 1. Don't cut off the shadow of the field items
*/
@ -41,11 +37,9 @@
right: $euiSizeXS; /* 1 */
}
.lnsInnerIndexPatternDataPanel__filterButton {
width: 100%;
color: $euiColorPrimary;
padding-left: $euiSizeS;
padding-right: $euiSizeS;
.lnsInnerIndexPatternDataPanel__fieldItems {
// Quick fix for making sure the shadow and focus rings are visible outside the accordion bounds
padding: $euiSizeXS $euiSizeXS 0;
}
.lnsInnerIndexPatternDataPanel__textField {
@ -54,7 +48,9 @@
}
.lnsInnerIndexPatternDataPanel__filterType {
font-size: $euiFontSizeS;
padding: $euiSizeS;
border-bottom: 1px solid $euiColorLightestShade;
}
.lnsInnerIndexPatternDataPanel__filterTypeInner {

View file

@ -9,19 +9,19 @@ import { createMockedDragDropContext } from './mocks';
import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks';
import { InnerIndexPatternDataPanel, IndexPatternDataPanel, MemoizedDataPanel } from './datapanel';
import { FieldItem } from './field_item';
import { NoFieldsCallout } from './no_fields_callout';
import { act } from 'react-dom/test-utils';
import { coreMock } from 'src/core/public/mocks';
import { IndexPatternPrivateState } from './types';
import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers';
import { ChangeIndexPattern } from './change_indexpattern';
import { EuiProgress } from '@elastic/eui';
import { EuiProgress, EuiLoadingSpinner } from '@elastic/eui';
import { documentField } from './document_field';
const initialState: IndexPatternPrivateState = {
indexPatternRefs: [],
existingFields: {},
currentIndexPatternId: '1',
showEmptyFields: false,
layers: {
first: {
indexPatternId: '1',
@ -229,8 +229,6 @@ describe('IndexPattern Data Panel', () => {
},
query: { query: '', language: 'lucene' },
filters: [],
showEmptyFields: false,
onToggleEmptyFields: jest.fn(),
};
});
@ -303,7 +301,6 @@ describe('IndexPattern Data Panel', () => {
state: {
indexPatternRefs: [],
existingFields: {},
showEmptyFields: false,
currentIndexPatternId: 'a',
indexPatterns: {
a: { id: 'a', title: 'aaa', timeFieldName: 'atime', fields: [] },
@ -534,117 +531,11 @@ describe('IndexPattern Data Panel', () => {
});
});
describe('while showing empty fields', () => {
it('should list all supported fields in the pattern sorted alphabetically', async () => {
const wrapper = shallowWithIntl(
<InnerIndexPatternDataPanel {...defaultProps} showEmptyFields={true} />
);
expect(wrapper.find(FieldItem).map((fieldItem) => fieldItem.prop('field').name)).toEqual([
'Records',
'bytes',
'client',
'memory',
'source',
'timestamp',
]);
});
it('should filter down by name', () => {
const wrapper = shallowWithIntl(
<InnerIndexPatternDataPanel {...defaultProps} showEmptyFields={true} />
);
act(() => {
wrapper.find('[data-test-subj="lnsIndexPatternFieldSearch"]').prop('onChange')!({
target: { value: 'mem' },
} as ChangeEvent<HTMLInputElement>);
});
expect(wrapper.find(FieldItem).map((fieldItem) => fieldItem.prop('field').name)).toEqual([
'memory',
]);
});
it('should filter down by type', () => {
const wrapper = mountWithIntl(
<InnerIndexPatternDataPanel {...defaultProps} showEmptyFields={true} />
);
wrapper.find('[data-test-subj="lnsIndexPatternFiltersToggle"]').first().simulate('click');
wrapper.find('[data-test-subj="typeFilter-number"]').first().simulate('click');
expect(wrapper.find(FieldItem).map((fieldItem) => fieldItem.prop('field').name)).toEqual([
'bytes',
'memory',
]);
});
it('should toggle type if clicked again', () => {
const wrapper = mountWithIntl(
<InnerIndexPatternDataPanel {...defaultProps} showEmptyFields={true} />
);
wrapper.find('[data-test-subj="lnsIndexPatternFiltersToggle"]').first().simulate('click');
wrapper.find('[data-test-subj="typeFilter-number"]').first().simulate('click');
wrapper.find('[data-test-subj="typeFilter-number"]').first().simulate('click');
expect(wrapper.find(FieldItem).map((fieldItem) => fieldItem.prop('field').name)).toEqual([
'Records',
'bytes',
'client',
'memory',
'source',
'timestamp',
]);
});
it('should filter down by type and by name', () => {
const wrapper = mountWithIntl(
<InnerIndexPatternDataPanel {...defaultProps} showEmptyFields={true} />
);
act(() => {
wrapper.find('[data-test-subj="lnsIndexPatternFieldSearch"]').prop('onChange')!({
target: { value: 'mem' },
} as ChangeEvent<HTMLInputElement>);
});
wrapper.find('[data-test-subj="lnsIndexPatternFiltersToggle"]').first().simulate('click');
wrapper.find('[data-test-subj="typeFilter-number"]').first().simulate('click');
expect(wrapper.find(FieldItem).map((fieldItem) => fieldItem.prop('field').name)).toEqual([
'memory',
]);
});
});
describe('filtering out empty fields', () => {
let emptyFieldsTestProps: typeof defaultProps;
describe('displaying field list', () => {
let props: Parameters<typeof InnerIndexPatternDataPanel>[0];
beforeEach(() => {
emptyFieldsTestProps = {
props = {
...defaultProps,
indexPatterns: {
...defaultProps.indexPatterns,
'1': {
...defaultProps.indexPatterns['1'],
fields: defaultProps.indexPatterns['1'].fields.map((field) => ({
...field,
exists: field.type === 'number',
})),
},
},
onToggleEmptyFields: jest.fn(),
};
});
it('should list all supported fields in the pattern sorted alphabetically', async () => {
const props = {
...emptyFieldsTestProps,
existingFields: {
idx1: {
bytes: true,
@ -652,41 +543,145 @@ describe('IndexPattern Data Panel', () => {
},
},
};
const wrapper = shallowWithIntl(<InnerIndexPatternDataPanel {...props} />);
});
it('should list all supported fields in the pattern sorted alphabetically in groups', async () => {
const wrapper = mountWithIntl(<InnerIndexPatternDataPanel {...props} />);
expect(wrapper.find(FieldItem).first().prop('field').name).toEqual('Records');
expect(
wrapper
.find('[data-test-subj="lnsIndexPatternAvailableFields"]')
.find(FieldItem)
.map((fieldItem) => fieldItem.prop('field').name)
).toEqual(['bytes', 'memory']);
wrapper
.find('[data-test-subj="lnsIndexPatternEmptyFields"]')
.find('button')
.first()
.simulate('click');
expect(
wrapper
.find('[data-test-subj="lnsIndexPatternEmptyFields"]')
.find(FieldItem)
.map((fieldItem) => fieldItem.prop('field').name)
).toEqual(['client', 'source', 'timestamp']);
});
it('should display NoFieldsCallout when all fields are empty', async () => {
const wrapper = mountWithIntl(
<InnerIndexPatternDataPanel {...defaultProps} existingFields={{ idx1: {} }} />
);
expect(wrapper.find(NoFieldsCallout).length).toEqual(1);
expect(
wrapper
.find('[data-test-subj="lnsIndexPatternAvailableFields"]')
.find(FieldItem)
.map((fieldItem) => fieldItem.prop('field').name)
).toEqual([]);
wrapper
.find('[data-test-subj="lnsIndexPatternEmptyFields"]')
.find('button')
.first()
.simulate('click');
expect(
wrapper
.find('[data-test-subj="lnsIndexPatternEmptyFields"]')
.find(FieldItem)
.map((fieldItem) => fieldItem.prop('field').name)
).toEqual(['bytes', 'client', 'memory', 'source', 'timestamp']);
});
it('should display spinner for available fields accordion if existing fields are not loaded yet', async () => {
const wrapper = mountWithIntl(<InnerIndexPatternDataPanel {...defaultProps} />);
expect(
wrapper.find('[data-test-subj="lnsIndexPatternAvailableFields"]').find(EuiLoadingSpinner)
.length
).toEqual(1);
wrapper.setProps({ existingFields: { idx1: {} } });
expect(wrapper.find(NoFieldsCallout).length).toEqual(1);
});
it('should filter down by name', () => {
const wrapper = mountWithIntl(<InnerIndexPatternDataPanel {...props} />);
act(() => {
wrapper.find('[data-test-subj="lnsIndexPatternFieldSearch"]').prop('onChange')!({
target: { value: 'me' },
} as ChangeEvent<HTMLInputElement>);
});
wrapper
.find('[data-test-subj="lnsIndexPatternEmptyFields"]')
.find('button')
.first()
.simulate('click');
expect(wrapper.find(FieldItem).map((fieldItem) => fieldItem.prop('field').name)).toEqual([
'memory',
'timestamp',
]);
});
it('should filter down by type', () => {
const wrapper = mountWithIntl(<InnerIndexPatternDataPanel {...props} />);
wrapper.find('[data-test-subj="lnsIndexPatternFiltersToggle"]').first().simulate('click');
wrapper.find('[data-test-subj="typeFilter-number"]').first().simulate('click');
expect(wrapper.find(FieldItem).map((fieldItem) => fieldItem.prop('field').name)).toEqual([
'Records',
'bytes',
'memory',
]);
});
it('should filter down by name', () => {
const wrapper = shallowWithIntl(
<InnerIndexPatternDataPanel {...emptyFieldsTestProps} showEmptyFields={true} />
);
it('should display no fields in groups when filtered by type Record', () => {
const wrapper = mountWithIntl(<InnerIndexPatternDataPanel {...props} />);
wrapper.find('[data-test-subj="lnsIndexPatternFiltersToggle"]').first().simulate('click');
wrapper.find('[data-test-subj="typeFilter-document"]').first().simulate('click');
expect(wrapper.find(FieldItem).map((fieldItem) => fieldItem.prop('field').name)).toEqual([
'Records',
]);
expect(wrapper.find(NoFieldsCallout).length).toEqual(2);
});
it('should toggle type if clicked again', () => {
const wrapper = mountWithIntl(<InnerIndexPatternDataPanel {...props} />);
wrapper.find('[data-test-subj="lnsIndexPatternFiltersToggle"]').first().simulate('click');
wrapper.find('[data-test-subj="typeFilter-number"]').first().simulate('click');
wrapper.find('[data-test-subj="typeFilter-number"]').first().simulate('click');
wrapper
.find('[data-test-subj="lnsIndexPatternEmptyFields"]')
.find('button')
.first()
.simulate('click');
expect(wrapper.find(FieldItem).map((fieldItem) => fieldItem.prop('field').name)).toEqual([
'Records',
'bytes',
'memory',
'client',
'source',
'timestamp',
]);
});
it('should filter down by type and by name', () => {
const wrapper = mountWithIntl(<InnerIndexPatternDataPanel {...props} />);
act(() => {
wrapper.find('[data-test-subj="lnsIndexPatternFieldSearch"]').prop('onChange')!({
target: { value: 'mem' },
target: { value: 'me' },
} as ChangeEvent<HTMLInputElement>);
});
wrapper.find('[data-test-subj="lnsIndexPatternFiltersToggle"]').first().simulate('click');
wrapper.find('[data-test-subj="typeFilter-number"]').first().simulate('click');
expect(wrapper.find(FieldItem).map((fieldItem) => fieldItem.prop('field').name)).toEqual([
'memory',
]);
});
it('should allow removing the filter for data', () => {
const wrapper = mountWithIntl(<InnerIndexPatternDataPanel {...emptyFieldsTestProps} />);
wrapper.find('[data-test-subj="lnsIndexPatternFiltersToggle"]').first().simulate('click');
wrapper.find('[data-test-subj="lnsEmptyFilter"]').first().prop('onChange')!(
{} as ChangeEvent
);
expect(emptyFieldsTestProps.onToggleEmptyFields).toHaveBeenCalled();
});
});
});

View file

@ -4,26 +4,21 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { uniq, indexBy } from 'lodash';
import React, { useState, useEffect, memo, useCallback } from 'react';
import './datapanel.scss';
import { uniq, indexBy, groupBy, throttle } from 'lodash';
import React, { useState, useEffect, memo, useCallback, useMemo } from 'react';
import {
// @ts-ignore
EuiHighlight,
EuiFlexGroup,
EuiFlexItem,
EuiContextMenuPanel,
EuiContextMenuItem,
EuiContextMenuPanelProps,
EuiPopover,
EuiPopoverTitle,
EuiPopoverFooter,
EuiCallOut,
EuiFormControlLayout,
EuiSwitch,
EuiFacetButton,
EuiIcon,
EuiSpacer,
EuiFormLabel,
EuiFilterGroup,
EuiFilterButton,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
@ -31,6 +26,7 @@ import { DataPublicPluginStart } from 'src/plugins/data/public';
import { DatasourceDataPanelProps, DataType, StateSetter } from '../types';
import { ChildDragDropProvider, DragContextState } from '../drag_drop';
import { FieldItem } from './field_item';
import { NoFieldsCallout } from './no_fields_callout';
import {
IndexPattern,
IndexPatternPrivateState,
@ -41,6 +37,7 @@ import { trackUiEvent } from '../lens_ui_telemetry';
import { syncExistingFields } from './loader';
import { fieldExists } from './pure_helpers';
import { Loader } from '../loader';
import { FieldsAccordion } from './fields_accordion';
import { esQuery, IIndexPattern } from '../../../../../src/plugins/data/public';
export type Props = DatasourceDataPanelProps<IndexPatternPrivateState> & {
@ -87,21 +84,9 @@ export function IndexPatternDataPanel({
changeIndexPattern,
}: Props) {
const { indexPatternRefs, indexPatterns, currentIndexPatternId } = state;
const onChangeIndexPattern = useCallback(
(id: string) => changeIndexPattern(id, state, setState),
[state, setState]
);
const onToggleEmptyFields = useCallback(
(showEmptyFields?: boolean) => {
setState((prevState) => ({
...prevState,
showEmptyFields:
showEmptyFields === undefined ? !prevState.showEmptyFields : showEmptyFields,
}));
},
[setState]
[state, setState, changeIndexPattern]
);
const indexPatternList = uniq(
@ -179,8 +164,6 @@ export function IndexPatternDataPanel({
dateRange={dateRange}
filters={filters}
dragDropContext={dragDropContext}
showEmptyFields={state.showEmptyFields}
onToggleEmptyFields={onToggleEmptyFields}
core={core}
data={data}
onChangeIndexPattern={onChangeIndexPattern}
@ -195,8 +178,26 @@ interface DataPanelState {
nameFilter: string;
typeFilter: DataType[];
isTypeFilterOpen: boolean;
isAvailableAccordionOpen: boolean;
isEmptyAccordionOpen: boolean;
}
export interface FieldsGroup {
specialFields: IndexPatternField[];
availableFields: IndexPatternField[];
emptyFields: IndexPatternField[];
}
const defaultFieldGroups = {
specialFields: [],
availableFields: [],
emptyFields: [],
};
const fieldFiltersLabel = i18n.translate('xpack.lens.indexPatterns.fieldFiltersLabel', {
defaultMessage: 'Field filters',
});
export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({
currentIndexPatternId,
indexPatternRefs,
@ -206,8 +207,6 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({
filters,
dragDropContext,
onChangeIndexPattern,
showEmptyFields,
onToggleEmptyFields,
core,
data,
existingFields,
@ -217,8 +216,6 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({
indexPatternRefs: IndexPatternRef[];
indexPatterns: Record<string, IndexPattern>;
dragDropContext: DragContextState;
showEmptyFields: boolean;
onToggleEmptyFields: (showEmptyFields?: boolean) => void;
onChangeIndexPattern: (newId: string) => void;
existingFields: IndexPatternPrivateState['existingFields'];
}) {
@ -226,79 +223,158 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({
nameFilter: '',
typeFilter: [],
isTypeFilterOpen: false,
isAvailableAccordionOpen: true,
isEmptyAccordionOpen: false,
});
const [pageSize, setPageSize] = useState(PAGINATION_SIZE);
const [scrollContainer, setScrollContainer] = useState<Element | undefined>(undefined);
const currentIndexPattern = indexPatterns[currentIndexPatternId];
const allFields = currentIndexPattern.fields;
const fieldByName = indexBy(allFields, 'name');
const clearLocalState = () => setLocalState((s) => ({ ...s, nameFilter: '', typeFilter: [] }));
const lazyScroll = () => {
if (scrollContainer) {
const nearBottom =
scrollContainer.scrollTop + scrollContainer.clientHeight >
scrollContainer.scrollHeight * 0.9;
if (nearBottom) {
setPageSize(Math.max(PAGINATION_SIZE, Math.min(pageSize * 1.5, allFields.length)));
}
}
};
const hasSyncedExistingFields = existingFields[currentIndexPattern.title];
const availableFieldTypes = uniq(allFields.map(({ type }) => type)).filter(
(type) => type in fieldTypeNames
);
useEffect(() => {
// Reset the scroll if we have made material changes to the field list
if (scrollContainer) {
scrollContainer.scrollTop = 0;
setPageSize(PAGINATION_SIZE);
lazyScroll();
}
}, [localState.nameFilter, localState.typeFilter, currentIndexPatternId, showEmptyFields]);
}, [localState.nameFilter, localState.typeFilter, currentIndexPatternId, scrollContainer]);
const availableFieldTypes = uniq(allFields.map(({ type }) => type)).filter(
(type) => type in fieldTypeNames
);
const fieldGroups: FieldsGroup = useMemo(() => {
const containsData = (field: IndexPatternField) => {
const fieldByName = indexBy(allFields, 'name');
const overallField = fieldByName[field.name];
const displayedFields = allFields.filter((field) => {
if (!supportedFieldTypes.has(field.type)) {
return false;
return (
overallField && fieldExists(existingFields, currentIndexPattern.title, overallField.name)
);
};
const allSupportedTypesFields = allFields.filter((field) =>
supportedFieldTypes.has(field.type)
);
const sorted = allSupportedTypesFields.sort(sortFields);
// optimization before existingFields are synced
if (!hasSyncedExistingFields) {
return {
...defaultFieldGroups,
...groupBy(sorted, (field) => {
if (field.type === 'document') {
return 'specialFields';
} else {
return 'emptyFields';
}
}),
};
}
return {
...defaultFieldGroups,
...groupBy(sorted, (field) => {
if (field.type === 'document') {
return 'specialFields';
} else if (containsData(field)) {
return 'availableFields';
} else return 'emptyFields';
}),
};
}, [allFields, existingFields, currentIndexPattern, hasSyncedExistingFields]);
if (
localState.nameFilter.length &&
!field.name.toLowerCase().includes(localState.nameFilter.toLowerCase())
) {
return false;
}
const filteredFieldGroups: FieldsGroup = useMemo(() => {
const filterFieldGroup = (fieldGroup: IndexPatternField[]) =>
fieldGroup.filter((field) => {
if (
localState.nameFilter.length &&
!field.name.toLowerCase().includes(localState.nameFilter.toLowerCase())
) {
return false;
}
if (!showEmptyFields) {
const indexField = currentIndexPattern && fieldByName[field.name];
const exists =
field.type === 'document' ||
(indexField && fieldExists(existingFields, currentIndexPattern.title, indexField.name));
if (localState.typeFilter.length > 0) {
return exists && localState.typeFilter.includes(field.type as DataType);
if (localState.typeFilter.length > 0) {
return localState.typeFilter.includes(field.type as DataType);
}
return true;
});
return Object.entries(fieldGroups).reduce((acc, [name, fields]) => {
return {
...acc,
[name]: filterFieldGroup(fields),
};
}, defaultFieldGroups);
}, [fieldGroups, localState.nameFilter, localState.typeFilter]);
const lazyScroll = useCallback(() => {
if (scrollContainer) {
const nearBottom =
scrollContainer.scrollTop + scrollContainer.clientHeight >
scrollContainer.scrollHeight * 0.9;
if (nearBottom) {
const displayedFieldsLength =
(localState.isAvailableAccordionOpen ? filteredFieldGroups.availableFields.length : 0) +
(localState.isEmptyAccordionOpen ? filteredFieldGroups.emptyFields.length : 0);
setPageSize(
Math.max(
PAGINATION_SIZE,
Math.min(pageSize + PAGINATION_SIZE * 0.5, displayedFieldsLength)
)
);
}
}
}, [
scrollContainer,
localState.isAvailableAccordionOpen,
localState.isEmptyAccordionOpen,
filteredFieldGroups,
pageSize,
setPageSize,
]);
return exists;
const [paginatedAvailableFields, paginatedEmptyFields]: [
IndexPatternField[],
IndexPatternField[]
] = useMemo(() => {
const { availableFields, emptyFields } = filteredFieldGroups;
const isAvailableAccordionOpen = localState.isAvailableAccordionOpen;
const isEmptyAccordionOpen = localState.isEmptyAccordionOpen;
if (isAvailableAccordionOpen && isEmptyAccordionOpen) {
if (availableFields.length > pageSize) {
return [availableFields.slice(0, pageSize), []];
} else {
return [availableFields, emptyFields.slice(0, pageSize - availableFields.length)];
}
}
if (isAvailableAccordionOpen && !isEmptyAccordionOpen) {
return [availableFields.slice(0, pageSize), []];
}
if (localState.typeFilter.length > 0) {
return localState.typeFilter.includes(field.type as DataType);
if (!isAvailableAccordionOpen && isEmptyAccordionOpen) {
return [[], emptyFields.slice(0, pageSize)];
}
return [[], []];
}, [
localState.isAvailableAccordionOpen,
localState.isEmptyAccordionOpen,
filteredFieldGroups,
pageSize,
]);
return true;
});
const specialFields = displayedFields.filter((f) => f.type === 'document');
const paginatedFields = displayedFields
.filter((f) => f.type !== 'document')
.sort(sortFields)
.slice(0, pageSize);
const hilight = localState.nameFilter.toLowerCase();
const filterByTypeLabel = i18n.translate('xpack.lens.indexPatterns.filterByTypeLabel', {
defaultMessage: 'Filter by type',
});
const fieldProps = useMemo(
() => ({
core,
data,
indexPattern: currentIndexPattern,
highlight: localState.nameFilter.toLowerCase(),
dateRange,
query,
filters,
}),
[core, data, currentIndexPattern, dateRange, query, filters, localState.nameFilter]
);
return (
<ChildDragDropProvider {...dragDropContext}>
@ -308,7 +384,7 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({
direction="column"
responsive={false}
>
<EuiFlexItem grow={null}>
<EuiFlexItem grow={false}>
<div className="lnsInnerIndexPatternDataPanel__header">
<ChangeIndexPattern
data-test-subj="indexPattern-switcher"
@ -327,58 +403,59 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({
/>
</div>
</EuiFlexItem>
<EuiFlexItem>
<div className="lnsInnerIndexPatternDataPanel__filterWrapper">
<EuiFormControlLayout
icon="search"
fullWidth
clear={{
title: i18n.translate('xpack.lens.indexPatterns.clearFiltersLabel', {
defaultMessage: 'Clear name and type filters',
}),
'aria-label': i18n.translate('xpack.lens.indexPatterns.clearFiltersLabel', {
defaultMessage: 'Clear name and type filters',
}),
onClick: () => {
trackUiEvent('indexpattern_filters_cleared');
clearLocalState();
},
<EuiFlexItem grow={false}>
<EuiFormControlLayout
icon="search"
fullWidth
clear={{
title: i18n.translate('xpack.lens.indexPatterns.clearFiltersLabel', {
defaultMessage: 'Clear name and type filters',
}),
'aria-label': i18n.translate('xpack.lens.indexPatterns.clearFiltersLabel', {
defaultMessage: 'Clear name and type filters',
}),
onClick: () => {
trackUiEvent('indexpattern_filters_cleared');
clearLocalState();
},
}}
>
<input
className="euiFieldText euiFieldText--fullWidth lnsInnerIndexPatternDataPanel__textField"
data-test-subj="lnsIndexPatternFieldSearch"
placeholder={i18n.translate('xpack.lens.indexPatterns.filterByNameLabel', {
defaultMessage: 'Search field names',
description: 'Search the list of fields in the index pattern for the provided text',
})}
value={localState.nameFilter}
onChange={(e) => {
setLocalState({ ...localState, nameFilter: e.target.value });
}}
>
<input
className="euiFieldText euiFieldText--fullWidth lnsInnerIndexPatternDataPanel__textField"
data-test-subj="lnsIndexPatternFieldSearch"
placeholder={i18n.translate('xpack.lens.indexPatterns.filterByNameLabel', {
defaultMessage: 'Search field names',
description:
'Search the list of fields in the index pattern for the provided text',
})}
value={localState.nameFilter}
onChange={(e) => {
setLocalState({ ...localState, nameFilter: e.target.value });
}}
aria-label={i18n.translate('xpack.lens.indexPatterns.filterByNameAriaLabel', {
defaultMessage: 'Search fields',
})}
/>
</EuiFormControlLayout>
</div>
<div className="lnsInnerIndexPatternDataPanel__filtersWrapper">
aria-label={i18n.translate('xpack.lens.indexPatterns.filterByNameAriaLabel', {
defaultMessage: 'Search fields',
})}
/>
</EuiFormControlLayout>
<EuiSpacer size="xs" />
<EuiFilterGroup>
<EuiPopover
id="dataPanelTypeFilter"
panelClassName="euiFilterGroup__popoverPanel"
panelPaddingSize="none"
anchorPosition="rightDown"
anchorPosition="rightUp"
display="block"
isOpen={localState.isTypeFilterOpen}
closePopover={() => setLocalState(() => ({ ...localState, isTypeFilterOpen: false }))}
button={
<EuiFacetButton
<EuiFilterButton
iconType="arrowDown"
isSelected={localState.isTypeFilterOpen}
numFilters={localState.typeFilter.length}
hasActiveFilters={!!localState.typeFilter.length}
numActiveFilters={localState.typeFilter.length}
data-test-subj="lnsIndexPatternFiltersToggle"
className="lnsInnerIndexPatternDataPanel__filterButton"
quantity={localState.typeFilter.length}
icon={<EuiIcon type="filter" />}
isSelected={localState.typeFilter.length ? true : false}
onClick={() => {
setLocalState((s) => ({
...s,
@ -386,11 +463,10 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({
}));
}}
>
{filterByTypeLabel}
</EuiFacetButton>
{fieldFiltersLabel}
</EuiFilterButton>
}
>
<EuiPopoverTitle>{filterByTypeLabel}</EuiPopoverTitle>
<FixedEuiContextMenuPanel
watchedItemProps={['icon', 'disabled']}
data-test-subj="lnsIndexPatternTypeFilterOptions"
@ -416,22 +492,10 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({
</EuiContextMenuItem>
))}
/>
<EuiPopoverFooter>
<EuiSwitch
compressed
checked={!showEmptyFields}
onChange={() => {
trackUiEvent('indexpattern_existence_toggled');
onToggleEmptyFields();
}}
label={i18n.translate('xpack.lens.indexPatterns.toggleEmptyFieldsSwitch', {
defaultMessage: 'Only show fields with data',
})}
data-test-subj="lnsEmptyFilter"
/>
</EuiPopoverFooter>
</EuiPopover>
</div>
</EuiFilterGroup>
</EuiFlexItem>
<EuiFlexItem>
<div
className="lnsInnerIndexPatternDataPanel__listWrapper"
ref={(el) => {
@ -440,101 +504,95 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({
setScrollContainer(el);
}
}}
onScroll={lazyScroll}
onScroll={throttle(lazyScroll, 100)}
>
<div className="lnsInnerIndexPatternDataPanel__list">
{specialFields.map((field) => (
{filteredFieldGroups.specialFields.map((field: IndexPatternField) => (
<FieldItem
core={core}
data={data}
key={field.name}
indexPattern={currentIndexPattern}
{...fieldProps}
exists={!!fieldGroups.availableFields.length}
field={field}
highlight={hilight}
exists={paginatedFields.length > 0}
dateRange={dateRange}
query={query}
filters={filters}
hideDetails={true}
key={field.name}
/>
))}
{specialFields.length > 0 && (
<>
<EuiSpacer size="s" />
<EuiFormLabel>
{i18n.translate('xpack.lens.indexPattern.individualFieldsLabel', {
defaultMessage: 'Individual fields',
})}
</EuiFormLabel>
<EuiSpacer size="s" />
</>
)}
{paginatedFields.map((field) => {
const overallField = fieldByName[field.name];
return (
<FieldItem
core={core}
data={data}
indexPattern={currentIndexPattern}
key={field.name}
field={field}
highlight={hilight}
exists={
overallField &&
fieldExists(existingFields, currentIndexPattern.title, overallField.name)
}
dateRange={dateRange}
query={query}
filters={filters}
/>
);
})}
{paginatedFields.length === 0 && (
<EuiCallOut
size="s"
color="warning"
title={
localState.typeFilter.length || localState.nameFilter.length
? i18n.translate('xpack.lens.indexPatterns.noFilteredFieldsLabel', {
defaultMessage: 'No fields match the current filters.',
})
: showEmptyFields
? i18n.translate('xpack.lens.indexPatterns.noFieldsLabel', {
defaultMessage: 'No fields exist in this index pattern.',
})
: i18n.translate('xpack.lens.indexPatterns.emptyFieldsWithDataLabel', {
defaultMessage: 'Looks like you dont have any data.',
})
}
>
{(!showEmptyFields ||
localState.typeFilter.length ||
localState.nameFilter.length) && (
<>
<strong>
{i18n.translate('xpack.lens.indexPatterns.noFields.tryText', {
defaultMessage: 'Try:',
})}
</strong>
<ul>
<li>
{i18n.translate('xpack.lens.indexPatterns.noFields.extendTimeBullet', {
defaultMessage: 'Extending the time range',
})}
</li>
<li>
{i18n.translate('xpack.lens.indexPatterns.noFields.fieldFilterBullet', {
defaultMessage:
'Using {filterByTypeLabel} {arrow} to show fields without data',
values: { filterByTypeLabel, arrow: '↑' },
})}
</li>
</ul>
</>
)}
</EuiCallOut>
)}
<EuiSpacer size="s" />
<FieldsAccordion
initialIsOpen={localState.isAvailableAccordionOpen}
id="lnsIndexPatternAvailableFields"
label={i18n.translate('xpack.lens.indexPattern.availableFieldsLabel', {
defaultMessage: 'Available fields',
})}
exists={true}
hasLoaded={!!hasSyncedExistingFields}
fieldsCount={filteredFieldGroups.availableFields.length}
isFiltered={
filteredFieldGroups.availableFields.length !== fieldGroups.availableFields.length
}
paginatedFields={paginatedAvailableFields}
fieldProps={fieldProps}
onToggle={(open) => {
setLocalState((s) => ({
...s,
isAvailableAccordionOpen: open,
}));
const displayedFieldLength =
(open ? filteredFieldGroups.availableFields.length : 0) +
(localState.isEmptyAccordionOpen ? filteredFieldGroups.emptyFields.length : 0);
setPageSize(
Math.max(PAGINATION_SIZE, Math.min(pageSize * 1.5, displayedFieldLength))
);
}}
renderCallout={
<NoFieldsCallout
isAffectedByGlobalFilter={!!filters.length}
isAffectedByFieldFilter={
!!(localState.typeFilter.length || localState.nameFilter.length)
}
isAffectedByTimerange={true}
existFieldsInIndex={!!fieldGroups.emptyFields.length}
/>
}
/>
<EuiSpacer size="m" />
<FieldsAccordion
initialIsOpen={localState.isEmptyAccordionOpen}
isFiltered={
filteredFieldGroups.emptyFields.length !== fieldGroups.emptyFields.length
}
fieldsCount={filteredFieldGroups.emptyFields.length}
paginatedFields={paginatedEmptyFields}
hasLoaded={!!hasSyncedExistingFields}
exists={false}
fieldProps={fieldProps}
id="lnsIndexPatternEmptyFields"
label={i18n.translate('xpack.lens.indexPattern.emptyFieldsLabel', {
defaultMessage: 'Empty fields',
})}
onToggle={(open) => {
setLocalState((s) => ({
...s,
isEmptyAccordionOpen: open,
}));
const displayedFieldLength =
(localState.isAvailableAccordionOpen
? filteredFieldGroups.availableFields.length
: 0) + (open ? filteredFieldGroups.emptyFields.length : 0);
setPageSize(
Math.max(PAGINATION_SIZE, Math.min(pageSize * 1.5, displayedFieldLength))
);
}}
renderCallout={
<NoFieldsCallout
isAffectedByFieldFilter={
!!(localState.typeFilter.length || localState.nameFilter.length)
}
existFieldsInIndex={!!fieldGroups.emptyFields.length}
/>
}
/>
<EuiSpacer size="m" />
</div>
</div>
</EuiFlexItem>

View file

@ -79,7 +79,6 @@ describe('IndexPatternDimensionEditorPanel', () => {
indexPatternRefs: [],
indexPatterns: expectedIndexPatterns,
currentIndexPatternId: '1',
showEmptyFields: false,
existingFields: {
'my-fake-index-pattern': {
timestamp: true,
@ -1258,7 +1257,6 @@ describe('IndexPatternDimensionEditorPanel', () => {
},
},
currentIndexPatternId: '1',
showEmptyFields: false,
layers: {
myLayer: {
indexPatternId: 'foo',

View file

@ -27,7 +27,6 @@ export interface FieldChoice {
export interface FieldSelectProps {
currentIndexPattern: IndexPattern;
showEmptyFields: boolean;
fieldMap: Record<string, IndexPatternField>;
incompatibleSelectedOperationType: OperationType | null;
selectedColumnOperationType?: OperationType;
@ -40,7 +39,6 @@ export interface FieldSelectProps {
export function FieldSelect({
currentIndexPattern,
showEmptyFields,
fieldMap,
incompatibleSelectedOperationType,
selectedColumnOperationType,
@ -69,6 +67,10 @@ export function FieldSelect({
(field) => fieldMap[field].type === 'document'
);
const containsData = (field: string) =>
fieldMap[field].type === 'document' ||
fieldExists(existingFields, currentIndexPattern.title, field);
function fieldNamesToOptions(items: string[]) {
return items
.map((field) => ({
@ -82,12 +84,9 @@ export function FieldSelect({
? selectedColumnOperationType
: undefined,
},
exists:
fieldMap[field].type === 'document' ||
fieldExists(existingFields, currentIndexPattern.title, field),
exists: containsData(field),
compatible: isCompatibleWithCurrentOperation(field),
}))
.filter((field) => showEmptyFields || field.exists)
.sort((a, b) => {
if (a.compatible && !b.compatible) {
return -1;
@ -108,18 +107,33 @@ export function FieldSelect({
}));
}
const fieldOptions: unknown[] = fieldNamesToOptions(specialFields);
const [availableFields, emptyFields] = _.partition(normalFields, containsData);
if (fields.length > 0) {
fieldOptions.push({
label: i18n.translate('xpack.lens.indexPattern.individualFieldsLabel', {
defaultMessage: 'Individual fields',
}),
options: fieldNamesToOptions(normalFields),
});
}
const constructFieldsOptions = (fieldsArr: string[], label: string) =>
fieldsArr.length > 0 && {
label,
options: fieldNamesToOptions(fieldsArr),
};
return fieldOptions;
const availableFieldsOptions = constructFieldsOptions(
availableFields,
i18n.translate('xpack.lens.indexPattern.availableFieldsLabel', {
defaultMessage: 'Available fields',
})
);
const emptyFieldsOptions = constructFieldsOptions(
emptyFields,
i18n.translate('xpack.lens.indexPattern.emptyFieldsLabel', {
defaultMessage: 'Empty fields',
})
);
return [
...fieldNamesToOptions(specialFields),
availableFieldsOptions,
emptyFieldsOptions,
].filter(Boolean);
}, [
incompatibleSelectedOperationType,
selectedColumnOperationType,
@ -127,7 +141,6 @@ export function FieldSelect({
operationFieldSupportMatrix,
currentIndexPattern,
fieldMap,
showEmptyFields,
]);
return (

View file

@ -200,7 +200,6 @@ export function PopoverEditor(props: PopoverEditorProps) {
<FieldSelect
currentIndexPattern={currentIndexPattern}
existingFields={state.existingFields}
showEmptyFields={state.showEmptyFields}
fieldMap={fieldMap}
operationFieldSupportMatrix={operationFieldSupportMatrix}
selectedColumnOperationType={selectedColumn && selectedColumn.operationType}

View file

@ -7,7 +7,7 @@
import React from 'react';
import { act } from 'react-dom/test-utils';
import { EuiLoadingSpinner, EuiPopover } from '@elastic/eui';
import { FieldItem, FieldItemProps } from './field_item';
import { InnerFieldItem, FieldItemProps } from './field_item';
import { coreMock } from 'src/core/public/mocks';
import { mountWithIntl } from 'test_utils/enzyme_helpers';
import { DataPublicPluginStart } from '../../../../../src/plugins/data/public';
@ -94,7 +94,7 @@ describe('IndexPattern Field Item', () => {
core.http.post.mockImplementationOnce(() => {
return Promise.resolve({});
});
const wrapper = mountWithIntl(<FieldItem {...defaultProps} />);
const wrapper = mountWithIntl(<InnerFieldItem {...defaultProps} />);
await act(async () => {
wrapper.find('[data-test-subj="lnsFieldListPanelField-bytes"]').simulate('click');
@ -119,7 +119,7 @@ describe('IndexPattern Field Item', () => {
});
});
const wrapper = mountWithIntl(<FieldItem {...defaultProps} />);
const wrapper = mountWithIntl(<InnerFieldItem {...defaultProps} />);
wrapper.find('[data-test-subj="lnsFieldListPanelField-bytes"]').simulate('click');

View file

@ -49,6 +49,8 @@ import { IndexPattern, IndexPatternField } from './types';
import { LensFieldIcon } from './lens_field_icon';
import { trackUiEvent } from '../lens_ui_telemetry';
import { debouncedComponent } from '../debounced_component';
export interface FieldItemProps {
core: DatasourceDataPanelProps['core'];
data: DataPublicPluginStart;
@ -78,7 +80,7 @@ function wrapOnDot(str?: string) {
return str ? str.replace(/\./g, '.\u200B') : '';
}
export const FieldItem = React.memo(function FieldItem(props: FieldItemProps) {
export const InnerFieldItem = function InnerFieldItem(props: FieldItemProps) {
const {
core,
field,
@ -239,7 +241,9 @@ export const FieldItem = React.memo(function FieldItem(props: FieldItemProps) {
<FieldItemPopoverContents {...state} {...props} />
</EuiPopover>
);
});
};
export const FieldItem = debouncedComponent(InnerFieldItem);
function FieldItemPopoverContents(props: State & FieldItemProps) {
const {

View file

@ -0,0 +1,97 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { EuiLoadingSpinner, EuiNotificationBadge } from '@elastic/eui';
import { coreMock } from 'src/core/public/mocks';
import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers';
import { DataPublicPluginStart } from '../../../../../src/plugins/data/public';
import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks';
import { IndexPattern } from './types';
import { FieldItem } from './field_item';
import { FieldsAccordion, FieldsAccordionProps, FieldItemSharedProps } from './fields_accordion';
describe('Fields Accordion', () => {
let defaultProps: FieldsAccordionProps;
let indexPattern: IndexPattern;
let core: ReturnType<typeof coreMock['createSetup']>;
let data: DataPublicPluginStart;
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.createSetup();
data = dataPluginMock.createStartContract();
core.http.post.mockClear();
fieldProps = {
indexPattern,
data,
core,
highlight: '',
dateRange: {
fromDate: 'now-7d',
toDate: 'now',
},
query: { query: '', language: 'lucene' },
filters: [],
};
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,
};
});
it('renders correct number of Field Items', () => {
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

@ -0,0 +1,101 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import './datapanel.scss';
import React, { memo, useCallback } from 'react';
import {
EuiText,
EuiNotificationBadge,
EuiSpacer,
EuiAccordion,
EuiLoadingSpinner,
} from '@elastic/eui';
import { DataPublicPluginStart } from 'src/plugins/data/public';
import { IndexPatternField } from './types';
import { FieldItem } from './field_item';
import { Query, Filter } from '../../../../../src/plugins/data/public';
import { DatasourceDataPanelProps } from '../types';
import { IndexPattern } from './types';
export interface FieldItemSharedProps {
core: DatasourceDataPanelProps['core'];
data: DataPublicPluginStart;
indexPattern: IndexPattern;
highlight?: string;
query: Query;
dateRange: DatasourceDataPanelProps['dateRange'];
filters: Filter[];
}
export interface FieldsAccordionProps {
initialIsOpen: boolean;
onToggle: (open: boolean) => void;
id: string;
label: string;
hasLoaded: boolean;
fieldsCount: number;
isFiltered: boolean;
paginatedFields: IndexPatternField[];
fieldProps: FieldItemSharedProps;
renderCallout: JSX.Element;
exists: boolean;
}
export const InnerFieldsAccordion = function InnerFieldsAccordion({
initialIsOpen,
onToggle,
id,
label,
hasLoaded,
fieldsCount,
isFiltered,
paginatedFields,
fieldProps,
renderCallout,
exists,
}: FieldsAccordionProps) {
const renderField = useCallback(
(field: IndexPatternField) => {
return <FieldItem {...fieldProps} key={field.name} field={field} exists={!!exists} />;
},
[fieldProps, exists]
);
return (
<EuiAccordion
initialIsOpen={initialIsOpen}
onToggle={onToggle}
data-test-subj={id}
id={id}
buttonContent={
<EuiText size="xs">
<strong>{label}</strong>
</EuiText>
}
extraAction={
hasLoaded ? (
<EuiNotificationBadge size="m" color={isFiltered ? 'accent' : 'subdued'}>
{fieldsCount}
</EuiNotificationBadge>
) : (
<EuiLoadingSpinner size="m" />
)
}
>
<EuiSpacer size="s" />
{hasLoaded &&
(!!fieldsCount ? (
<div className="lnsInnerIndexPatternDataPanel__fieldItems">
{paginatedFields && paginatedFields.map(renderField)}
</div>
) : (
renderCallout
))}
</EuiAccordion>
);
};
export const FieldsAccordion = memo(InnerFieldsAccordion);

View file

@ -127,7 +127,6 @@ function stateFromPersistedState(
indexPatterns: expectedIndexPatterns,
indexPatternRefs: [],
existingFields: {},
showEmptyFields: true,
};
}
@ -402,7 +401,6 @@ describe('IndexPattern Data Source', () => {
},
},
currentIndexPatternId: '1',
showEmptyFields: false,
};
expect(indexPatternDatasource.insertLayer(state, 'newLayer')).toEqual({
...state,
@ -423,7 +421,6 @@ describe('IndexPattern Data Source', () => {
const state = {
indexPatternRefs: [],
existingFields: {},
showEmptyFields: false,
indexPatterns: expectedIndexPatterns,
layers: {
first: {
@ -458,7 +455,6 @@ describe('IndexPattern Data Source', () => {
indexPatternDatasource.getLayers({
indexPatternRefs: [],
existingFields: {},
showEmptyFields: false,
indexPatterns: expectedIndexPatterns,
layers: {
first: {
@ -484,7 +480,6 @@ describe('IndexPattern Data Source', () => {
indexPatternDatasource.getMetaData({
indexPatternRefs: [],
existingFields: {},
showEmptyFields: false,
indexPatterns: expectedIndexPatterns,
layers: {
first: {

View file

@ -146,7 +146,6 @@ function testInitialState(): IndexPatternPrivateState {
},
},
},
showEmptyFields: false,
};
}
@ -305,7 +304,6 @@ describe('IndexPattern Data Source suggestions', () => {
indexPatternRefs: [],
existingFields: {},
currentIndexPatternId: '1',
showEmptyFields: false,
indexPatterns: {
1: {
id: '1',
@ -510,7 +508,6 @@ describe('IndexPattern Data Source suggestions', () => {
indexPatternRefs: [],
existingFields: {},
currentIndexPatternId: '1',
showEmptyFields: false,
indexPatterns: {
1: {
id: '1',
@ -1049,7 +1046,6 @@ describe('IndexPattern Data Source suggestions', () => {
it('returns no suggestions if there are no columns', () => {
expect(
getDatasourceSuggestionsFromCurrentState({
showEmptyFields: false,
indexPatternRefs: [],
existingFields: {},
indexPatterns: expectedIndexPatterns,
@ -1355,7 +1351,6 @@ describe('IndexPattern Data Source suggestions', () => {
],
},
},
showEmptyFields: true,
layers: {
first: {
...initialState.layers.first,
@ -1475,7 +1470,6 @@ describe('IndexPattern Data Source suggestions', () => {
],
},
},
showEmptyFields: true,
layers: {
first: {
...initialState.layers.first,
@ -1529,7 +1523,6 @@ describe('IndexPattern Data Source suggestions', () => {
],
},
},
showEmptyFields: true,
layers: {
first: {
...initialState.layers.first,
@ -1560,7 +1553,6 @@ describe('IndexPattern Data Source suggestions', () => {
existingFields: {},
currentIndexPatternId: '1',
indexPatterns: expectedIndexPatterns,
showEmptyFields: true,
layers: {
first: {
...initialState.layers.first,

View file

@ -22,7 +22,6 @@ const initialState: IndexPatternPrivateState = {
],
existingFields: {},
currentIndexPatternId: '1',
showEmptyFields: false,
layers: {
first: {
indexPatternId: '1',

View file

@ -294,7 +294,6 @@ describe('loader', () => {
a: sampleIndexPatterns.a,
},
layers: {},
showEmptyFields: false,
});
expect(storage.set).toHaveBeenCalledWith('lens-settings', {
indexPatternId: 'a',
@ -361,7 +360,6 @@ describe('loader', () => {
b: sampleIndexPatterns.b,
},
layers: {},
showEmptyFields: false,
});
expect(storage.set).toHaveBeenCalledWith('lens-settings', {
indexPatternId: 'b',
@ -414,7 +412,6 @@ describe('loader', () => {
b: sampleIndexPatterns.b,
},
layers: savedState.layers,
showEmptyFields: false,
});
expect(storage.set).toHaveBeenCalledWith('lens-settings', {
@ -432,7 +429,6 @@ describe('loader', () => {
indexPatterns: {},
existingFields: {},
layers: {},
showEmptyFields: true,
};
const storage = createMockStorage({ indexPatternId: 'b' });
@ -467,7 +463,6 @@ describe('loader', () => {
existingFields: {},
indexPatterns: {},
layers: {},
showEmptyFields: true,
};
const storage = createMockStorage({ indexPatternId: 'b' });
@ -525,7 +520,6 @@ describe('loader', () => {
indexPatternId: 'a',
},
},
showEmptyFields: true,
};
const storage = createMockStorage({ indexPatternId: 'a' });
@ -594,7 +588,6 @@ describe('loader', () => {
indexPatternId: 'a',
},
},
showEmptyFields: true,
};
const storage = createMockStorage({ indexPatternId: 'b' });

View file

@ -118,7 +118,6 @@ export async function loadInitialState({
currentIndexPatternId,
indexPatternRefs,
indexPatterns,
showEmptyFields: false,
existingFields: {},
};
}
@ -128,7 +127,6 @@ export async function loadInitialState({
indexPatternRefs,
indexPatterns,
layers: {},
showEmptyFields: false,
existingFields: {},
};
}

View file

@ -0,0 +1,36 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { shallow } from 'enzyme';
import { NoFieldsCallout } from './no_fields_callout';
describe('NoFieldCallout', () => {
it('renders properly for index with no fields', () => {
const component = shallow(
<NoFieldsCallout existFieldsInIndex={false} isAffectedByFieldFilter={false} />
);
expect(component).toMatchSnapshot();
});
it('renders properly when affected by field filters, global filter and timerange', () => {
const component = shallow(
<NoFieldsCallout
existFieldsInIndex={true}
isAffectedByFieldFilter={true}
isAffectedByTimerange={true}
isAffectedByGlobalFilter={true}
/>
);
expect(component).toMatchSnapshot();
});
it('renders properly when affected by field filter', () => {
const component = shallow(
<NoFieldsCallout existFieldsInIndex={true} isAffectedByFieldFilter={true} />
);
expect(component).toMatchSnapshot();
});
});

View file

@ -0,0 +1,75 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { EuiCallOut } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
export const NoFieldsCallout = ({
isAffectedByFieldFilter,
existFieldsInIndex,
isAffectedByTimerange = false,
isAffectedByGlobalFilter = false,
}: {
isAffectedByFieldFilter: boolean;
existFieldsInIndex: boolean;
isAffectedByTimerange?: boolean;
isAffectedByGlobalFilter?: boolean;
}) => {
return (
<EuiCallOut
size="s"
color="warning"
title={
isAffectedByFieldFilter
? i18n.translate('xpack.lens.indexPatterns.noFilteredFieldsLabel', {
defaultMessage: 'No fields match the selected filters.',
})
: existFieldsInIndex
? i18n.translate('xpack.lens.indexPatterns.noDataLabel', {
defaultMessage: `There are no available fields that contain data.`,
})
: i18n.translate('xpack.lens.indexPatterns.noFieldsLabel', {
defaultMessage: 'No fields exist in this index pattern.',
})
}
>
{existFieldsInIndex && (
<>
<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>
) : null}
{isAffectedByGlobalFilter ? (
<li>
{i18n.translate('xpack.lens.indexPatterns.noFields.globalFiltersBullet', {
defaultMessage: 'Changing the global filters',
})}
</li>
) : null}
</ul>
</>
)}
</EuiCallOut>
);
};

View file

@ -51,7 +51,6 @@ describe('date_histogram', () => {
indexPatternRefs: [],
existingFields: {},
currentIndexPatternId: '1',
showEmptyFields: false,
indexPatterns: {
1: {
id: '1',

View file

@ -34,7 +34,6 @@ describe('terms', () => {
indexPatterns: {},
existingFields: {},
currentIndexPatternId: '1',
showEmptyFields: false,
layers: {
first: {
indexPatternId: '1',

View file

@ -147,7 +147,6 @@ describe('getOperationTypesForField', () => {
indexPatternRefs: [],
existingFields: {},
currentIndexPatternId: '1',
showEmptyFields: false,
indexPatterns: expectedIndexPatterns,
layers: {
first: {

View file

@ -42,7 +42,6 @@ describe('state_helpers', () => {
existingFields: {},
indexPatterns: {},
currentIndexPatternId: '1',
showEmptyFields: false,
layers: {
first: {
indexPatternId: '1',
@ -96,7 +95,6 @@ describe('state_helpers', () => {
existingFields: {},
indexPatterns: {},
currentIndexPatternId: '1',
showEmptyFields: false,
layers: {
first: {
indexPatternId: '1',
@ -147,7 +145,6 @@ describe('state_helpers', () => {
existingFields: {},
indexPatterns: {},
currentIndexPatternId: '1',
showEmptyFields: false,
layers: {
first: {
indexPatternId: '1',
@ -188,7 +185,6 @@ describe('state_helpers', () => {
existingFields: {},
indexPatterns: {},
currentIndexPatternId: '1',
showEmptyFields: false,
layers: {
first: {
indexPatternId: '1',
@ -222,7 +218,6 @@ describe('state_helpers', () => {
existingFields: {},
indexPatterns: {},
currentIndexPatternId: '1',
showEmptyFields: false,
layers: {
first: {
indexPatternId: '1',
@ -284,7 +279,6 @@ describe('state_helpers', () => {
existingFields: {},
indexPatterns: {},
currentIndexPatternId: '1',
showEmptyFields: false,
layers: {
first: {
indexPatternId: '1',
@ -337,7 +331,6 @@ describe('state_helpers', () => {
existingFields: {},
indexPatterns: {},
currentIndexPatternId: '1',
showEmptyFields: false,
layers: {
first: {
indexPatternId: '1',
@ -417,7 +410,6 @@ describe('state_helpers', () => {
existingFields: {},
indexPatterns: {},
currentIndexPatternId: '1',
showEmptyFields: false,
layers: {
first: {
indexPatternId: '1',

View file

@ -51,7 +51,6 @@ export type IndexPatternPrivateState = IndexPatternPersistedState & {
* indexPatternId -> fieldName -> boolean
*/
existingFields: Record<string, Record<string, boolean>>;
showEmptyFields: boolean;
};
export interface IndexPatternRef {

View file

@ -8651,7 +8651,6 @@
"xpack.lens.indexPattern.groupingSecondDateHistogram": "各 {target} の日付",
"xpack.lens.indexPattern.groupingSecondTerms": "各 {target} のトップの値",
"xpack.lens.indexPattern.indexPatternLoadError": "インデックスパターンの読み込み中にエラーが発生",
"xpack.lens.indexPattern.individualFieldsLabel": "個々のフィールド",
"xpack.lens.indexPattern.invalidInterval": "無効な間隔値",
"xpack.lens.indexPattern.invalidOperationLabel": "この関数を使用するには、別のフィールドを選択してください。",
"xpack.lens.indexPattern.max": "最高",
@ -8682,16 +8681,11 @@
"xpack.lens.indexPattern.termsOf": "{name} のトップの値",
"xpack.lens.indexPattern.uniqueLabel": "{label} [{num}]",
"xpack.lens.indexPatterns.clearFiltersLabel": "名前とタイプフィルターを消去",
"xpack.lens.indexPatterns.emptyFieldsWithDataLabel": "データがないようです。",
"xpack.lens.indexPatterns.filterByNameAriaLabel": "検索フィールド",
"xpack.lens.indexPatterns.filterByNameLabel": "フィールドを検索",
"xpack.lens.indexPatterns.filterByTypeLabel": "タイプでフィルタリング",
"xpack.lens.indexPatterns.noFields.extendTimeBullet": "時間範囲を拡張中",
"xpack.lens.indexPatterns.noFields.fieldFilterBullet": "{filterByTypeLabel} {arrow} を使用してデータなしのフィールドを表示",
"xpack.lens.indexPatterns.noFields.tryText": "試行対象:",
"xpack.lens.indexPatterns.noFieldsLabel": "このインデックスパターンにはフィールドがありません。",
"xpack.lens.indexPatterns.noFilteredFieldsLabel": "現在のフィルターと一致するフィールドはありません。",
"xpack.lens.indexPatterns.toggleEmptyFieldsSwitch": "データがあるフィールドだけを表示",
"xpack.lens.indexPatternSuggestion.removeLayerLabel": "{indexPatternTitle}のみを表示",
"xpack.lens.indexPatternSuggestion.removeLayerPositionLabel": "レイヤー{layerNumber}のみを表示",
"xpack.lens.lensSavedObjectLabel": "レンズビジュアライゼーション",

View file

@ -8655,7 +8655,6 @@
"xpack.lens.indexPattern.groupingSecondDateHistogram": "每个 {target} 的日期",
"xpack.lens.indexPattern.groupingSecondTerms": "每个 {target} 的排名最前值",
"xpack.lens.indexPattern.indexPatternLoadError": "加载索引模式时出错",
"xpack.lens.indexPattern.individualFieldsLabel": "各个字段",
"xpack.lens.indexPattern.invalidInterval": "时间间隔值无效",
"xpack.lens.indexPattern.invalidOperationLabel": "要使用此函数,请选择不同的字段。",
"xpack.lens.indexPattern.max": "最大值",
@ -8686,16 +8685,11 @@
"xpack.lens.indexPattern.termsOf": "{name} 的排名最前值",
"xpack.lens.indexPattern.uniqueLabel": "{label} [{num}]",
"xpack.lens.indexPatterns.clearFiltersLabel": "清除名称和类型筛选",
"xpack.lens.indexPatterns.emptyFieldsWithDataLabel": "似乎您没有任何数据。",
"xpack.lens.indexPatterns.filterByNameAriaLabel": "搜索字段",
"xpack.lens.indexPatterns.filterByNameLabel": "搜索字段",
"xpack.lens.indexPatterns.filterByTypeLabel": "按类型筛选",
"xpack.lens.indexPatterns.noFields.extendTimeBullet": "延伸时间范围",
"xpack.lens.indexPatterns.noFields.fieldFilterBullet": "使用 {filterByTypeLabel} {arrow} 显示没有数据的字段",
"xpack.lens.indexPatterns.noFields.tryText": "尝试:",
"xpack.lens.indexPatterns.noFieldsLabel": "在此索引模式中不存在任何字段。",
"xpack.lens.indexPatterns.noFilteredFieldsLabel": "没有任何字段匹配当前筛选。",
"xpack.lens.indexPatterns.toggleEmptyFieldsSwitch": "仅显示具有数据的字段",
"xpack.lens.indexPatternSuggestion.removeLayerLabel": "仅显示 {indexPatternTitle}",
"xpack.lens.indexPatternSuggestion.removeLayerPositionLabel": "仅显示图层 {layerNumber}",
"xpack.lens.lensSavedObjectLabel": "Lens 可视化",

View file

@ -30,15 +30,6 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont
await testSubjects.click('lnsIndexPatternFiltersToggle');
},
/**
* Toggles the field existence checkbox.
*/
async toggleExistenceFilter() {
await this.toggleIndexPatternFiltersPopover();
await testSubjects.click('lnsEmptyFilter');
await this.toggleIndexPatternFiltersPopover();
},
async findAllFields() {
return await testSubjects.findAll('lnsFieldListPanelField');
},