mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Discover] Enhance flyout customization to update content (#169634)
## 📓 Summary Closes #169394 This PR extends the `flyout` customization extension point to support updating/replacing the content shown in the document flyout. A consumer would need to show/hide/highlight details related to the expanded document, and it might also want to control whether the default content (currently only the UnifiedDocViewer) is shown/hidden. Finally, it could be necessary to perform imperative actions such as adding/removing columns or filter. To get this flexibility at the moment of customizing the content, the existing extension point takes now a `Content` component, where some props are injected. ``` export interface FlyoutContentProps { actions: { setFilter?: DocViewFilterFn; addColumn: (column: string) => void; removeColumn: (column: string) => void; }; doc: DataTableRecord; renderDefaultContent: () => React.ReactNode; } ``` N.B. `renderDefaultContent` is passed as a function instead of a React element to avoid its creation in the Discover flyout in case the consumer doesn't want to display it. Here is a usage example of the new extension point property. ``` customizations.set({ id: 'flyout', Content: ({ actions, doc, renderDefaultContent }) => { return ( <Panel> <HighlightComponent timestamp={doc.flattened['@timestamp']} /> <Columns onAddColumn={actions.addColumns} onAddColumn={actions.removeColumn} /> <Filters onFilter={actions.setFilter} /> {renderDefaultContent()} </Panel> ); }, }); ``` --------- Co-authored-by: Marco Antonio Ghiani <marcoantonio.ghiani@elastic.co>
This commit is contained in:
parent
227d7acae7
commit
002ac40dc1
5 changed files with 195 additions and 71 deletions
|
@ -8,6 +8,7 @@
|
|||
|
||||
import React from 'react';
|
||||
import { findTestSubject } from '@elastic/eui/lib/test';
|
||||
import { EuiFlexItem } from '@elastic/eui';
|
||||
import { mountWithIntl } from '@kbn/test-jest-helpers';
|
||||
import type { Query, AggregateQuery } from '@kbn/es-query';
|
||||
import { DiscoverGridFlyout, DiscoverGridFlyoutProps } from './discover_grid_flyout';
|
||||
|
@ -24,7 +25,6 @@ import { ReactWrapper } from 'enzyme';
|
|||
import { setUnifiedDocViewerServices } from '@kbn/unified-doc-viewer-plugin/public/plugin';
|
||||
import { mockUnifiedDocViewerServices } from '@kbn/unified-doc-viewer-plugin/public/__mocks__';
|
||||
import { FlyoutCustomization, useDiscoverCustomization } from '../../customizations';
|
||||
import { EuiFlexItem } from '@elastic/eui';
|
||||
|
||||
const mockFlyoutCustomization: FlyoutCustomization = {
|
||||
id: 'flyout',
|
||||
|
@ -69,6 +69,9 @@ describe('Discover flyout', function () {
|
|||
},
|
||||
contextLocator: { getRedirectUrl: jest.fn(() => 'mock-context-redirect-url') },
|
||||
singleDocLocator: { getRedirectUrl: jest.fn(() => 'mock-doc-redirect-url') },
|
||||
toastNotifications: {
|
||||
addSuccess: jest.fn(),
|
||||
},
|
||||
} as unknown as DiscoverServices;
|
||||
setUnifiedDocViewerServices(mockUnifiedDocViewerServices);
|
||||
|
||||
|
@ -103,11 +106,12 @@ describe('Discover flyout', function () {
|
|||
const component = mountWithIntl(<Proxy {...props} />);
|
||||
await waitNextUpdate(component);
|
||||
|
||||
return { component, props };
|
||||
return { component, props, services };
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mockFlyoutCustomization.actions.defaultActions = undefined;
|
||||
mockFlyoutCustomization.Content = undefined;
|
||||
jest.clearAllMocks();
|
||||
|
||||
(useDiscoverCustomization as jest.Mock).mockImplementation(() => mockFlyoutCustomization);
|
||||
|
@ -226,44 +230,98 @@ describe('Discover flyout', function () {
|
|||
expect(flyoutTitle.text()).toBe('Expanded row');
|
||||
});
|
||||
|
||||
describe('when customizations actions exists', () => {
|
||||
it('should display actions added by getActionItems', async () => {
|
||||
mockFlyoutCustomization.actions = {
|
||||
getActionItems: jest.fn(() => [
|
||||
{
|
||||
id: 'action-item-1',
|
||||
enabled: true,
|
||||
Content: () => <EuiFlexItem data-test-subj="customActionItem1">Action 1</EuiFlexItem>,
|
||||
describe('with applied customizations', () => {
|
||||
describe('when actions are customized', () => {
|
||||
it('should display actions added by getActionItems', async () => {
|
||||
mockFlyoutCustomization.actions = {
|
||||
getActionItems: jest.fn(() => [
|
||||
{
|
||||
id: 'action-item-1',
|
||||
enabled: true,
|
||||
Content: () => <EuiFlexItem data-test-subj="customActionItem1">Action 1</EuiFlexItem>,
|
||||
},
|
||||
{
|
||||
id: 'action-item-2',
|
||||
enabled: true,
|
||||
Content: () => <EuiFlexItem data-test-subj="customActionItem2">Action 2</EuiFlexItem>,
|
||||
},
|
||||
]),
|
||||
};
|
||||
|
||||
const { component } = await mountComponent({});
|
||||
|
||||
const action1 = findTestSubject(component, 'customActionItem1');
|
||||
const action2 = findTestSubject(component, 'customActionItem2');
|
||||
|
||||
expect(action1.text()).toBe('Action 1');
|
||||
expect(action2.text()).toBe('Action 2');
|
||||
});
|
||||
|
||||
it('should allow disabling default actions', async () => {
|
||||
mockFlyoutCustomization.actions = {
|
||||
defaultActions: {
|
||||
viewSingleDocument: { disabled: true },
|
||||
viewSurroundingDocument: { disabled: true },
|
||||
},
|
||||
{
|
||||
id: 'action-item-2',
|
||||
enabled: true,
|
||||
Content: () => <EuiFlexItem data-test-subj="customActionItem2">Action 2</EuiFlexItem>,
|
||||
},
|
||||
]),
|
||||
};
|
||||
};
|
||||
|
||||
const { component } = await mountComponent({});
|
||||
const { component } = await mountComponent({});
|
||||
|
||||
const action1 = findTestSubject(component, 'customActionItem1');
|
||||
const action2 = findTestSubject(component, 'customActionItem2');
|
||||
|
||||
expect(action1.text()).toBe('Action 1');
|
||||
expect(action2.text()).toBe('Action 2');
|
||||
const singleDocumentView = findTestSubject(component, 'docTableRowAction');
|
||||
expect(singleDocumentView.length).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow disabling default actions', async () => {
|
||||
mockFlyoutCustomization.actions = {
|
||||
defaultActions: {
|
||||
viewSingleDocument: { disabled: true },
|
||||
viewSurroundingDocument: { disabled: true },
|
||||
},
|
||||
};
|
||||
describe('when content is customized', () => {
|
||||
it('should display the component passed to the Content customization', async () => {
|
||||
mockFlyoutCustomization.Content = () => (
|
||||
<span data-test-subj="flyoutCustomContent">Custom content</span>
|
||||
);
|
||||
|
||||
const { component } = await mountComponent({});
|
||||
const { component } = await mountComponent({});
|
||||
|
||||
const singleDocumentView = findTestSubject(component, 'docTableRowAction');
|
||||
expect(singleDocumentView.length).toBeFalsy();
|
||||
const customContent = findTestSubject(component, 'flyoutCustomContent');
|
||||
|
||||
expect(customContent.text()).toBe('Custom content');
|
||||
});
|
||||
|
||||
it('should provide a doc property to display details about the current document overview', async () => {
|
||||
mockFlyoutCustomization.Content = ({ doc }) => {
|
||||
return (
|
||||
<span data-test-subj="flyoutCustomContent">{doc.flattened.message as string}</span>
|
||||
);
|
||||
};
|
||||
|
||||
const { component } = await mountComponent({});
|
||||
|
||||
const customContent = findTestSubject(component, 'flyoutCustomContent');
|
||||
|
||||
expect(customContent.text()).toBe('test1');
|
||||
});
|
||||
|
||||
it('should provide an actions prop collection to optionally update the grid content', async () => {
|
||||
mockFlyoutCustomization.Content = ({ actions }) => (
|
||||
<>
|
||||
<button data-test-subj="addColumn" onClick={() => actions.addColumn('message')} />
|
||||
<button data-test-subj="removeColumn" onClick={() => actions.removeColumn('message')} />
|
||||
<button
|
||||
data-test-subj="addFilter"
|
||||
onClick={() => actions.addFilter?.('_exists_', 'message', '+')}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
const { component, props, services } = await mountComponent({});
|
||||
|
||||
findTestSubject(component, 'addColumn').simulate('click');
|
||||
findTestSubject(component, 'removeColumn').simulate('click');
|
||||
findTestSubject(component, 'addFilter').simulate('click');
|
||||
|
||||
expect(props.onAddColumn).toHaveBeenCalled();
|
||||
expect(props.onRemoveColumn).toHaveBeenCalled();
|
||||
expect(services.toastNotifications.addSuccess).toHaveBeenCalledTimes(2);
|
||||
expect(props.onFilter).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -28,6 +28,7 @@ import { UnifiedDocViewer } from '@kbn/unified-doc-viewer-plugin/public';
|
|||
import { useDiscoverServices } from '../../hooks/use_discover_services';
|
||||
import { isTextBasedQuery } from '../../application/main/utils/is_text_based_query';
|
||||
import { useFlyoutActions } from './use_flyout_actions';
|
||||
import { useDiscoverCustomization } from '../../customizations';
|
||||
|
||||
export interface DiscoverGridFlyoutProps {
|
||||
savedSearchId?: string;
|
||||
|
@ -69,6 +70,8 @@ export function DiscoverGridFlyout({
|
|||
setExpandedDoc,
|
||||
}: DiscoverGridFlyoutProps) {
|
||||
const services = useDiscoverServices();
|
||||
const flyoutCustomization = useDiscoverCustomization('flyout');
|
||||
|
||||
const isPlainRecord = isTextBasedQuery(query);
|
||||
// Get actual hit with updated highlighted searches
|
||||
const actualHit = useMemo(() => hits?.find(({ id }) => id === hit?.id) || hit, [hit, hits]);
|
||||
|
@ -103,6 +106,7 @@ export function DiscoverGridFlyout({
|
|||
);
|
||||
|
||||
const { flyoutActions } = useFlyoutActions({
|
||||
actions: flyoutCustomization?.actions,
|
||||
dataView,
|
||||
rowIndex: hit.raw._index,
|
||||
rowId: hit.raw._id,
|
||||
|
@ -111,6 +115,77 @@ export function DiscoverGridFlyout({
|
|||
savedSearchId,
|
||||
});
|
||||
|
||||
const addColumn = useCallback(
|
||||
(columnName: string) => {
|
||||
onAddColumn(columnName);
|
||||
services.toastNotifications.addSuccess(
|
||||
i18n.translate('discover.grid.flyout.toastColumnAdded', {
|
||||
defaultMessage: `Column '{columnName}' was added`,
|
||||
values: { columnName },
|
||||
})
|
||||
);
|
||||
},
|
||||
[onAddColumn, services.toastNotifications]
|
||||
);
|
||||
|
||||
const removeColumn = useCallback(
|
||||
(columnName: string) => {
|
||||
onRemoveColumn(columnName);
|
||||
services.toastNotifications.addSuccess(
|
||||
i18n.translate('discover.grid.flyout.toastColumnRemoved', {
|
||||
defaultMessage: `Column '{columnName}' was removed`,
|
||||
values: { columnName },
|
||||
})
|
||||
);
|
||||
},
|
||||
[onRemoveColumn, services.toastNotifications]
|
||||
);
|
||||
|
||||
const renderDefaultContent = useCallback(
|
||||
() => (
|
||||
<UnifiedDocViewer
|
||||
columns={columns}
|
||||
columnTypes={columnTypes}
|
||||
dataView={dataView}
|
||||
filter={onFilter}
|
||||
hit={actualHit}
|
||||
onAddColumn={addColumn}
|
||||
onRemoveColumn={removeColumn}
|
||||
textBasedHits={isPlainRecord ? hits : undefined}
|
||||
/>
|
||||
),
|
||||
[
|
||||
actualHit,
|
||||
addColumn,
|
||||
columns,
|
||||
columnTypes,
|
||||
dataView,
|
||||
hits,
|
||||
isPlainRecord,
|
||||
onFilter,
|
||||
removeColumn,
|
||||
]
|
||||
);
|
||||
|
||||
const contentActions = useMemo(
|
||||
() => ({
|
||||
addFilter: onFilter,
|
||||
addColumn,
|
||||
removeColumn,
|
||||
}),
|
||||
[onFilter, addColumn, removeColumn]
|
||||
);
|
||||
|
||||
const bodyContent = flyoutCustomization?.Content ? (
|
||||
<flyoutCustomization.Content
|
||||
actions={contentActions}
|
||||
doc={actualHit}
|
||||
renderDefaultContent={renderDefaultContent}
|
||||
/>
|
||||
) : (
|
||||
renderDefaultContent()
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiPortal>
|
||||
<EuiFlyout
|
||||
|
@ -136,7 +211,6 @@ export function DiscoverGridFlyout({
|
|||
})}
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
<EuiFlexGroup responsive={false} gutterSize="s" alignItems="center">
|
||||
{!isPlainRecord &&
|
||||
|
@ -158,34 +232,7 @@ export function DiscoverGridFlyout({
|
|||
)}
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
<UnifiedDocViewer
|
||||
hit={actualHit}
|
||||
columns={columns}
|
||||
columnTypes={columnTypes}
|
||||
dataView={dataView}
|
||||
filter={onFilter}
|
||||
onRemoveColumn={(columnName: string) => {
|
||||
onRemoveColumn(columnName);
|
||||
services.toastNotifications.addSuccess(
|
||||
i18n.translate('discover.grid.flyout.toastColumnRemoved', {
|
||||
defaultMessage: `Column '{columnName}' was removed`,
|
||||
values: { columnName },
|
||||
})
|
||||
);
|
||||
}}
|
||||
onAddColumn={(columnName: string) => {
|
||||
onAddColumn(columnName);
|
||||
services.toastNotifications.addSuccess(
|
||||
i18n.translate('discover.grid.flyout.toastColumnAdded', {
|
||||
defaultMessage: `Column '{columnName}' was added`,
|
||||
values: { columnName },
|
||||
})
|
||||
);
|
||||
}}
|
||||
textBasedHits={isPlainRecord ? hits : undefined}
|
||||
/>
|
||||
</EuiFlyoutBody>
|
||||
<EuiFlyoutBody>{bodyContent}</EuiFlyoutBody>
|
||||
</EuiFlyout>
|
||||
</EuiPortal>
|
||||
);
|
||||
|
|
|
@ -16,9 +16,13 @@ import {
|
|||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useDiscoverCustomization } from '../../customizations';
|
||||
import { FlyoutCustomization } from '../../customizations';
|
||||
import { UseNavigationProps, useNavigationProps } from '../../hooks/use_navigation_props';
|
||||
|
||||
interface UseFlyoutActionsParams extends UseNavigationProps {
|
||||
actions?: FlyoutCustomization['actions'];
|
||||
}
|
||||
|
||||
interface FlyoutActionProps {
|
||||
onClick: React.MouseEventHandler<Element>;
|
||||
href: string;
|
||||
|
@ -30,18 +34,16 @@ const staticViewDocumentItem = {
|
|||
Content: () => <ViewDocument />,
|
||||
};
|
||||
|
||||
export const useFlyoutActions = (navigationProps: UseNavigationProps) => {
|
||||
const { dataView } = navigationProps;
|
||||
export const useFlyoutActions = ({ actions, ...props }: UseFlyoutActionsParams) => {
|
||||
const { dataView } = props;
|
||||
const { singleDocHref, contextViewHref, onOpenSingleDoc, onOpenContextView } =
|
||||
useNavigationProps(navigationProps);
|
||||
|
||||
const flyoutCustomization = useDiscoverCustomization('flyout');
|
||||
useNavigationProps(props);
|
||||
|
||||
const {
|
||||
viewSingleDocument = { disabled: false },
|
||||
viewSurroundingDocument = { disabled: false },
|
||||
} = flyoutCustomization?.actions?.defaultActions ?? {};
|
||||
const customActions = [...(flyoutCustomization?.actions?.getActionItems?.() ?? [])];
|
||||
} = actions?.defaultActions ?? {};
|
||||
const customActions = [...(actions?.getActionItems?.() ?? [])];
|
||||
|
||||
const flyoutActions = [
|
||||
{
|
||||
|
|
|
@ -6,6 +6,10 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import type { DataTableRecord } from '@kbn/discover-utils/types';
|
||||
import type { DocViewFilterFn } from '@kbn/unified-doc-viewer/types';
|
||||
import React, { type ComponentType } from 'react';
|
||||
|
||||
export interface FlyoutDefaultActionItem {
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
@ -21,10 +25,23 @@ export interface FlyoutActionItem {
|
|||
enabled: boolean;
|
||||
}
|
||||
|
||||
export interface FlyoutContentActions {
|
||||
addFilter?: DocViewFilterFn;
|
||||
addColumn: (column: string) => void;
|
||||
removeColumn: (column: string) => void;
|
||||
}
|
||||
|
||||
export interface FlyoutContentProps {
|
||||
actions: FlyoutContentActions;
|
||||
doc: DataTableRecord;
|
||||
renderDefaultContent: () => React.ReactNode;
|
||||
}
|
||||
|
||||
export interface FlyoutCustomization {
|
||||
id: 'flyout';
|
||||
actions: {
|
||||
defaultActions?: FlyoutDefaultActions;
|
||||
getActionItems?: () => FlyoutActionItem[];
|
||||
};
|
||||
Content?: ComponentType<FlyoutContentProps>;
|
||||
}
|
||||
|
|
|
@ -1725,6 +1725,6 @@
|
|||
"@kbn/ambient-ui-types",
|
||||
"@kbn/ambient-common-types",
|
||||
"@kbn/ambient-storybook-types"
|
||||
]
|
||||
],
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue