[Discover Session][Tabs] Tab preview (#214090)

- Closes https://github.com/elastic/kibana/issues/214554

## Summary

This PR:
- adds TabPreview component, which is visible when you hover over a
particular tab
- adds tests for TabPreview component

About TabPreview component
- EUI doesn't have a component, which would suit for our needs, hence a
custom component activated on hover
- TabPreview should activate (with 500ms of delay) after hovering over a
whole tab
- It should hide when we click action button in the tab or we open
editing name mode
- It shouldn't appear on hover when we have tab context menu open or
we're editing a title
- For now the data inside is mocked (besides of title), so you can see
random queries and statuses each time you hover over the tab
- Preview should not overflow the screen if there are a lot of tabs and
they're "touching" right side of the screen


https://github.com/user-attachments/assets/da0a47dd-b594-4c20-b76c-49e6889f3814



## Testing

Two options are possible:

1. start Storybook with `yarn storybook unified_tabs` and navigate to
`http://localhost:9001`.
2. start Kibana with `yarn start --run-examples`. Then navigate to the
Unified Tabs example plugin
`http://localhost:5601/app/unifiedTabsExamples`.

### Checklist
- [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

---------

Co-authored-by: Julia Rechkunova <julia.rechkunova@elastic.co>
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Ania Kowalska 2025-03-20 13:10:56 +01:00 committed by GitHub
parent 38de01504b
commit 748d54ba91
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 462 additions and 36 deletions

View file

@ -25,9 +25,33 @@ import type { DataView } from '@kbn/data-views-plugin/public';
import type { DataViewField } from '@kbn/data-views-plugin/public';
import type { DataViewPickerProps } from '@kbn/unified-search-plugin/public';
import { type TabItem, UnifiedTabs, useNewTabProps } from '@kbn/unified-tabs';
import { type TabPreviewData, TabStatus } from '@kbn/unified-tabs';
import { PLUGIN_ID, PLUGIN_NAME } from '../common';
import { FieldListSidebar, FieldListSidebarProps } from './field_list_sidebar';
// TODO: replace with real data when ready
const TAB_CONTENT_MOCK: TabPreviewData[] = [
{
query: {
esql: 'FROM logs-* | FIND ?findText | WHERE host.name == ?hostName AND log.level == ?logLevel',
},
status: TabStatus.SUCCESS,
},
{
query: {
esql: 'FROM logs-* | FIND ?findText | WHERE host.name == ?hostName AND log.level == ?logLevel',
},
status: TabStatus.RUNNING,
},
{
query: {
language: 'kql',
query: 'agent.name : "activemq-integrations-5f6677988-hjp58"',
},
status: TabStatus.ERROR,
},
];
interface UnifiedTabsExampleAppProps {
services: FieldListSidebarProps['services'];
setHeaderActionMenu: AppMountParameters['setHeaderActionMenu'];
@ -102,6 +126,9 @@ export const UnifiedTabsExampleApp: React.FC<UnifiedTabsExampleAppProps> = ({
services={services}
onChanged={() => {}}
createItem={getNewTabDefaultProps}
getPreviewData={
() => TAB_CONTENT_MOCK[Math.floor(Math.random() * TAB_CONTENT_MOCK.length)] // TODO change mock to real data when ready
}
renderContent={({ label }) => {
return (
<EuiFlexGroup direction="column" gutterSize="none">

View file

@ -7,7 +7,8 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
export type { TabItem } from './src/types';
export type { TabItem, TabPreviewData } from './src/types';
export { TabStatus } from './src/types';
export {
TabbedContent as UnifiedTabs,
type TabbedContentProps as UnifiedTabsProps,

View file

@ -12,6 +12,7 @@ import { render, screen, fireEvent } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Tab } from './tab';
import { MAX_TAB_WIDTH, MIN_TAB_WIDTH } from '../../constants';
import { TabStatus } from '../../types';
import { servicesMock } from '../../../__mocks__/services';
const tabItem = {
@ -28,6 +29,13 @@ const tabsSizeConfig = {
regularTabMinWidth: MIN_TAB_WIDTH,
};
const previewQuery = {
query: {
esql: 'SELECT * FROM table',
},
status: TabStatus.SUCCESS,
};
describe('Tab', () => {
it('renders tab', async () => {
const onLabelEdited = jest.fn();
@ -44,10 +52,13 @@ describe('Tab', () => {
onLabelEdited={onLabelEdited}
onSelect={onSelect}
onClose={onClose}
tabPreviewData={previewQuery}
/>
);
expect(screen.getByText(tabItem.label)).toBeInTheDocument();
const tabButton = screen.getByTestId(tabButtonTestSubj);
expect(tabButton).toBeInTheDocument();
expect(tabButton).toHaveTextContent(tabItem.label);
const tab = screen.getByRole('tab');
expect(tab).toHaveAttribute('id', `tab-${tabItem.id}`);
@ -56,7 +67,6 @@ describe('Tab', () => {
expect(onSelect).toHaveBeenCalled();
expect(onClose).not.toHaveBeenCalled();
const tabButton = screen.getByTestId(tabButtonTestSubj);
tabButton.click();
expect(onSelect).toHaveBeenCalledTimes(2);
@ -88,6 +98,7 @@ describe('Tab', () => {
onLabelEdited={jest.fn()}
onSelect={jest.fn()}
onClose={jest.fn()}
tabPreviewData={previewQuery}
/>
);
@ -117,11 +128,12 @@ describe('Tab', () => {
onLabelEdited={onLabelEdited}
onSelect={onSelect}
onClose={onClose}
tabPreviewData={previewQuery}
/>
);
expect(screen.queryByTestId(tabButtonTestSubj)).toBeInTheDocument();
await userEvent.dblClick(screen.getByText(tabItem.label));
await userEvent.dblClick(screen.getByTestId(tabButtonTestSubj));
expect(onSelect).toHaveBeenCalled();
expect(screen.queryByTestId(tabButtonTestSubj)).not.toBeInTheDocument();
@ -151,11 +163,12 @@ describe('Tab', () => {
onLabelEdited={onLabelEdited}
onSelect={onSelect}
onClose={onClose}
tabPreviewData={previewQuery}
/>
);
expect(screen.queryByTestId(tabButtonTestSubj)).toBeInTheDocument();
await userEvent.dblClick(screen.getByText(tabItem.label));
await userEvent.dblClick(screen.getByTestId(tabButtonTestSubj));
expect(onSelect).toHaveBeenCalled();
expect(screen.queryByTestId(tabButtonTestSubj)).not.toBeInTheDocument();

View file

@ -15,14 +15,21 @@ import {
EuiFlexGroup,
EuiFlexItem,
EuiText,
EuiThemeComputed,
useEuiTheme,
type EuiThemeComputed,
} from '@elastic/eui';
import { TabMenu } from '../tab_menu';
import { EditTabLabel, type EditTabLabelProps } from './edit_tab_label';
import { getTabAttributes } from '../../utils/get_tab_attributes';
import type { TabItem, TabsSizeConfig, GetTabMenuItems, TabsServices } from '../../types';
import type {
TabItem,
TabsSizeConfig,
GetTabMenuItems,
TabsServices,
TabPreviewData,
} from '../../types';
import { TabWithBackground } from '../tabs_visual_glue_to_header/tab_with_background';
import { TabPreview } from '../tab_preview';
export interface TabProps {
item: TabItem;
@ -34,6 +41,7 @@ export interface TabProps {
onLabelEdited: EditTabLabelProps['onLabelEdited'];
onSelect: (item: TabItem) => Promise<void>;
onClose: ((item: TabItem) => Promise<void>) | undefined;
tabPreviewData: TabPreviewData;
}
export const Tab: React.FC<TabProps> = (props) => {
@ -47,10 +55,13 @@ export const Tab: React.FC<TabProps> = (props) => {
onLabelEdited,
onSelect,
onClose,
tabPreviewData,
} = props;
const { euiTheme } = useEuiTheme();
const tabRef = useRef<HTMLDivElement | null>(null);
const [isInlineEditActive, setIsInlineEditActive] = useState<boolean>(false);
const [showPreview, setShowPreview] = useState<boolean>(false);
const [isActionPopoverOpen, setActionPopover] = useState<boolean>(false);
const closeButtonLabel = i18n.translate('unifiedTabs.closeTabButton', {
defaultMessage: 'Close session',
@ -60,9 +71,12 @@ export const Tab: React.FC<TabProps> = (props) => {
defaultMessage: 'Click to select or double-click to edit session name',
});
const hidePreview = () => setShowPreview(false);
const onSelectEvent = useCallback(
async (event: MouseEvent<HTMLElement>) => {
event.stopPropagation();
hidePreview();
if (!isSelected) {
await onSelect(item);
@ -89,6 +103,11 @@ export const Tab: React.FC<TabProps> = (props) => {
[onSelectEvent]
);
const handleDoubleClick = useCallback(() => {
setIsInlineEditActive(true);
hidePreview();
}, []);
const mainTabContent = (
<EuiFlexGroup
alignItems="center"
@ -111,10 +130,9 @@ export const Tab: React.FC<TabProps> = (props) => {
css={getTabButtonCss(euiTheme)}
className="unifiedTabs__tabBtn"
data-test-subj={`unifiedTabs_selectTabBtn_${item.id}`}
title={item.label}
type="button"
onClick={onSelectEvent}
onDoubleClick={() => setIsInlineEditActive(true)}
onDoubleClick={handleDoubleClick}
>
<EuiText color="inherit" size="s" css={getTabLabelCss(euiTheme)}>
{item.label}
@ -124,7 +142,12 @@ export const Tab: React.FC<TabProps> = (props) => {
<EuiFlexGroup responsive={false} direction="row" gutterSize="none">
{!!getTabMenuItems && (
<EuiFlexItem grow={false} className="unifiedTabs__tabMenuBtn">
<TabMenu item={item} getTabMenuItems={getTabMenuItems} />
<TabMenu
item={item}
getTabMenuItems={getTabMenuItems}
isPopoverOpen={isActionPopoverOpen}
setPopover={setActionPopover}
/>
</EuiFlexItem>
)}
{!!onClose && (
@ -148,18 +171,26 @@ export const Tab: React.FC<TabProps> = (props) => {
);
return (
<TabWithBackground
{...getTabAttributes(item, tabContentId)}
ref={tabRef}
role="tab"
aria-selected={isSelected}
data-test-subj={`unifiedTabs_tab_${item.id}`}
isSelected={isSelected}
services={services}
onClick={onClickEvent}
<TabPreview
showPreview={showPreview}
setShowPreview={setShowPreview}
stopPreviewOnHover={isInlineEditActive || isActionPopoverOpen}
tabPreviewData={tabPreviewData}
tabItem={item}
>
{mainTabContent}
</TabWithBackground>
<TabWithBackground
{...getTabAttributes(item, tabContentId)}
ref={tabRef}
role="tab"
aria-selected={isSelected}
data-test-subj={`unifiedTabs_tab_${item.id}`}
isSelected={isSelected}
services={services}
onClick={onClickEvent}
>
{mainTabContent}
</TabWithBackground>
</TabPreview>
);
};

View file

@ -7,7 +7,7 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import React, { useCallback, useMemo, useState } from 'react';
import React, { useCallback, useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import {
EuiButtonIcon,
@ -22,10 +22,16 @@ import type { TabItem, GetTabMenuItems } from '../../types';
export interface TabMenuProps {
item: TabItem;
getTabMenuItems: GetTabMenuItems;
isPopoverOpen: boolean;
setPopover: React.Dispatch<React.SetStateAction<boolean>>;
}
export const TabMenu: React.FC<TabMenuProps> = ({ item, getTabMenuItems }) => {
const [isPopoverOpen, setPopover] = useState<boolean>(false);
export const TabMenu: React.FC<TabMenuProps> = ({
item,
getTabMenuItems,
isPopoverOpen,
setPopover,
}) => {
const contextMenuPopoverId = useGeneratedHtmlId();
const menuButtonLabel = i18n.translate('unifiedTabs.tabMenuButton', {

View file

@ -0,0 +1,10 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
export { TabPreview } from './tab_preview';

View file

@ -0,0 +1,108 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { TabPreview } from './tab_preview';
import type { TabPreviewData, TabItem } from '../../types';
import { TabStatus } from '../../types';
const tabPreviewData: TabPreviewData = {
query: {
esql: 'SELECT * FROM table',
},
status: TabStatus.SUCCESS,
};
const tabItem: TabItem = {
id: 'test-id',
label: 'test-label',
};
const previewTestSubj = `unifiedTabs_tabPreview_${tabItem.id}`;
describe('TabPreview', () => {
it('should call setShowPreview when mouse enters and change opacity after a delay', async () => {
const setShowPreview = jest.fn();
render(
<TabPreview
showPreview={false}
setShowPreview={setShowPreview}
tabPreviewData={tabPreviewData}
tabItem={tabItem}
stopPreviewOnHover={false}
previewDelay={0}
>
<span>Tab Example</span>
</TabPreview>
);
const previewContainer = screen.getByTestId(previewTestSubj);
expect(previewContainer).toBeInTheDocument();
expect(previewContainer).toHaveStyle('opacity: 0');
const tabElement = screen.getByText('Tab Example');
fireEvent.mouseEnter(tabElement);
await waitFor(() => {
expect(setShowPreview).toHaveBeenCalledWith(true);
});
});
it('should call setShowPreview when mouse leaves', async () => {
const setShowPreview = jest.fn();
render(
<TabPreview
showPreview={true}
setShowPreview={setShowPreview}
tabPreviewData={tabPreviewData}
tabItem={tabItem}
stopPreviewOnHover={false}
>
<span>Tab Example</span>
</TabPreview>
);
const previewContainer = screen.getByTestId(previewTestSubj);
expect(previewContainer).toBeInTheDocument();
expect(previewContainer).toHaveStyle('opacity: 1');
const tabElement = screen.getByText('Tab Example');
fireEvent.mouseLeave(tabElement);
await waitFor(() => {
expect(setShowPreview).toHaveBeenCalledWith(false);
});
});
it('should not call setShowPreview when stopPreviewOnHover is true', async () => {
const setShowPreview = jest.fn();
render(
<TabPreview
showPreview={false}
setShowPreview={setShowPreview}
tabPreviewData={tabPreviewData}
tabItem={tabItem}
stopPreviewOnHover={true}
>
<span>Tab Example</span>
</TabPreview>
);
const tabElement = screen.getByText('Tab Example');
fireEvent.mouseEnter(tabElement);
await waitFor(() => {
expect(setShowPreview).not.toHaveBeenCalledWith(true);
});
});
});

View file

@ -0,0 +1,202 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import React, { useCallback, useEffect, useState, useRef } from 'react';
import { css } from '@emotion/react';
import useEvent from 'react-use/lib/useEvent';
import {
EuiSplitPanel,
EuiHealth,
keys,
useEuiTheme,
EuiCodeBlock,
EuiFlexItem,
EuiFlexGroup,
EuiLoadingSpinner,
EuiPortal,
EuiText,
type EuiThemeComputed,
} from '@elastic/eui';
import { isOfAggregateQueryType } from '@kbn/es-query';
import { PREVIEW_WIDTH } from '../../constants';
import type { TabPreviewData, TabItem } from '../../types';
import { TabStatus } from '../../types';
interface TabPreviewProps {
children: React.ReactNode;
showPreview: boolean;
setShowPreview: (show: boolean) => void;
tabItem: TabItem;
tabPreviewData: TabPreviewData;
stopPreviewOnHover?: boolean;
previewDelay?: number;
}
const getQueryLanguage = (tabPreviewData: TabPreviewData) => {
if (isOfAggregateQueryType(tabPreviewData.query)) {
return 'esql';
}
return tabPreviewData.query.language;
};
const getPreviewQuery = (tabPreviewData: TabPreviewData) => {
if (isOfAggregateQueryType(tabPreviewData.query)) {
return tabPreviewData.query.esql;
}
return typeof tabPreviewData.query.query === 'string' ? tabPreviewData.query.query : '';
};
export const TabPreview: React.FC<TabPreviewProps> = ({
children,
showPreview,
setShowPreview,
tabItem,
tabPreviewData,
stopPreviewOnHover,
previewDelay = 500,
}) => {
const { euiTheme } = useEuiTheme();
const [previewTimer, setPreviewTimer] = useState<NodeJS.Timeout | null>(null);
const tabRef = useRef<HTMLSpanElement>(null);
const [tabPosition, setTabPosition] = useState({ top: 0, left: 0 });
useEffect(() => {
if (showPreview && tabRef.current) {
const rect = tabRef.current.getBoundingClientRect();
const windowWidth = window.innerWidth;
// Check if preview would extend beyond right edge
const wouldExtendBeyondRight = rect.left + PREVIEW_WIDTH > windowWidth;
// Calculate left position based on screen edge constraints
let leftPosition = rect.left + window.scrollX;
if (wouldExtendBeyondRight) {
// Align right edge of preview with right edge of window
leftPosition = windowWidth - PREVIEW_WIDTH + window.scrollX;
}
setTabPosition({
top: rect.bottom + window.scrollY,
left: leftPosition,
});
}
}, [showPreview]);
const onKeyDown = useCallback(
(event: KeyboardEvent) => {
if (event.key === keys.ESCAPE) {
setShowPreview(false);
}
},
[setShowPreview]
);
useEvent('keydown', onKeyDown);
useEffect(() => {
return () => {
if (previewTimer) {
clearTimeout(previewTimer);
}
};
}, [previewTimer]);
const handleMouseEnter = useCallback(() => {
if (stopPreviewOnHover) return;
const timer = setTimeout(() => {
setShowPreview(true);
}, previewDelay);
setPreviewTimer(timer);
}, [previewDelay, setShowPreview, stopPreviewOnHover]);
const handleMouseLeave = useCallback(() => {
if (previewTimer) {
clearTimeout(previewTimer);
setPreviewTimer(null);
}
setShowPreview(false);
}, [previewTimer, setShowPreview]);
return (
<div>
<span onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} ref={tabRef}>
{children}
</span>
<EuiPortal>
<EuiSplitPanel.Outer
grow
css={getPreviewContainerCss(euiTheme, showPreview, tabPosition)}
data-test-subj={`unifiedTabs_tabPreview_${tabItem.id}`}
>
<EuiSplitPanel.Inner paddingSize="none" css={getSplitPanelCss(euiTheme)}>
<EuiCodeBlock
language={getQueryLanguage(tabPreviewData)}
transparentBackground
paddingSize="none"
>
{getPreviewQuery(tabPreviewData)}
</EuiCodeBlock>
</EuiSplitPanel.Inner>
<EuiSplitPanel.Inner
grow={false}
color="subdued"
paddingSize="none"
css={getSplitPanelCss(euiTheme)}
>
{tabPreviewData.status === TabStatus.RUNNING ? (
<EuiFlexGroup alignItems="center" gutterSize="s" responsive={false}>
<EuiFlexItem grow={false}>
<EuiLoadingSpinner />
</EuiFlexItem>
<EuiFlexItem>
<EuiText>{tabItem.label}</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
) : (
<EuiHealth color={tabPreviewData.status} textSize="m">
{tabItem.label}
</EuiHealth>
)}
</EuiSplitPanel.Inner>
</EuiSplitPanel.Outer>
</EuiPortal>
</div>
);
};
const getPreviewContainerCss = (
euiTheme: EuiThemeComputed,
showPreview: boolean,
tabPosition: { top: number; left: number }
) => {
return css`
position: absolute;
top: ${tabPosition.top}px;
left: ${tabPosition.left}px;
z-index: 10000;
margin-top: ${euiTheme.size.xs};
width: ${PREVIEW_WIDTH}px;
opacity: ${showPreview ? 1 : 0};
transition: opacity ${euiTheme.animation.normal} ease;
`;
};
const getSplitPanelCss = (euiTheme: EuiThemeComputed) => {
return css`
padding-inline: ${euiTheme.size.base};
padding-block: ${euiTheme.size.s};
`;
};

View file

@ -21,7 +21,7 @@ import {
closeOtherTabs,
closeTabsToTheRight,
} from '../../utils/manage_tabs';
import type { TabItem, TabsServices } from '../../types';
import type { TabItem, TabsServices, TabPreviewData } from '../../types';
export interface TabbedContentProps extends Pick<TabsBarProps, 'maxItemsCount'> {
initialItems: TabItem[];
@ -31,6 +31,7 @@ export interface TabbedContentProps extends Pick<TabsBarProps, 'maxItemsCount'>
renderContent: (selectedItem: TabItem) => React.ReactNode;
createItem: () => TabItem;
onChanged: (state: TabbedContentState) => void;
getPreviewData: (item: TabItem) => TabPreviewData;
}
export interface TabbedContentState {
@ -46,6 +47,7 @@ export const TabbedContent: React.FC<TabbedContentProps> = ({
renderContent,
createItem,
onChanged,
getPreviewData,
}) => {
const [tabContentId] = useState(() => htmlIdGenerator()());
const [state, _setState] = useState<TabbedContentState>(() => {
@ -134,6 +136,7 @@ export const TabbedContent: React.FC<TabbedContentProps> = ({
onLabelEdited={onLabelEdited}
onSelect={onSelect}
onClose={onClose}
getPreviewData={getPreviewData}
/>
</EuiFlexItem>
{selectedItem ? (

View file

@ -10,6 +10,7 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { TabsBar } from './tabs_bar';
import { TabStatus } from '../../types';
import { servicesMock } from '../../../__mocks__/services';
const items = Array.from({ length: 5 }).map((_, i) => ({
@ -25,9 +26,17 @@ describe('TabsBar', () => {
const onSelect = jest.fn();
const onLabelEdited = jest.fn();
const onClose = jest.fn();
const getPreviewData = jest.fn();
const selectedItem = items[0];
getPreviewData.mockReturnValue({
query: {
esql: 'SELECT * FROM table',
},
status: TabStatus.SUCCESS,
});
render(
<TabsBar
tabContentId={tabContentId}
@ -38,6 +47,7 @@ describe('TabsBar', () => {
onLabelEdited={onLabelEdited}
onSelect={onSelect}
onClose={onClose}
getPreviewData={getPreviewData}
/>
);
@ -46,7 +56,9 @@ describe('TabsBar', () => {
items.forEach((tabItem, index) => {
const tab = tabs[index];
expect(screen.getByText(tabItem.label)).toBeInTheDocument();
const tabButton = screen.getByTestId(`unifiedTabs_selectTabBtn_${tabItem.id}`);
expect(tabButton).toBeInTheDocument();
expect(tabButton).toHaveTextContent(tabItem.label);
expect(tab).toHaveAttribute('id', `tab-${tabItem.id}`);
expect(tab).toHaveAttribute('aria-controls', tabContentId);
expect(tab).toHaveAttribute(
@ -55,7 +67,7 @@ describe('TabsBar', () => {
);
});
const tab = screen.getByText(items[1].label);
const tab = screen.getByTestId(`unifiedTabs_selectTabBtn_${items[1].id}`);
tab.click();
expect(onSelect).toHaveBeenCalled();

View file

@ -12,7 +12,7 @@ import { i18n } from '@kbn/i18n';
import { css } from '@emotion/react';
import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, useEuiTheme } from '@elastic/eui';
import { Tab, type TabProps } from '../tab';
import type { TabItem, TabsServices } from '../../types';
import type { TabItem, TabsServices, TabPreviewData } from '../../types';
import { getTabIdAttribute } from '../../utils/get_tab_attributes';
import { useResponsiveTabs } from '../../hooks/use_responsive_tabs';
import { TabsBarWithBackground } from '../tabs_visual_glue_to_header/tabs_bar_with_background';
@ -30,6 +30,7 @@ export type TabsBarProps = Pick<
maxItemsCount?: number;
services: TabsServices;
onAdd: () => Promise<void>;
getPreviewData: (item: TabItem) => TabPreviewData;
};
export const TabsBar: React.FC<TabsBarProps> = ({
@ -43,6 +44,7 @@ export const TabsBar: React.FC<TabsBarProps> = ({
onLabelEdited,
onSelect,
onClose,
getPreviewData,
}) => {
const { euiTheme } = useEuiTheme();
const [tabsContainerWithPlusElement, setTabsContainerWithPlusElement] =
@ -107,6 +109,7 @@ export const TabsBar: React.FC<TabsBarProps> = ({
onLabelEdited={onLabelEdited}
onSelect={onSelect}
onClose={items.length > 1 ? onClose : undefined} // prevents closing the last tab
tabPreviewData={getPreviewData(item)}
/>
))}
</EuiFlexGroup>

View file

@ -11,3 +11,5 @@ export const MAX_TAB_LABEL_LENGTH = 500;
export const MAX_TAB_WIDTH = 280;
export const MIN_TAB_WIDTH = 96;
export const PREVIEW_WIDTH = 280;

View file

@ -7,6 +7,7 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { AggregateQuery, Query } from '@kbn/es-query';
import type { CoreStart } from '@kbn/core/public';
export interface TabItem {
@ -28,6 +29,19 @@ export interface TabsSizeConfig {
// TODO: extend with possibly different sizes for pinned tabs
}
// TODO status value for now matches EuiHealth colors for mocking simplicity, adjust when real data is available
export enum TabStatus {
SUCCESS = 'success',
RUNNING = 'running',
ERROR = 'danger',
}
// TODO adjust interface when real data is available, this currently types TAB_CONTENT_MOCK
export interface TabPreviewData {
query: AggregateQuery | Query;
status: TabStatus;
}
export type TabMenuItem = TabMenuItemWithClick | 'divider';
export type GetTabMenuItems = (item: TabItem) => TabMenuItem[];

View file

@ -4,12 +4,6 @@
"outDir": "target/types"
},
"include": ["**/*.ts", "**/*.tsx"],
"exclude": [
"target/**/*"
],
"kbn_references": [
"@kbn/i18n",
"@kbn/core-chrome-browser",
"@kbn/core",
]
"exclude": ["target/**/*"],
"kbn_references": ["@kbn/i18n", "@kbn/es-query", "@kbn/core-chrome-browser", "@kbn/core"]
}