From b8c2ddc8a580edf9fcd205037193893ea58869c7 Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Wed, 12 Oct 2022 13:49:19 +0300 Subject: [PATCH 1/2] [Lens]Add selected field accordion to the fields list --- .../datasources/form_based/datapanel.tsx | 20 ++++ .../datasources/form_based/field_list.tsx | 102 +++++++++--------- .../datasources/form_based/form_based.tsx | 16 +++ .../datasources/text_based/datapanel.tsx | 61 +++++------ .../text_based/fields_accordion.tsx | 101 +++++++++++++++++ .../text_based/text_based_languages.tsx | 14 +++ x-pack/plugins/lens/public/types.ts | 2 + 7 files changed, 235 insertions(+), 81 deletions(-) create mode 100644 x-pack/plugins/lens/public/datasources/text_based/fields_accordion.tsx diff --git a/x-pack/plugins/lens/public/datasources/form_based/datapanel.tsx b/x-pack/plugins/lens/public/datasources/form_based/datapanel.tsx index 970db62925fe6..8a7916a01a09a 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/datapanel.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/datapanel.tsx @@ -62,6 +62,7 @@ export type Props = Omit< frame: FramePublicAPI; indexPatternService: IndexPatternServiceAPI; onIndexPatternRefresh: () => void; + layerFields?: string[]; }; function sortFields(fieldA: IndexPatternField, fieldB: IndexPatternField) { @@ -144,6 +145,7 @@ export function FormBasedDataPanel({ frame, onIndexPatternRefresh, usedIndexPatterns, + layerFields, }: Props) { const { indexPatterns, indexPatternRefs, existingFields, isFirstExistenceFetch } = frame.dataViews; @@ -234,6 +236,7 @@ export function FormBasedDataPanel({ indexPatternService={indexPatternService} onIndexPatternRefresh={onIndexPatternRefresh} frame={frame} + layerFields={layerFields} /> )} @@ -282,6 +285,7 @@ export const InnerFormBasedDataPanel = function InnerFormBasedDataPanel({ indexPatternService, frame, onIndexPatternRefresh, + layerFields, }: Omit< DatasourceDataPanelProps, 'state' | 'setState' | 'showNoDataPopover' | 'core' | 'onChangeIndexPattern' | 'usedIndexPatterns' @@ -296,6 +300,7 @@ export const InnerFormBasedDataPanel = function InnerFormBasedDataPanel({ frame: FramePublicAPI; indexPatternFieldEditor: IndexPatternFieldEditorStart; onIndexPatternRefresh: () => void; + layerFields?: string[]; }) { const [localState, setLocalState] = useState({ nameFilter: '', @@ -345,6 +350,7 @@ export const InnerFormBasedDataPanel = function InnerFormBasedDataPanel({ 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, @@ -372,6 +378,19 @@ export const InnerFormBasedDataPanel = function InnerFormBasedDataPanel({ 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, @@ -451,6 +470,7 @@ export const InnerFormBasedDataPanel = function InnerFormBasedDataPanel({ existenceFetchTimeout, currentIndexPattern, existingFieldsForIndexPattern, + layerFields, ]); const fieldGroups: FieldGroups = useMemo(() => { diff --git a/x-pack/plugins/lens/public/datasources/form_based/field_list.tsx b/x-pack/plugins/lens/public/datasources/form_based/field_list.tsx index e2226def27a9a..3c33770a62560 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/field_list.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/field_list.tsx @@ -29,6 +29,7 @@ export type FieldGroups = Record< isAffectedByTimeFilter: boolean; hideDetails?: boolean; defaultNoFieldsMessage?: string; + hideIfEmpty?: boolean; } >; @@ -161,55 +162,58 @@ export const FieldList = React.memo(function FieldList({ )} - {fieldGroupsToShow.map(([key, fieldGroup], index) => ( - - { - 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={ - - } - uiActions={uiActions} - /> - - - ))} + {fieldGroupsToShow.map(([key, fieldGroup], index) => { + if (Boolean(fieldGroup.hideIfEmpty) && !fieldGroup.fields.length) return null; + return ( + + { + 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={ + + } + uiActions={uiActions} + /> + + + ); + })} ); diff --git a/x-pack/plugins/lens/public/datasources/form_based/form_based.tsx b/x-pack/plugins/lens/public/datasources/form_based/form_based.tsx index 984ce87dfefaf..f84cb519ed4a2 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/form_based.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/form_based.tsx @@ -263,11 +263,26 @@ export function getFormBasedDatasource({ }); }, + getUsedFields(state) { + const fields: string[] = []; + Object.values(state?.layers)?.forEach((l) => { + const { columns } = l; + Object.values(columns).forEach((c) => { + if ('sourceField' in c) { + fields.push(c.sourceField); + } + }); + }); + return fields; + }, + toExpression: (state, layerId, indexPatterns) => toExpression(state, layerId, indexPatterns, uiSettings), renderDataPanel(domElement: Element, props: DatasourceDataPanelProps) { const { onChangeIndexPattern, ...otherProps } = props; + const layerFields = formBasedDatasource?.getUsedFields?.(props.state); + render( @@ -292,6 +307,7 @@ export function getFormBasedDatasource({ core={core} uiActions={uiActions} onIndexPatternRefresh={onRefreshIndexPattern} + layerFields={layerFields} /> diff --git a/x-pack/plugins/lens/public/datasources/text_based/datapanel.tsx b/x-pack/plugins/lens/public/datasources/text_based/datapanel.tsx index 925d92be5aa99..1b0699b2eb930 100644 --- a/x-pack/plugins/lens/public/datasources/text_based/datapanel.tsx +++ b/x-pack/plugins/lens/public/datasources/text_based/datapanel.tsx @@ -15,19 +15,18 @@ import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import { isOfAggregateQueryType } from '@kbn/es-query'; import { ExpressionsStart } from '@kbn/expressions-plugin/public'; -import { FieldButton } from '@kbn/react-field'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; -import { DatasourceDataPanelProps, DataType } from '../../types'; +import type { DatasourceDataPanelProps } from '../../types'; import type { TextBasedPrivateState } from './types'; import { getStateFromAggregateQuery } from './utils'; -import { DragDrop } from '../../drag_drop'; -import { LensFieldIcon } from '../../shared_components'; import { ChildDragDropProvider } from '../../drag_drop'; +import { FieldsAccordion } from './fields_accordion'; export type TextBasedDataPanelProps = DatasourceDataPanelProps & { data: DataPublicPluginStart; expressions: ExpressionsStart; dataViews: DataViewsPublicPluginStart; + layerFields?: string[]; }; const htmlId = htmlIdGenerator('datapanel-text-based-languages'); const fieldSearchDescriptionId = htmlId(); @@ -43,9 +42,11 @@ export function TextBasedDataPanel({ dateRange, expressions, dataViews, + layerFields, }: TextBasedDataPanelProps) { const prevQuery = usePrevious(query); const [localState, setLocalState] = useState({ nameFilter: '' }); + const [dataHasLoaded, setDataHasLoaded] = useState(false); const clearLocalState = () => setLocalState((s) => ({ ...s, nameFilter: '' })); useEffect(() => { async function fetchData() { @@ -58,6 +59,7 @@ export function TextBasedDataPanel({ expressions ); + setDataHasLoaded(true); setState(stateFromQuery); } } @@ -76,6 +78,7 @@ export function TextBasedDataPanel({ return true; }); }, [fieldList, localState.nameFilter]); + const usedByLayersFields = fieldList.filter((field) => layerFields?.includes(field.name)); return (
-
    - {filteredFields.length > 0 && - filteredFields.map((field, index) => ( -
  • - - {}} - fieldIcon={} - fieldName={field?.name} - /> - -
  • - ))} -
+ {usedByLayersFields.length > 0 && ( + + )} +
diff --git a/x-pack/plugins/lens/public/datasources/text_based/fields_accordion.tsx b/x-pack/plugins/lens/public/datasources/text_based/fields_accordion.tsx new file mode 100644 index 0000000000000..965ed11da64ba --- /dev/null +++ b/x-pack/plugins/lens/public/datasources/text_based/fields_accordion.tsx @@ -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 + * 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 ( + + {label} + + ); + }, [label]); + + const extraAction = useMemo(() => { + if (hasLoaded) { + return ( + + {fields.length} + + ); + } + + return ; + }, [fields.length, hasLoaded, isFiltered]); + + return ( + <> + +
    + {fields.length > 0 && + fields.map((field, index) => ( +
  • + + {}} + fieldIcon={} + fieldName={field?.name} + /> + +
  • + ))} +
+
+ + + ); +}); diff --git a/x-pack/plugins/lens/public/datasources/text_based/text_based_languages.tsx b/x-pack/plugins/lens/public/datasources/text_based/text_based_languages.tsx index d7897362e86c4..28b96aa20d189 100644 --- a/x-pack/plugins/lens/public/datasources/text_based/text_based_languages.tsx +++ b/x-pack/plugins/lens/public/datasources/text_based/text_based_languages.tsx @@ -327,14 +327,28 @@ export function getTextBasedDatasource({ toExpression: (state, layerId, indexPatterns) => { return toExpression(state, layerId); }, + getUsedFields(state) { + const fields: string[] = []; + Object.values(state?.layers)?.forEach((l) => { + const { columns } = l; + Object.values(columns).forEach((c) => { + if ('fieldName' in c) { + fields.push(c.fieldName); + } + }); + }); + return fields; + }, renderDataPanel(domElement: Element, props: DatasourceDataPanelProps) { + const layerFields = TextBasedDatasource?.getUsedFields?.(props.state); render( , diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index bed1acfad574f..3b1cabb67aa6e 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -282,6 +282,8 @@ export interface Datasource { } ) => T; + getUsedFields?: (state: T) => string[]; + renderDataPanel: ( domElement: Element, props: DatasourceDataPanelProps From 7655d71e27c75cff90c6d32070b5c920d06937ac Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Wed, 12 Oct 2022 14:59:35 +0300 Subject: [PATCH 2/2] Adds some tests --- .../datasources/form_based/datapanel.test.tsx | 25 +++++++++++++++++++ .../datasources/form_based/form_based.test.ts | 14 +++++++++++ .../datasources/form_based/form_based.tsx | 4 +-- .../datasources/text_based/datapanel.test.tsx | 14 +++++++++++ .../text_based/fields_accordion.tsx | 1 + .../text_based/text_based_languages.test.ts | 14 +++++++++++ .../text_based/text_based_languages.tsx | 4 +-- x-pack/plugins/lens/public/types.ts | 2 +- 8 files changed, 73 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/lens/public/datasources/form_based/datapanel.test.tsx b/x-pack/plugins/lens/public/datasources/form_based/datapanel.test.tsx index deb9642e25dc3..e7b0cd6d457a9 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/datapanel.test.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/datapanel.test.tsx @@ -690,6 +690,31 @@ describe('FormBased Data Panel', () => { }), }; }); + + it('should list all selected fields if exist', async () => { + const newProps = { + ...props, + layerFields: ['bytes'], + }; + const wrapper = mountWithIntl(); + expect( + wrapper + .find('[data-test-subj="lnsIndexPatternSelectedFields"]') + .find(FieldItem) + .map((fieldItem) => fieldItem.prop('field').name) + ).toEqual(['bytes']); + }); + + it('should not list the selected fields accordion if no fields given', async () => { + const wrapper = mountWithIntl(); + expect( + wrapper + .find('[data-test-subj="lnsIndexPatternSelectedFields"]') + .find(FieldItem) + .map((fieldItem) => fieldItem.prop('field').name) + ).toEqual([]); + }); + it('should list all supported fields in the pattern sorted alphabetically in groups', async () => { const wrapper = mountWithIntl(); expect(wrapper.find(FieldItem).first().prop('field').displayName).toEqual('Records'); diff --git a/x-pack/plugins/lens/public/datasources/form_based/form_based.test.ts b/x-pack/plugins/lens/public/datasources/form_based/form_based.test.ts index 5a30dad263123..6021cdc1e821e 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/form_based.test.ts +++ b/x-pack/plugins/lens/public/datasources/form_based/form_based.test.ts @@ -298,6 +298,20 @@ describe('IndexPattern Data Source', () => { }); }); + describe('#getSelectedFields', () => { + it('should return the fields used per layer', async () => { + expect(FormBasedDatasource?.getSelectedFields?.(baseState)).toEqual(['op']); + }); + + it('should return empty array for empty layers', async () => { + const state = { + ...baseState, + layers: {}, + }; + expect(FormBasedDatasource?.getSelectedFields?.(state)).toEqual([]); + }); + }); + describe('#toExpression', () => { it('should generate an empty expression when no columns are selected', async () => { const state = FormBasedDatasource.initialize(); diff --git a/x-pack/plugins/lens/public/datasources/form_based/form_based.tsx b/x-pack/plugins/lens/public/datasources/form_based/form_based.tsx index f84cb519ed4a2..67d3acda068c9 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/form_based.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/form_based.tsx @@ -263,7 +263,7 @@ export function getFormBasedDatasource({ }); }, - getUsedFields(state) { + getSelectedFields(state) { const fields: string[] = []; Object.values(state?.layers)?.forEach((l) => { const { columns } = l; @@ -281,7 +281,7 @@ export function getFormBasedDatasource({ renderDataPanel(domElement: Element, props: DatasourceDataPanelProps) { const { onChangeIndexPattern, ...otherProps } = props; - const layerFields = formBasedDatasource?.getUsedFields?.(props.state); + const layerFields = formBasedDatasource?.getSelectedFields?.(props.state); render( diff --git a/x-pack/plugins/lens/public/datasources/text_based/datapanel.test.tsx b/x-pack/plugins/lens/public/datasources/text_based/datapanel.test.tsx index 781b6547f8e15..b5f158ecd453f 100644 --- a/x-pack/plugins/lens/public/datasources/text_based/datapanel.test.tsx +++ b/x-pack/plugins/lens/public/datasources/text_based/datapanel.test.tsx @@ -216,6 +216,20 @@ describe('TextBased Query Languages Data Panel', () => { ).toEqual(['timestamp', 'bytes', 'memory']); }); + it('should not display the selected fields accordion if there are no fields displayed', async () => { + const wrapper = mountWithIntl(); + expect(wrapper.find('[data-test-subj="lnsSelectedFieldsTextBased"]').length).toEqual(0); + }); + + it('should display the selected fields accordion if there are fields displayed', async () => { + const props = { + ...defaultProps, + layerFields: ['memory'], + }; + const wrapper = mountWithIntl(); + expect(wrapper.find('[data-test-subj="lnsSelectedFieldsTextBased"]').length).not.toEqual(0); + }); + it('should list all supported fields in the pattern that match the search input', async () => { const wrapper = mountWithIntl(); const searchBox = wrapper.find('[data-test-subj="lnsTextBasedLangugesFieldSearch"]'); diff --git a/x-pack/plugins/lens/public/datasources/text_based/fields_accordion.tsx b/x-pack/plugins/lens/public/datasources/text_based/fields_accordion.tsx index 965ed11da64ba..23f71e991bc82 100644 --- a/x-pack/plugins/lens/public/datasources/text_based/fields_accordion.tsx +++ b/x-pack/plugins/lens/public/datasources/text_based/fields_accordion.tsx @@ -65,6 +65,7 @@ export const FieldsAccordion = memo(function InnerFieldsAccordion({ id={id} buttonContent={renderButton} extraAction={extraAction} + data-test-subj={id} >
    { }); }); + describe('#getSelectedFields', () => { + it('should return the fields used per layer', async () => { + expect(TextBasedDatasource?.getSelectedFields?.(baseState)).toEqual(['Test 1']); + }); + + it('should return empty array for empty layers', async () => { + const state = { + ...baseState, + layers: {}, + }; + expect(TextBasedDatasource?.getSelectedFields?.(state)).toEqual([]); + }); + }); + describe('#insertLayer', () => { it('should insert an empty layer into the previous state', () => { expect(TextBasedDatasource.insertLayer(baseState, 'newLayer')).toEqual({ diff --git a/x-pack/plugins/lens/public/datasources/text_based/text_based_languages.tsx b/x-pack/plugins/lens/public/datasources/text_based/text_based_languages.tsx index 28b96aa20d189..4b56b633715b4 100644 --- a/x-pack/plugins/lens/public/datasources/text_based/text_based_languages.tsx +++ b/x-pack/plugins/lens/public/datasources/text_based/text_based_languages.tsx @@ -327,7 +327,7 @@ export function getTextBasedDatasource({ toExpression: (state, layerId, indexPatterns) => { return toExpression(state, layerId); }, - getUsedFields(state) { + getSelectedFields(state) { const fields: string[] = []; Object.values(state?.layers)?.forEach((l) => { const { columns } = l; @@ -341,7 +341,7 @@ export function getTextBasedDatasource({ }, renderDataPanel(domElement: Element, props: DatasourceDataPanelProps) { - const layerFields = TextBasedDatasource?.getUsedFields?.(props.state); + const layerFields = TextBasedDatasource?.getSelectedFields?.(props.state); render( { } ) => T; - getUsedFields?: (state: T) => string[]; + getSelectedFields?: (state: T) => string[]; renderDataPanel: ( domElement: Element,