mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 10:40:07 -04:00
[UnifiedFieldList][Discover] Create a high level unified field list building block (#160397)
- Closes https://github.com/elastic/kibana/issues/145162 - Closes https://github.com/elastic/kibana/issues/147884 ## Summary This PR creates a wrapper/container component (building block) for unified field list subcomponents:93acc6f707/packages/kbn-unified-field-list/README.md (L5)
Available customization options are listed here:93acc6f707/packages/kbn-unified-field-list/src/types.ts (L116)
It's now integrated [into Discover](93acc6f707/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.tsx (L373)
) and [into example plugin](93acc6f707/examples/unified_field_list_examples/public/field_list_sidebar.tsx (L84)
). Usage of unified field list subcomponents and hooks stays unchanged in Lens plugin as it requires more complex customization (for example Lens uses IndexPattern/IndexPatternField types instead of data view types). Also this PR allows to disable multifields grouping and select a variant (responsive, list only, button only) via `UnifiedFieldListSidebarContainer` properties. There should no visual changes on Discover and Lens pages. Unified Field List Examples plugin will get the same sidebar UI as it's on Discover. ### Checklist - [x] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [x] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [x] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Davis McPhee <davismcphee@hotmail.com> Co-authored-by: Stratoula Kalafateli <efstratia.kalafateli@elastic.co>
This commit is contained in:
parent
d8c8b7b0f0
commit
ea53763028
39 changed files with 1799 additions and 1528 deletions
|
@ -17,7 +17,8 @@
|
|||
"dataViews",
|
||||
"dataViewFieldEditor",
|
||||
"charts",
|
||||
"fieldFormats"
|
||||
"fieldFormats",
|
||||
"uiActions"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { I18nProvider } from '@kbn/i18n-react';
|
||||
import { CoreThemeProvider } from '@kbn/core-theme-browser-internal';
|
||||
import type { AppMountParameters, CoreStart } from '@kbn/core/public';
|
||||
import { AppPluginStartDependencies } from './types';
|
||||
import { UnifiedFieldListExampleApp } from './example_app';
|
||||
|
@ -16,17 +17,18 @@ import { UnifiedFieldListExampleApp } from './example_app';
|
|||
export const renderApp = (
|
||||
core: CoreStart,
|
||||
deps: AppPluginStartDependencies,
|
||||
{ element }: AppMountParameters
|
||||
{ element, theme$ }: AppMountParameters
|
||||
) => {
|
||||
ReactDOM.render(
|
||||
<I18nProvider>
|
||||
<UnifiedFieldListExampleApp
|
||||
services={{
|
||||
core,
|
||||
uiSettings: core.uiSettings,
|
||||
...deps,
|
||||
}}
|
||||
/>
|
||||
<CoreThemeProvider theme$={theme$}>
|
||||
<UnifiedFieldListExampleApp
|
||||
services={{
|
||||
core,
|
||||
...deps,
|
||||
}}
|
||||
/>
|
||||
</CoreThemeProvider>
|
||||
</I18nProvider>,
|
||||
element
|
||||
);
|
||||
|
|
|
@ -7,13 +7,11 @@
|
|||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { css } from '@emotion/react';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiPage,
|
||||
EuiPageBody,
|
||||
EuiPageSidebar,
|
||||
EuiTitle,
|
||||
EuiEmptyPrompt,
|
||||
EuiLoadingLogo,
|
||||
|
@ -38,7 +36,7 @@ export const UnifiedFieldListExampleApp: React.FC<UnifiedFieldListExampleAppProp
|
|||
const [dataView, setDataView] = useState<DataView | null>();
|
||||
const [selectedFieldNames, setSelectedFieldNames] = useState<string[]>([]);
|
||||
|
||||
const onAddFieldToWorkplace = useCallback(
|
||||
const onAddFieldToWorkspace = useCallback(
|
||||
(field: DataViewField) => {
|
||||
setSelectedFieldNames((names) => [...names, field.name]);
|
||||
},
|
||||
|
@ -124,20 +122,13 @@ export const UnifiedFieldListExampleApp: React.FC<UnifiedFieldListExampleAppProp
|
|||
<RootDragDropProvider>
|
||||
<EuiFlexGroup direction="row" alignItems="stretch">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiPageSidebar
|
||||
css={css`
|
||||
flex: 1;
|
||||
width: 320px;
|
||||
`}
|
||||
>
|
||||
<FieldListSidebar
|
||||
services={services}
|
||||
dataView={dataView}
|
||||
selectedFieldNames={selectedFieldNames}
|
||||
onAddFieldToWorkplace={onAddFieldToWorkplace}
|
||||
onRemoveFieldFromWorkspace={onRemoveFieldFromWorkspace}
|
||||
/>
|
||||
</EuiPageSidebar>
|
||||
<FieldListSidebar
|
||||
services={services}
|
||||
dataView={dataView}
|
||||
selectedFieldNames={selectedFieldNames}
|
||||
onAddFieldToWorkspace={onAddFieldToWorkspace}
|
||||
onRemoveFieldFromWorkspace={onRemoveFieldFromWorkspace}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<ExampleDropZone onDropField={onDropFieldToWorkplace} />
|
||||
|
|
|
@ -1,225 +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 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
/*
|
||||
* 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 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import type { DataViewField, DataView } from '@kbn/data-views-plugin/common';
|
||||
import { DragDrop } from '@kbn/dom-drag-drop';
|
||||
import {
|
||||
AddFieldFilterHandler,
|
||||
FieldItemButton,
|
||||
FieldItemButtonProps,
|
||||
FieldPopover,
|
||||
FieldPopoverHeader,
|
||||
FieldsGroupNames,
|
||||
FieldStats,
|
||||
hasQuerySubscriberData,
|
||||
RenderFieldItemParams,
|
||||
useQuerySubscriber,
|
||||
} from '@kbn/unified-field-list';
|
||||
import { generateFilters } from '@kbn/data-plugin/public';
|
||||
import type { CoreStart } from '@kbn/core-lifecycle-browser';
|
||||
import type { AppPluginStartDependencies } from './types';
|
||||
|
||||
export interface FieldListItemProps extends RenderFieldItemParams<DataViewField> {
|
||||
dataView: DataView;
|
||||
services: AppPluginStartDependencies & {
|
||||
core: CoreStart;
|
||||
uiSettings: CoreStart['uiSettings'];
|
||||
};
|
||||
isSelected: boolean;
|
||||
onAddFieldToWorkspace: FieldItemButtonProps<DataViewField>['onAddFieldToWorkspace'];
|
||||
onRemoveFieldFromWorkspace: FieldItemButtonProps<DataViewField>['onRemoveFieldFromWorkspace'];
|
||||
onRefreshFields: () => void;
|
||||
}
|
||||
|
||||
export function FieldListItem({
|
||||
dataView,
|
||||
services,
|
||||
isSelected,
|
||||
field,
|
||||
fieldSearchHighlight,
|
||||
groupIndex,
|
||||
groupName,
|
||||
itemIndex,
|
||||
hideDetails,
|
||||
onRefreshFields,
|
||||
onAddFieldToWorkspace,
|
||||
onRemoveFieldFromWorkspace,
|
||||
}: FieldListItemProps) {
|
||||
const { dataViewFieldEditor, data } = services;
|
||||
const querySubscriberResult = useQuerySubscriber({ data, listenToSearchSessionUpdates: false }); // this example app does not use search sessions
|
||||
const filterManager = data?.query?.filterManager;
|
||||
const [infoIsOpen, setOpen] = useState(false);
|
||||
|
||||
const togglePopover = useCallback(() => {
|
||||
setOpen((value) => !value);
|
||||
}, [setOpen]);
|
||||
|
||||
const closePopover = useCallback(() => {
|
||||
setOpen(false);
|
||||
}, [setOpen]);
|
||||
|
||||
const closeFieldEditor = useRef<() => void | undefined>();
|
||||
const setFieldEditorRef = useCallback((ref: () => void | undefined) => {
|
||||
closeFieldEditor.current = ref;
|
||||
}, []);
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
id: field.name,
|
||||
humanData: { label: field.displayName || field.name },
|
||||
}),
|
||||
[field]
|
||||
);
|
||||
|
||||
const order = useMemo(() => [0, groupIndex, itemIndex], [groupIndex, itemIndex]);
|
||||
|
||||
const addFilterAndClose: AddFieldFilterHandler | undefined = useMemo(
|
||||
() =>
|
||||
filterManager && dataView
|
||||
? (clickedField, values, operation) => {
|
||||
closePopover();
|
||||
const newFilters = generateFilters(
|
||||
filterManager,
|
||||
clickedField,
|
||||
values,
|
||||
operation,
|
||||
dataView
|
||||
);
|
||||
filterManager.addFilters(newFilters);
|
||||
}
|
||||
: undefined,
|
||||
[dataView, filterManager, closePopover]
|
||||
);
|
||||
|
||||
const editFieldAndClose = useMemo(
|
||||
() =>
|
||||
dataView
|
||||
? (fieldName?: string) => {
|
||||
const ref = dataViewFieldEditor.openEditor({
|
||||
ctx: {
|
||||
dataView,
|
||||
},
|
||||
fieldName,
|
||||
onSave: async () => {
|
||||
onRefreshFields();
|
||||
},
|
||||
});
|
||||
if (setFieldEditorRef) {
|
||||
setFieldEditorRef(ref);
|
||||
}
|
||||
closePopover();
|
||||
}
|
||||
: undefined,
|
||||
[dataViewFieldEditor, dataView, setFieldEditorRef, closePopover, onRefreshFields]
|
||||
);
|
||||
|
||||
const removeFieldAndClose = useMemo(
|
||||
() =>
|
||||
dataView
|
||||
? async (fieldName: string) => {
|
||||
const ref = dataViewFieldEditor.openDeleteModal({
|
||||
ctx: {
|
||||
dataView,
|
||||
},
|
||||
fieldName,
|
||||
onDelete: async () => {
|
||||
onRefreshFields();
|
||||
},
|
||||
});
|
||||
if (setFieldEditorRef) {
|
||||
setFieldEditorRef(ref);
|
||||
}
|
||||
closePopover();
|
||||
}
|
||||
: undefined,
|
||||
[dataView, setFieldEditorRef, closePopover, dataViewFieldEditor, onRefreshFields]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const cleanup = () => {
|
||||
if (closeFieldEditor?.current) {
|
||||
closeFieldEditor?.current();
|
||||
}
|
||||
};
|
||||
return () => {
|
||||
// Make sure to close the editor when unmounting
|
||||
cleanup();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<li>
|
||||
<FieldPopover
|
||||
isOpen={infoIsOpen}
|
||||
closePopover={closePopover}
|
||||
button={
|
||||
<DragDrop
|
||||
draggable
|
||||
order={order}
|
||||
value={value}
|
||||
dataTestSubj={`fieldListPanelField-${field.name}`}
|
||||
onDragStart={closePopover}
|
||||
>
|
||||
<FieldItemButton
|
||||
field={field}
|
||||
fieldSearchHighlight={fieldSearchHighlight}
|
||||
size="xs"
|
||||
isActive={infoIsOpen}
|
||||
isEmpty={groupName === FieldsGroupNames.EmptyFields}
|
||||
isSelected={isSelected}
|
||||
onClick={togglePopover}
|
||||
onAddFieldToWorkspace={onAddFieldToWorkspace}
|
||||
onRemoveFieldFromWorkspace={onRemoveFieldFromWorkspace}
|
||||
/>
|
||||
</DragDrop>
|
||||
}
|
||||
renderHeader={() => {
|
||||
return (
|
||||
<FieldPopoverHeader
|
||||
field={field}
|
||||
closePopover={closePopover}
|
||||
onAddFieldToWorkspace={!isSelected ? onAddFieldToWorkspace : undefined}
|
||||
onAddFilter={addFilterAndClose}
|
||||
onEditField={editFieldAndClose}
|
||||
onDeleteField={removeFieldAndClose}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
renderContent={
|
||||
!hideDetails
|
||||
? () => (
|
||||
<>
|
||||
{hasQuerySubscriberData(querySubscriberResult) && (
|
||||
<FieldStats
|
||||
services={services}
|
||||
query={querySubscriberResult.query}
|
||||
filters={querySubscriberResult.filters}
|
||||
fromDate={querySubscriberResult.fromDate}
|
||||
toDate={querySubscriberResult.toDate}
|
||||
dataViewOrDataViewId={dataView}
|
||||
onAddFilter={addFilterAndClose}
|
||||
field={field}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
}
|
|
@ -14,114 +14,86 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useContext, useMemo } from 'react';
|
||||
import type { DataView, DataViewField } from '@kbn/data-views-plugin/public';
|
||||
import React, { useCallback, useContext, useMemo, useRef } from 'react';
|
||||
import type { DataView } from '@kbn/data-views-plugin/public';
|
||||
import { generateFilters } from '@kbn/data-plugin/public';
|
||||
import { ChildDragDropProvider, DragContext } from '@kbn/dom-drag-drop';
|
||||
import {
|
||||
FieldList,
|
||||
FieldListFilters,
|
||||
FieldListGrouped,
|
||||
FieldListGroupedProps,
|
||||
FieldsGroupNames,
|
||||
useExistingFieldsFetcher,
|
||||
useGroupedFields,
|
||||
useQuerySubscriber,
|
||||
UnifiedFieldListSidebarContainer,
|
||||
type UnifiedFieldListSidebarContainerProps,
|
||||
type UnifiedFieldListSidebarContainerApi,
|
||||
type AddFieldFilterHandler,
|
||||
} from '@kbn/unified-field-list';
|
||||
import { FieldListItem, FieldListItemProps } from './field_list_item';
|
||||
import { type CoreStart } from '@kbn/core-lifecycle-browser';
|
||||
import { PLUGIN_ID } from '../common';
|
||||
import { type AppPluginStartDependencies } from './types';
|
||||
|
||||
const getCreationOptions: UnifiedFieldListSidebarContainerProps['getCreationOptions'] = () => {
|
||||
return {
|
||||
originatingApp: PLUGIN_ID,
|
||||
localStorageKeyPrefix: 'examples',
|
||||
timeRangeUpdatesType: 'timefilter',
|
||||
disablePopularFields: true,
|
||||
};
|
||||
};
|
||||
|
||||
export interface FieldListSidebarProps {
|
||||
dataView: DataView;
|
||||
selectedFieldNames: string[];
|
||||
services: FieldListItemProps['services'];
|
||||
onAddFieldToWorkplace: FieldListItemProps['onAddFieldToWorkspace'];
|
||||
onRemoveFieldFromWorkspace: FieldListItemProps['onRemoveFieldFromWorkspace'];
|
||||
services: AppPluginStartDependencies & {
|
||||
core: CoreStart;
|
||||
};
|
||||
onAddFieldToWorkspace: UnifiedFieldListSidebarContainerProps['onAddFieldToWorkspace'];
|
||||
onRemoveFieldFromWorkspace: UnifiedFieldListSidebarContainerProps['onRemoveFieldFromWorkspace'];
|
||||
}
|
||||
|
||||
export const FieldListSidebar: React.FC<FieldListSidebarProps> = ({
|
||||
dataView,
|
||||
selectedFieldNames,
|
||||
services,
|
||||
onAddFieldToWorkplace,
|
||||
onAddFieldToWorkspace,
|
||||
onRemoveFieldFromWorkspace,
|
||||
}) => {
|
||||
const dragDropContext = useContext(DragContext);
|
||||
const allFields = dataView.fields;
|
||||
const activeDataViews = useMemo(() => [dataView], [dataView]);
|
||||
const querySubscriberResult = useQuerySubscriber({
|
||||
data: services.data,
|
||||
listenToSearchSessionUpdates: false, // this example app does not use search sessions
|
||||
});
|
||||
const unifiedFieldListContainerRef = useRef<UnifiedFieldListSidebarContainerApi>(null);
|
||||
const filterManager = services.data?.query?.filterManager;
|
||||
|
||||
const onSelectedFieldFilter = useCallback(
|
||||
(field: DataViewField) => {
|
||||
return selectedFieldNames.includes(field.name);
|
||||
},
|
||||
[selectedFieldNames]
|
||||
const onAddFilter: AddFieldFilterHandler | undefined = useMemo(
|
||||
() =>
|
||||
filterManager && dataView
|
||||
? (clickedField, values, operation) => {
|
||||
const newFilters = generateFilters(
|
||||
filterManager,
|
||||
clickedField,
|
||||
values,
|
||||
operation,
|
||||
dataView
|
||||
);
|
||||
filterManager.addFilters(newFilters);
|
||||
}
|
||||
: undefined,
|
||||
[dataView, filterManager]
|
||||
);
|
||||
|
||||
const { refetchFieldsExistenceInfo, isProcessing } = useExistingFieldsFetcher({
|
||||
dataViews: activeDataViews, // if you need field existence info for more than one data view, you can specify it here
|
||||
query: querySubscriberResult.query,
|
||||
filters: querySubscriberResult.filters,
|
||||
fromDate: querySubscriberResult.fromDate,
|
||||
toDate: querySubscriberResult.toDate,
|
||||
services,
|
||||
});
|
||||
|
||||
const { fieldListFiltersProps, fieldListGroupedProps } = useGroupedFields({
|
||||
dataViewId: dataView.id ?? null,
|
||||
allFields,
|
||||
services,
|
||||
isAffectedByGlobalFilter: Boolean(querySubscriberResult.filters?.length),
|
||||
onSupportedFieldFilter,
|
||||
onSelectedFieldFilter,
|
||||
});
|
||||
|
||||
const onRefreshFields = useCallback(() => {
|
||||
refetchFieldsExistenceInfo();
|
||||
}, [refetchFieldsExistenceInfo]);
|
||||
|
||||
const renderFieldItem: FieldListGroupedProps<DataViewField>['renderFieldItem'] = useCallback(
|
||||
(params) => (
|
||||
<FieldListItem
|
||||
dataView={dataView}
|
||||
services={services}
|
||||
isSelected={
|
||||
params.groupName === FieldsGroupNames.SelectedFields ||
|
||||
selectedFieldNames.includes(params.field.name)
|
||||
}
|
||||
onRefreshFields={onRefreshFields}
|
||||
onAddFieldToWorkspace={onAddFieldToWorkplace}
|
||||
onRemoveFieldFromWorkspace={onRemoveFieldFromWorkspace}
|
||||
{...params}
|
||||
/>
|
||||
),
|
||||
[
|
||||
dataView,
|
||||
services,
|
||||
onRefreshFields,
|
||||
selectedFieldNames,
|
||||
onAddFieldToWorkplace,
|
||||
onRemoveFieldFromWorkspace,
|
||||
]
|
||||
);
|
||||
const onFieldEdited = useCallback(async () => {
|
||||
unifiedFieldListContainerRef.current?.refetchFieldsExistenceInfo();
|
||||
}, [unifiedFieldListContainerRef]);
|
||||
|
||||
return (
|
||||
<ChildDragDropProvider {...dragDropContext}>
|
||||
<FieldList
|
||||
isProcessing={isProcessing}
|
||||
prepend={<FieldListFilters {...fieldListFiltersProps} />}
|
||||
>
|
||||
<FieldListGrouped
|
||||
{...fieldListGroupedProps}
|
||||
renderFieldItem={renderFieldItem}
|
||||
localStorageKeyPrefix="examples"
|
||||
/>
|
||||
</FieldList>
|
||||
<UnifiedFieldListSidebarContainer
|
||||
ref={unifiedFieldListContainerRef}
|
||||
variant="responsive"
|
||||
getCreationOptions={getCreationOptions}
|
||||
services={services}
|
||||
dataView={dataView}
|
||||
allFields={dataView.fields}
|
||||
workspaceSelectedFieldNames={selectedFieldNames}
|
||||
onAddFieldToWorkspace={onAddFieldToWorkspace}
|
||||
onRemoveFieldFromWorkspace={onRemoveFieldFromWorkspace}
|
||||
onAddFilter={onAddFilter}
|
||||
onFieldEdited={onFieldEdited}
|
||||
/>
|
||||
</ChildDragDropProvider>
|
||||
);
|
||||
};
|
||||
|
||||
function onSupportedFieldFilter(field: DataViewField): boolean {
|
||||
return field.name !== '_source';
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
|
|||
import type { ChartsPluginStart } from '@kbn/charts-plugin/public';
|
||||
import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
|
||||
import type { DataViewFieldEditorStart } from '@kbn/data-view-field-editor-plugin/public';
|
||||
import type { UiActionsStart } from '@kbn/ui-actions-plugin/public';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface UnifiedFieldListExamplesPluginSetup {}
|
||||
|
@ -32,4 +33,5 @@ export interface AppPluginStartDependencies {
|
|||
unifiedSearch: UnifiedSearchPublicPluginStart;
|
||||
charts: ChartsPluginStart;
|
||||
fieldFormats: FieldFormatsStart;
|
||||
uiActions: UiActionsStart;
|
||||
}
|
||||
|
|
|
@ -28,5 +28,7 @@
|
|||
"@kbn/field-formats-plugin",
|
||||
"@kbn/data-view-field-editor-plugin",
|
||||
"@kbn/unified-field-list",
|
||||
"@kbn/core-theme-browser-internal",
|
||||
"@kbn/ui-actions-plugin",
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue