diff --git a/examples/discover_customization_examples/public/plugin.tsx b/examples/discover_customization_examples/public/plugin.tsx
index 9368c943532a4a..b66559b020d7a7 100644
--- a/examples/discover_customization_examples/public/plugin.tsx
+++ b/examples/discover_customization_examples/public/plugin.tsx
@@ -12,6 +12,7 @@ import {
EuiFlexItem,
EuiPopover,
EuiWrappingPopover,
+ IconType,
} from '@elastic/eui';
import {
AppNavLinkStatus,
@@ -401,6 +402,28 @@ export class DiscoverCustomizationExamplesPlugin implements Plugin {
},
});
+ customizations.set({
+ id: 'flyout',
+ size: '60%',
+ title: 'Example custom flyout',
+ actions: {
+ getActionItems: () =>
+ Array.from({ length: 5 }, (_, i) => {
+ const index = i + 1;
+ return {
+ id: `action-item-${index}`,
+ enabled: true,
+ label: `Action ${index}`,
+ iconType: ['faceHappy', 'faceNeutral', 'faceSad', 'infinity', 'bell'].at(
+ i
+ ) as IconType,
+ dataTestSubj: `customActionItem${index}`,
+ onClick: () => alert(index),
+ };
+ }),
+ },
+ });
+
return () => {
// eslint-disable-next-line no-console
console.log('Cleaning up Logs explorer customizations');
diff --git a/packages/kbn-unified-data-table/src/components/data_table.scss b/packages/kbn-unified-data-table/src/components/data_table.scss
index e033206635eec0..5c3a6b14ecfa9f 100644
--- a/packages/kbn-unified-data-table/src/components/data_table.scss
+++ b/packages/kbn-unified-data-table/src/components/data_table.scss
@@ -73,14 +73,6 @@
min-height: 0;
}
-.unifiedDataTable__flyoutHeader {
- white-space: nowrap;
-}
-
-.unifiedDataTable__flyoutDocumentNavigation {
- justify-content: flex-end;
-}
-
// We only truncate if the cell is not a control column.
.euiDataGridHeader {
diff --git a/src/plugins/discover/public/components/discover_grid_flyout/discover_grid_flyout.test.tsx b/src/plugins/discover/public/components/discover_grid_flyout/discover_grid_flyout.test.tsx
index 8d4e9bbee4ae10..485a3d2f8a4fe7 100644
--- a/src/plugins/discover/public/components/discover_grid_flyout/discover_grid_flyout.test.tsx
+++ b/src/plugins/discover/public/components/discover_grid_flyout/discover_grid_flyout.test.tsx
@@ -7,8 +7,8 @@
*/
import React from 'react';
+import { EuiButtonIcon, EuiContextMenuItem, EuiPopover } from '@elastic/eui';
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';
@@ -36,6 +36,22 @@ jest.mock('../../customizations', () => ({
useDiscoverCustomization: jest.fn(),
}));
+let mockBreakpointSize: string | null = null;
+
+jest.mock('@elastic/eui', () => {
+ const original = jest.requireActual('@elastic/eui');
+ return {
+ ...original,
+ useIsWithinBreakpoints: jest.fn((breakpoints: string[]) => {
+ if (mockBreakpointSize && breakpoints.includes(mockBreakpointSize)) {
+ return true;
+ }
+
+ return original.useIsWithinBreakpoints(breakpoints);
+ }),
+ };
+});
+
const waitNextTick = () => new Promise((resolve) => setTimeout(resolve, 0));
const waitNextUpdate = async (component: ReactWrapper) => {
@@ -227,7 +243,7 @@ describe('Discover flyout', function () {
const singleDocumentView = findTestSubject(component, 'docTableRowAction');
expect(singleDocumentView.length).toBeFalsy();
const flyoutTitle = findTestSubject(component, 'docTableRowDetailsTitle');
- expect(flyoutTitle.text()).toBe('Expanded row');
+ expect(flyoutTitle.text()).toBe('Row');
});
describe('with applied customizations', () => {
@@ -246,17 +262,32 @@ describe('Discover flyout', function () {
describe('when actions are customized', () => {
it('should display actions added by getActionItems', async () => {
+ mockBreakpointSize = 'xl';
mockFlyoutCustomization.actions = {
getActionItems: jest.fn(() => [
{
id: 'action-item-1',
enabled: true,
- Content: () => Action 1,
+ label: 'Action 1',
+ iconType: 'document',
+ dataTestSubj: 'customActionItem1',
+ onClick: jest.fn(),
},
{
id: 'action-item-2',
enabled: true,
- Content: () => Action 2,
+ label: 'Action 2',
+ iconType: 'document',
+ dataTestSubj: 'customActionItem2',
+ onClick: jest.fn(),
+ },
+ {
+ id: 'action-item-3',
+ enabled: false,
+ label: 'Action 3',
+ iconType: 'document',
+ dataTestSubj: 'customActionItem3',
+ onClick: jest.fn(),
},
]),
};
@@ -268,6 +299,88 @@ describe('Discover flyout', function () {
expect(action1.text()).toBe('Action 1');
expect(action2.text()).toBe('Action 2');
+ expect(findTestSubject(component, 'customActionItem3').exists()).toBe(false);
+ mockBreakpointSize = null;
+ });
+
+ it('should display multiple actions added by getActionItems', async () => {
+ mockFlyoutCustomization.actions = {
+ getActionItems: jest.fn(() =>
+ Array.from({ length: 5 }, (_, i) => ({
+ id: `action-item-${i}`,
+ enabled: true,
+ label: `Action ${i}`,
+ iconType: 'document',
+ dataTestSubj: `customActionItem${i}`,
+ onClick: jest.fn(),
+ }))
+ ),
+ };
+
+ const { component } = await mountComponent({});
+ expect(
+ findTestSubject(component, 'docViewerFlyoutActions')
+ .find(EuiButtonIcon)
+ .map((button) => button.prop('data-test-subj'))
+ ).toEqual([
+ 'docTableRowAction',
+ 'customActionItem0',
+ 'customActionItem1',
+ 'docViewerMoreFlyoutActionsButton',
+ ]);
+
+ act(() => {
+ findTestSubject(component, 'docViewerMoreFlyoutActionsButton').simulate('click');
+ });
+
+ component.update();
+
+ expect(
+ component
+ .find(EuiPopover)
+ .find(EuiContextMenuItem)
+ .map((button) => button.prop('data-test-subj'))
+ ).toEqual(['customActionItem2', 'customActionItem3', 'customActionItem4']);
+ });
+
+ it('should display multiple actions added by getActionItems in mobile view', async () => {
+ mockBreakpointSize = 's';
+
+ mockFlyoutCustomization.actions = {
+ getActionItems: jest.fn(() =>
+ Array.from({ length: 3 }, (_, i) => ({
+ id: `action-item-${i}`,
+ enabled: true,
+ label: `Action ${i}`,
+ iconType: 'document',
+ dataTestSubj: `customActionItem${i}`,
+ onClick: jest.fn(),
+ }))
+ ),
+ };
+
+ const { component } = await mountComponent({});
+ expect(findTestSubject(component, 'docViewerFlyoutActions').length).toBe(0);
+
+ act(() => {
+ findTestSubject(component, 'docViewerMobileActionsButton').simulate('click');
+ });
+
+ component.update();
+
+ expect(
+ component
+ .find(EuiPopover)
+ .find(EuiContextMenuItem)
+ .map((button) => button.prop('data-test-subj'))
+ ).toEqual([
+ 'docTableRowAction',
+ 'customActionItem0',
+ 'customActionItem1',
+ 'customActionItem2',
+ ]);
+
+ mockBreakpointSize = null;
});
it('should allow disabling default actions', async () => {
diff --git a/src/plugins/discover/public/components/discover_grid_flyout/discover_grid_flyout.tsx b/src/plugins/discover/public/components/discover_grid_flyout/discover_grid_flyout.tsx
index 58d60466e17b08..40d47e1292f92f 100644
--- a/src/plugins/discover/public/components/discover_grid_flyout/discover_grid_flyout.tsx
+++ b/src/plugins/discover/public/components/discover_grid_flyout/discover_grid_flyout.tsx
@@ -8,6 +8,7 @@
import React, { useMemo, useCallback } from 'react';
import { i18n } from '@kbn/i18n';
+import { css } from '@emotion/react';
import type { DataView } from '@kbn/data-views-plugin/public';
import {
EuiFlexGroup,
@@ -29,6 +30,7 @@ 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';
+import { DiscoverGridFlyoutActions } from './discover_grid_flyout_actions';
export interface DiscoverGridFlyoutProps {
savedSearchId?: string;
@@ -189,14 +191,13 @@ export function DiscoverGridFlyout({
);
const defaultFlyoutTitle = isPlainRecord
- ? i18n.translate('discover.grid.tableRow.textBasedDetailHeading', {
- defaultMessage: 'Expanded row',
+ ? i18n.translate('discover.grid.tableRow.docViewerTextBasedDetailHeading', {
+ defaultMessage: 'Row',
})
- : i18n.translate('discover.grid.tableRow.detailHeading', {
- defaultMessage: 'Expanded document',
+ : i18n.translate('discover.grid.tableRow.docViewerDetailHeading', {
+ defaultMessage: 'Document',
});
const flyoutTitle = flyoutCustomization?.title ?? defaultFlyoutTitle;
-
const flyoutSize = flyoutCustomization?.size ?? 'm';
return (
@@ -209,17 +210,24 @@ export function DiscoverGridFlyout({
ownFocus={false}
>
-
- {flyoutTitle}
-
-
-
- {!isPlainRecord &&
- flyoutActions.map((action) => action.enabled && )}
+
+
+ {flyoutTitle}
+
+
{activePage !== -1 && (
)}
+ {isPlainRecord || !flyoutActions.length ? null : (
+ <>
+
+
+ >
+ )}
{bodyContent}
diff --git a/src/plugins/discover/public/components/discover_grid_flyout/discover_grid_flyout_actions.tsx b/src/plugins/discover/public/components/discover_grid_flyout/discover_grid_flyout_actions.tsx
new file mode 100644
index 00000000000000..a9b168ef7ae8e3
--- /dev/null
+++ b/src/plugins/discover/public/components/discover_grid_flyout/discover_grid_flyout_actions.tsx
@@ -0,0 +1,201 @@
+/*
+ * 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, { useState } from 'react';
+import { i18n } from '@kbn/i18n';
+import { slice } from 'lodash';
+import { css } from '@emotion/react';
+import {
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiPopover,
+ EuiContextMenuPanel,
+ EuiContextMenuItem,
+ EuiContextMenuItemIcon,
+ useIsWithinBreakpoints,
+ EuiText,
+ EuiButtonEmpty,
+ EuiButtonIcon,
+ EuiPopoverProps,
+ EuiToolTip,
+ useEuiTheme,
+} from '@elastic/eui';
+import type { FlyoutActionItem } from '../../customizations';
+
+const MAX_VISIBLE_ACTIONS_BEFORE_THE_FOLD = 3;
+
+export interface DiscoverGridFlyoutActionsProps {
+ flyoutActions: FlyoutActionItem[];
+}
+
+export function DiscoverGridFlyoutActions({ flyoutActions }: DiscoverGridFlyoutActionsProps) {
+ const { euiTheme } = useEuiTheme();
+ const [isMoreFlyoutActionsPopoverOpen, setIsMoreFlyoutActionsPopoverOpen] =
+ useState(false);
+ const isMobileScreen = useIsWithinBreakpoints(['xs', 's']);
+ const isLargeScreen = useIsWithinBreakpoints(['xl']);
+
+ if (isMobileScreen) {
+ return (
+ setIsMoreFlyoutActionsPopoverOpen(!isMoreFlyoutActionsPopoverOpen)}
+ >
+ {i18n.translate('discover.grid.tableRow.mobileFlyoutActionsButton', {
+ defaultMessage: 'Actions',
+ })}
+
+ }
+ isOpen={isMoreFlyoutActionsPopoverOpen}
+ closePopover={() => setIsMoreFlyoutActionsPopoverOpen(false)}
+ />
+ );
+ }
+
+ const visibleFlyoutActions = slice(flyoutActions, 0, MAX_VISIBLE_ACTIONS_BEFORE_THE_FOLD);
+ const remainingFlyoutActions = slice(
+ flyoutActions,
+ MAX_VISIBLE_ACTIONS_BEFORE_THE_FOLD,
+ flyoutActions.length
+ );
+ const showFlyoutIconsOnly =
+ remainingFlyoutActions.length > 0 || (!isLargeScreen && visibleFlyoutActions.length > 1);
+
+ return (
+
+
+
+
+ {i18n.translate('discover.grid.tableRow.actionsLabel', {
+ defaultMessage: 'Actions',
+ })}
+ :
+
+
+
+ {visibleFlyoutActions.map((action) => (
+
+ {showFlyoutIconsOnly ? (
+
+
+
+ ) : (
+
+ {/* eslint-disable-next-line @elastic/eui/href-or-on-click */}
+
+ {action.label}
+
+
+ )}
+
+ ))}
+ {remainingFlyoutActions.length > 0 && (
+
+
+ setIsMoreFlyoutActionsPopoverOpen(!isMoreFlyoutActionsPopoverOpen)}
+ />
+
+ }
+ isOpen={isMoreFlyoutActionsPopoverOpen}
+ closePopover={() => setIsMoreFlyoutActionsPopoverOpen(false)}
+ />
+
+ )}
+
+ );
+}
+
+function FlyoutActionsPopover({
+ flyoutActions,
+ button,
+ isOpen,
+ closePopover,
+}: {
+ flyoutActions: DiscoverGridFlyoutActionsProps['flyoutActions'];
+ button: EuiPopoverProps['button'];
+ isOpen: EuiPopoverProps['isOpen'];
+ closePopover: EuiPopoverProps['closePopover'];
+}) {
+ return (
+
+ (
+
+ {action.label}
+
+ ))}
+ />
+
+ );
+}
diff --git a/src/plugins/discover/public/components/discover_grid_flyout/use_flyout_actions.tsx b/src/plugins/discover/public/components/discover_grid_flyout/use_flyout_actions.tsx
index fb364995b1c215..e0df28e468003d 100644
--- a/src/plugins/discover/public/components/discover_grid_flyout/use_flyout_actions.tsx
+++ b/src/plugins/discover/public/components/discover_grid_flyout/use_flyout_actions.tsx
@@ -6,35 +6,18 @@
* Side Public License, v 1.
*/
-import React from 'react';
-import {
- EuiButtonEmpty,
- EuiFlexGroup,
- EuiFlexItem,
- EuiHideFor,
- EuiIconTip,
- EuiText,
-} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
-import { FlyoutCustomization } from '../../customizations';
+import { FlyoutActionItem, FlyoutCustomization } from '../../customizations';
import { UseNavigationProps, useNavigationProps } from '../../hooks/use_navigation_props';
interface UseFlyoutActionsParams extends UseNavigationProps {
actions?: FlyoutCustomization['actions'];
}
-interface FlyoutActionProps {
- onClick: React.MouseEventHandler;
- href: string;
-}
-
-const staticViewDocumentItem = {
- id: 'viewDocument',
- enabled: true,
- Content: () => ,
-};
-
-export const useFlyoutActions = ({ actions, ...props }: UseFlyoutActionsParams) => {
+export const useFlyoutActions = ({
+ actions,
+ ...props
+}: UseFlyoutActionsParams): { flyoutActions: FlyoutActionItem[] } => {
const { dataView } = props;
const { singleDocHref, contextViewHref, onOpenSingleDoc, onOpenContextView } =
useNavigationProps(props);
@@ -45,95 +28,35 @@ export const useFlyoutActions = ({ actions, ...props }: UseFlyoutActionsParams)
} = actions?.defaultActions ?? {};
const customActions = [...(actions?.getActionItems?.() ?? [])];
- const flyoutActions = [
+ const flyoutActions: FlyoutActionItem[] = [
{
id: 'singleDocument',
enabled: !viewSingleDocument.disabled,
- Content: () => ,
+ dataTestSubj: 'docTableRowAction',
+ iconType: 'document',
+ href: singleDocHref,
+ onClick: onOpenSingleDoc,
+ label: i18n.translate('discover.grid.tableRow.viewSingleDocumentLinkLabel', {
+ defaultMessage: 'View single document',
+ }),
},
{
id: 'surroundingDocument',
enabled: Boolean(!viewSurroundingDocument.disabled && dataView.isTimeBased() && dataView.id),
- Content: () => ,
+ dataTestSubj: 'docTableRowAction',
+ iconType: 'documents',
+ href: contextViewHref,
+ onClick: onOpenContextView,
+ label: i18n.translate('discover.grid.tableRow.viewSurroundingDocumentsLinkLabel', {
+ defaultMessage: 'View surrounding documents',
+ }),
+ helpText: i18n.translate('discover.grid.tableRow.viewSurroundingDocumentsHover', {
+ defaultMessage:
+ 'Inspect documents that occurred before and after this document. Only pinned filters remain active in the Surrounding documents view.',
+ }),
},
...customActions,
];
- const hasEnabledActions = flyoutActions.some((action) => action.enabled);
-
- if (hasEnabledActions) {
- flyoutActions.unshift(staticViewDocumentItem);
- }
-
- return { flyoutActions, hasEnabledActions };
-};
-
-const ViewDocument = () => {
- return (
-
-
-
-
- {i18n.translate('discover.grid.tableRow.viewText', {
- defaultMessage: 'View:',
- })}
-
-
-
-
- );
-};
-
-const SingleDocument = (props: FlyoutActionProps) => {
- return (
-
-
- {i18n.translate('discover.grid.tableRow.viewSingleDocumentLinkTextSimple', {
- defaultMessage: 'Single document',
- })}
-
-
- );
-};
-
-const SurroundingDocuments = (props: FlyoutActionProps) => {
- return (
-
-
-
- {i18n.translate('discover.grid.tableRow.viewSurroundingDocumentsLinkTextSimple', {
- defaultMessage: 'Surrounding documents',
- })}
-
-
-
-
-
-
- );
+ return { flyoutActions: flyoutActions.filter((action) => action.enabled) };
};
diff --git a/src/plugins/discover/public/customizations/customization_types/flyout_customization.ts b/src/plugins/discover/public/customizations/customization_types/flyout_customization.ts
index a57a538f216423..794711ba17b178 100644
--- a/src/plugins/discover/public/customizations/customization_types/flyout_customization.ts
+++ b/src/plugins/discover/public/customizations/customization_types/flyout_customization.ts
@@ -5,10 +5,10 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
-import { EuiFlyoutProps } from '@elastic/eui';
+import { EuiFlyoutProps, IconType } from '@elastic/eui';
import type { DataTableRecord } from '@kbn/discover-utils/types';
import type { DocViewRenderProps } from '@kbn/unified-doc-viewer/types';
-import React, { type ComponentType } from 'react';
+import React, { type ComponentType, MouseEventHandler } from 'react';
export interface FlyoutDefaultActionItem {
disabled?: boolean;
@@ -21,8 +21,13 @@ export interface FlyoutDefaultActions {
export interface FlyoutActionItem {
id: string;
- Content: React.ElementType;
enabled: boolean;
+ label: string;
+ helpText?: string;
+ iconType: IconType;
+ onClick: (() => void) | MouseEventHandler;
+ href?: string;
+ dataTestSubj?: string;
}
export interface FlyoutContentProps {
diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json
index 7d00d772784e68..0993a567fe06c0 100644
--- a/x-pack/plugins/translations/translations/fr-FR.json
+++ b/x-pack/plugins/translations/translations/fr-FR.json
@@ -2360,12 +2360,8 @@
"discover.grid.flyout.documentNavigation": "Navigation dans le document",
"discover.grid.flyout.toastColumnAdded": "La colonne \"{columnName}\" a été ajoutée.",
"discover.grid.flyout.toastColumnRemoved": "La colonne \"{columnName}\" a été supprimée.",
- "discover.grid.tableRow.detailHeading": "Document développé",
"discover.grid.tableRow.textBasedDetailHeading": "Ligne développée",
- "discover.grid.tableRow.viewSingleDocumentLinkTextSimple": "Document unique",
"discover.grid.tableRow.viewSurroundingDocumentsHover": "Inspectez des documents qui ont été créés avant et après ce document. Seuls les filtres épinglés restent actifs dans la vue Documents relatifs.",
- "discover.grid.tableRow.viewSurroundingDocumentsLinkTextSimple": "Documents relatifs",
- "discover.grid.tableRow.viewText": "Afficher :",
"discover.helpMenu.appName": "Découverte",
"discover.inspectorRequestDataTitleDocuments": "Documents",
"discover.inspectorRequestDataTitleMoreDocuments": "Plus de documents",
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index aeb90ddb6e8abd..7de467b1da61c3 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -2374,12 +2374,8 @@
"discover.grid.flyout.documentNavigation": "ドキュメントナビゲーション",
"discover.grid.flyout.toastColumnAdded": "列'{columnName}'が追加されました",
"discover.grid.flyout.toastColumnRemoved": "列'{columnName}'が削除されました",
- "discover.grid.tableRow.detailHeading": "拡張ドキュメント",
"discover.grid.tableRow.textBasedDetailHeading": "展開された行",
- "discover.grid.tableRow.viewSingleDocumentLinkTextSimple": "1つのドキュメント",
"discover.grid.tableRow.viewSurroundingDocumentsHover": "このドキュメントの前後に出現したドキュメントを検査します。周りのドキュメントビューでは、固定されたフィルターのみがアクティブのままです。",
- "discover.grid.tableRow.viewSurroundingDocumentsLinkTextSimple": "周りのドキュメント",
- "discover.grid.tableRow.viewText": "表示:",
"discover.helpMenu.appName": "Discover",
"discover.inspectorRequestDataTitleDocuments": "ドキュメント",
"discover.inspectorRequestDataTitleMoreDocuments": "その他のドキュメント",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 965af95be52733..97a9fec267ac74 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -2374,12 +2374,8 @@
"discover.grid.flyout.documentNavigation": "文档导航",
"discover.grid.flyout.toastColumnAdded": "已添加列“{columnName}”",
"discover.grid.flyout.toastColumnRemoved": "已移除列“{columnName}”",
- "discover.grid.tableRow.detailHeading": "已展开文档",
"discover.grid.tableRow.textBasedDetailHeading": "已展开行",
- "discover.grid.tableRow.viewSingleDocumentLinkTextSimple": "单个文档",
"discover.grid.tableRow.viewSurroundingDocumentsHover": "检查在此文档之前和之后出现的文档。在周围文档视图中,仅已固定筛选仍处于活动状态。",
- "discover.grid.tableRow.viewSurroundingDocumentsLinkTextSimple": "周围文档",
- "discover.grid.tableRow.viewText": "视图:",
"discover.helpMenu.appName": "Discover",
"discover.inspectorRequestDataTitleDocuments": "文档",
"discover.inspectorRequestDataTitleMoreDocuments": "更多文档",