mirror of
https://github.com/elastic/kibana.git
synced 2025-06-28 11:05:39 -04:00
[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:
parent
38de01504b
commit
748d54ba91
14 changed files with 462 additions and 36 deletions
|
@ -25,9 +25,33 @@ import type { DataView } from '@kbn/data-views-plugin/public';
|
||||||
import type { DataViewField } 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 { DataViewPickerProps } from '@kbn/unified-search-plugin/public';
|
||||||
import { type TabItem, UnifiedTabs, useNewTabProps } from '@kbn/unified-tabs';
|
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 { PLUGIN_ID, PLUGIN_NAME } from '../common';
|
||||||
import { FieldListSidebar, FieldListSidebarProps } from './field_list_sidebar';
|
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 {
|
interface UnifiedTabsExampleAppProps {
|
||||||
services: FieldListSidebarProps['services'];
|
services: FieldListSidebarProps['services'];
|
||||||
setHeaderActionMenu: AppMountParameters['setHeaderActionMenu'];
|
setHeaderActionMenu: AppMountParameters['setHeaderActionMenu'];
|
||||||
|
@ -102,6 +126,9 @@ export const UnifiedTabsExampleApp: React.FC<UnifiedTabsExampleAppProps> = ({
|
||||||
services={services}
|
services={services}
|
||||||
onChanged={() => {}}
|
onChanged={() => {}}
|
||||||
createItem={getNewTabDefaultProps}
|
createItem={getNewTabDefaultProps}
|
||||||
|
getPreviewData={
|
||||||
|
() => TAB_CONTENT_MOCK[Math.floor(Math.random() * TAB_CONTENT_MOCK.length)] // TODO change mock to real data when ready
|
||||||
|
}
|
||||||
renderContent={({ label }) => {
|
renderContent={({ label }) => {
|
||||||
return (
|
return (
|
||||||
<EuiFlexGroup direction="column" gutterSize="none">
|
<EuiFlexGroup direction="column" gutterSize="none">
|
||||||
|
|
|
@ -7,7 +7,8 @@
|
||||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
* 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 {
|
export {
|
||||||
TabbedContent as UnifiedTabs,
|
TabbedContent as UnifiedTabs,
|
||||||
type TabbedContentProps as UnifiedTabsProps,
|
type TabbedContentProps as UnifiedTabsProps,
|
||||||
|
|
|
@ -12,6 +12,7 @@ import { render, screen, fireEvent } from '@testing-library/react';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import { Tab } from './tab';
|
import { Tab } from './tab';
|
||||||
import { MAX_TAB_WIDTH, MIN_TAB_WIDTH } from '../../constants';
|
import { MAX_TAB_WIDTH, MIN_TAB_WIDTH } from '../../constants';
|
||||||
|
import { TabStatus } from '../../types';
|
||||||
import { servicesMock } from '../../../__mocks__/services';
|
import { servicesMock } from '../../../__mocks__/services';
|
||||||
|
|
||||||
const tabItem = {
|
const tabItem = {
|
||||||
|
@ -28,6 +29,13 @@ const tabsSizeConfig = {
|
||||||
regularTabMinWidth: MIN_TAB_WIDTH,
|
regularTabMinWidth: MIN_TAB_WIDTH,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const previewQuery = {
|
||||||
|
query: {
|
||||||
|
esql: 'SELECT * FROM table',
|
||||||
|
},
|
||||||
|
status: TabStatus.SUCCESS,
|
||||||
|
};
|
||||||
|
|
||||||
describe('Tab', () => {
|
describe('Tab', () => {
|
||||||
it('renders tab', async () => {
|
it('renders tab', async () => {
|
||||||
const onLabelEdited = jest.fn();
|
const onLabelEdited = jest.fn();
|
||||||
|
@ -44,10 +52,13 @@ describe('Tab', () => {
|
||||||
onLabelEdited={onLabelEdited}
|
onLabelEdited={onLabelEdited}
|
||||||
onSelect={onSelect}
|
onSelect={onSelect}
|
||||||
onClose={onClose}
|
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');
|
const tab = screen.getByRole('tab');
|
||||||
expect(tab).toHaveAttribute('id', `tab-${tabItem.id}`);
|
expect(tab).toHaveAttribute('id', `tab-${tabItem.id}`);
|
||||||
|
@ -56,7 +67,6 @@ describe('Tab', () => {
|
||||||
expect(onSelect).toHaveBeenCalled();
|
expect(onSelect).toHaveBeenCalled();
|
||||||
expect(onClose).not.toHaveBeenCalled();
|
expect(onClose).not.toHaveBeenCalled();
|
||||||
|
|
||||||
const tabButton = screen.getByTestId(tabButtonTestSubj);
|
|
||||||
tabButton.click();
|
tabButton.click();
|
||||||
expect(onSelect).toHaveBeenCalledTimes(2);
|
expect(onSelect).toHaveBeenCalledTimes(2);
|
||||||
|
|
||||||
|
@ -88,6 +98,7 @@ describe('Tab', () => {
|
||||||
onLabelEdited={jest.fn()}
|
onLabelEdited={jest.fn()}
|
||||||
onSelect={jest.fn()}
|
onSelect={jest.fn()}
|
||||||
onClose={jest.fn()}
|
onClose={jest.fn()}
|
||||||
|
tabPreviewData={previewQuery}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -117,11 +128,12 @@ describe('Tab', () => {
|
||||||
onLabelEdited={onLabelEdited}
|
onLabelEdited={onLabelEdited}
|
||||||
onSelect={onSelect}
|
onSelect={onSelect}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
|
tabPreviewData={previewQuery}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(screen.queryByTestId(tabButtonTestSubj)).toBeInTheDocument();
|
expect(screen.queryByTestId(tabButtonTestSubj)).toBeInTheDocument();
|
||||||
await userEvent.dblClick(screen.getByText(tabItem.label));
|
await userEvent.dblClick(screen.getByTestId(tabButtonTestSubj));
|
||||||
expect(onSelect).toHaveBeenCalled();
|
expect(onSelect).toHaveBeenCalled();
|
||||||
expect(screen.queryByTestId(tabButtonTestSubj)).not.toBeInTheDocument();
|
expect(screen.queryByTestId(tabButtonTestSubj)).not.toBeInTheDocument();
|
||||||
|
|
||||||
|
@ -151,11 +163,12 @@ describe('Tab', () => {
|
||||||
onLabelEdited={onLabelEdited}
|
onLabelEdited={onLabelEdited}
|
||||||
onSelect={onSelect}
|
onSelect={onSelect}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
|
tabPreviewData={previewQuery}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(screen.queryByTestId(tabButtonTestSubj)).toBeInTheDocument();
|
expect(screen.queryByTestId(tabButtonTestSubj)).toBeInTheDocument();
|
||||||
await userEvent.dblClick(screen.getByText(tabItem.label));
|
await userEvent.dblClick(screen.getByTestId(tabButtonTestSubj));
|
||||||
expect(onSelect).toHaveBeenCalled();
|
expect(onSelect).toHaveBeenCalled();
|
||||||
expect(screen.queryByTestId(tabButtonTestSubj)).not.toBeInTheDocument();
|
expect(screen.queryByTestId(tabButtonTestSubj)).not.toBeInTheDocument();
|
||||||
|
|
||||||
|
|
|
@ -15,14 +15,21 @@ import {
|
||||||
EuiFlexGroup,
|
EuiFlexGroup,
|
||||||
EuiFlexItem,
|
EuiFlexItem,
|
||||||
EuiText,
|
EuiText,
|
||||||
EuiThemeComputed,
|
|
||||||
useEuiTheme,
|
useEuiTheme,
|
||||||
|
type EuiThemeComputed,
|
||||||
} from '@elastic/eui';
|
} from '@elastic/eui';
|
||||||
import { TabMenu } from '../tab_menu';
|
import { TabMenu } from '../tab_menu';
|
||||||
import { EditTabLabel, type EditTabLabelProps } from './edit_tab_label';
|
import { EditTabLabel, type EditTabLabelProps } from './edit_tab_label';
|
||||||
import { getTabAttributes } from '../../utils/get_tab_attributes';
|
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 { TabWithBackground } from '../tabs_visual_glue_to_header/tab_with_background';
|
||||||
|
import { TabPreview } from '../tab_preview';
|
||||||
|
|
||||||
export interface TabProps {
|
export interface TabProps {
|
||||||
item: TabItem;
|
item: TabItem;
|
||||||
|
@ -34,6 +41,7 @@ export interface TabProps {
|
||||||
onLabelEdited: EditTabLabelProps['onLabelEdited'];
|
onLabelEdited: EditTabLabelProps['onLabelEdited'];
|
||||||
onSelect: (item: TabItem) => Promise<void>;
|
onSelect: (item: TabItem) => Promise<void>;
|
||||||
onClose: ((item: TabItem) => Promise<void>) | undefined;
|
onClose: ((item: TabItem) => Promise<void>) | undefined;
|
||||||
|
tabPreviewData: TabPreviewData;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Tab: React.FC<TabProps> = (props) => {
|
export const Tab: React.FC<TabProps> = (props) => {
|
||||||
|
@ -47,10 +55,13 @@ export const Tab: React.FC<TabProps> = (props) => {
|
||||||
onLabelEdited,
|
onLabelEdited,
|
||||||
onSelect,
|
onSelect,
|
||||||
onClose,
|
onClose,
|
||||||
|
tabPreviewData,
|
||||||
} = props;
|
} = props;
|
||||||
const { euiTheme } = useEuiTheme();
|
const { euiTheme } = useEuiTheme();
|
||||||
const tabRef = useRef<HTMLDivElement | null>(null);
|
const tabRef = useRef<HTMLDivElement | null>(null);
|
||||||
const [isInlineEditActive, setIsInlineEditActive] = useState<boolean>(false);
|
const [isInlineEditActive, setIsInlineEditActive] = useState<boolean>(false);
|
||||||
|
const [showPreview, setShowPreview] = useState<boolean>(false);
|
||||||
|
const [isActionPopoverOpen, setActionPopover] = useState<boolean>(false);
|
||||||
|
|
||||||
const closeButtonLabel = i18n.translate('unifiedTabs.closeTabButton', {
|
const closeButtonLabel = i18n.translate('unifiedTabs.closeTabButton', {
|
||||||
defaultMessage: 'Close session',
|
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',
|
defaultMessage: 'Click to select or double-click to edit session name',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const hidePreview = () => setShowPreview(false);
|
||||||
|
|
||||||
const onSelectEvent = useCallback(
|
const onSelectEvent = useCallback(
|
||||||
async (event: MouseEvent<HTMLElement>) => {
|
async (event: MouseEvent<HTMLElement>) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
hidePreview();
|
||||||
|
|
||||||
if (!isSelected) {
|
if (!isSelected) {
|
||||||
await onSelect(item);
|
await onSelect(item);
|
||||||
|
@ -89,6 +103,11 @@ export const Tab: React.FC<TabProps> = (props) => {
|
||||||
[onSelectEvent]
|
[onSelectEvent]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleDoubleClick = useCallback(() => {
|
||||||
|
setIsInlineEditActive(true);
|
||||||
|
hidePreview();
|
||||||
|
}, []);
|
||||||
|
|
||||||
const mainTabContent = (
|
const mainTabContent = (
|
||||||
<EuiFlexGroup
|
<EuiFlexGroup
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
|
@ -111,10 +130,9 @@ export const Tab: React.FC<TabProps> = (props) => {
|
||||||
css={getTabButtonCss(euiTheme)}
|
css={getTabButtonCss(euiTheme)}
|
||||||
className="unifiedTabs__tabBtn"
|
className="unifiedTabs__tabBtn"
|
||||||
data-test-subj={`unifiedTabs_selectTabBtn_${item.id}`}
|
data-test-subj={`unifiedTabs_selectTabBtn_${item.id}`}
|
||||||
title={item.label}
|
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onSelectEvent}
|
onClick={onSelectEvent}
|
||||||
onDoubleClick={() => setIsInlineEditActive(true)}
|
onDoubleClick={handleDoubleClick}
|
||||||
>
|
>
|
||||||
<EuiText color="inherit" size="s" css={getTabLabelCss(euiTheme)}>
|
<EuiText color="inherit" size="s" css={getTabLabelCss(euiTheme)}>
|
||||||
{item.label}
|
{item.label}
|
||||||
|
@ -124,7 +142,12 @@ export const Tab: React.FC<TabProps> = (props) => {
|
||||||
<EuiFlexGroup responsive={false} direction="row" gutterSize="none">
|
<EuiFlexGroup responsive={false} direction="row" gutterSize="none">
|
||||||
{!!getTabMenuItems && (
|
{!!getTabMenuItems && (
|
||||||
<EuiFlexItem grow={false} className="unifiedTabs__tabMenuBtn">
|
<EuiFlexItem grow={false} className="unifiedTabs__tabMenuBtn">
|
||||||
<TabMenu item={item} getTabMenuItems={getTabMenuItems} />
|
<TabMenu
|
||||||
|
item={item}
|
||||||
|
getTabMenuItems={getTabMenuItems}
|
||||||
|
isPopoverOpen={isActionPopoverOpen}
|
||||||
|
setPopover={setActionPopover}
|
||||||
|
/>
|
||||||
</EuiFlexItem>
|
</EuiFlexItem>
|
||||||
)}
|
)}
|
||||||
{!!onClose && (
|
{!!onClose && (
|
||||||
|
@ -148,18 +171,26 @@ export const Tab: React.FC<TabProps> = (props) => {
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TabWithBackground
|
<TabPreview
|
||||||
{...getTabAttributes(item, tabContentId)}
|
showPreview={showPreview}
|
||||||
ref={tabRef}
|
setShowPreview={setShowPreview}
|
||||||
role="tab"
|
stopPreviewOnHover={isInlineEditActive || isActionPopoverOpen}
|
||||||
aria-selected={isSelected}
|
tabPreviewData={tabPreviewData}
|
||||||
data-test-subj={`unifiedTabs_tab_${item.id}`}
|
tabItem={item}
|
||||||
isSelected={isSelected}
|
|
||||||
services={services}
|
|
||||||
onClick={onClickEvent}
|
|
||||||
>
|
>
|
||||||
{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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
* 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 { i18n } from '@kbn/i18n';
|
||||||
import {
|
import {
|
||||||
EuiButtonIcon,
|
EuiButtonIcon,
|
||||||
|
@ -22,10 +22,16 @@ import type { TabItem, GetTabMenuItems } from '../../types';
|
||||||
export interface TabMenuProps {
|
export interface TabMenuProps {
|
||||||
item: TabItem;
|
item: TabItem;
|
||||||
getTabMenuItems: GetTabMenuItems;
|
getTabMenuItems: GetTabMenuItems;
|
||||||
|
isPopoverOpen: boolean;
|
||||||
|
setPopover: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TabMenu: React.FC<TabMenuProps> = ({ item, getTabMenuItems }) => {
|
export const TabMenu: React.FC<TabMenuProps> = ({
|
||||||
const [isPopoverOpen, setPopover] = useState<boolean>(false);
|
item,
|
||||||
|
getTabMenuItems,
|
||||||
|
isPopoverOpen,
|
||||||
|
setPopover,
|
||||||
|
}) => {
|
||||||
const contextMenuPopoverId = useGeneratedHtmlId();
|
const contextMenuPopoverId = useGeneratedHtmlId();
|
||||||
|
|
||||||
const menuButtonLabel = i18n.translate('unifiedTabs.tabMenuButton', {
|
const menuButtonLabel = i18n.translate('unifiedTabs.tabMenuButton', {
|
||||||
|
|
|
@ -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';
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -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};
|
||||||
|
`;
|
||||||
|
};
|
|
@ -21,7 +21,7 @@ import {
|
||||||
closeOtherTabs,
|
closeOtherTabs,
|
||||||
closeTabsToTheRight,
|
closeTabsToTheRight,
|
||||||
} from '../../utils/manage_tabs';
|
} from '../../utils/manage_tabs';
|
||||||
import type { TabItem, TabsServices } from '../../types';
|
import type { TabItem, TabsServices, TabPreviewData } from '../../types';
|
||||||
|
|
||||||
export interface TabbedContentProps extends Pick<TabsBarProps, 'maxItemsCount'> {
|
export interface TabbedContentProps extends Pick<TabsBarProps, 'maxItemsCount'> {
|
||||||
initialItems: TabItem[];
|
initialItems: TabItem[];
|
||||||
|
@ -31,6 +31,7 @@ export interface TabbedContentProps extends Pick<TabsBarProps, 'maxItemsCount'>
|
||||||
renderContent: (selectedItem: TabItem) => React.ReactNode;
|
renderContent: (selectedItem: TabItem) => React.ReactNode;
|
||||||
createItem: () => TabItem;
|
createItem: () => TabItem;
|
||||||
onChanged: (state: TabbedContentState) => void;
|
onChanged: (state: TabbedContentState) => void;
|
||||||
|
getPreviewData: (item: TabItem) => TabPreviewData;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TabbedContentState {
|
export interface TabbedContentState {
|
||||||
|
@ -46,6 +47,7 @@ export const TabbedContent: React.FC<TabbedContentProps> = ({
|
||||||
renderContent,
|
renderContent,
|
||||||
createItem,
|
createItem,
|
||||||
onChanged,
|
onChanged,
|
||||||
|
getPreviewData,
|
||||||
}) => {
|
}) => {
|
||||||
const [tabContentId] = useState(() => htmlIdGenerator()());
|
const [tabContentId] = useState(() => htmlIdGenerator()());
|
||||||
const [state, _setState] = useState<TabbedContentState>(() => {
|
const [state, _setState] = useState<TabbedContentState>(() => {
|
||||||
|
@ -134,6 +136,7 @@ export const TabbedContent: React.FC<TabbedContentProps> = ({
|
||||||
onLabelEdited={onLabelEdited}
|
onLabelEdited={onLabelEdited}
|
||||||
onSelect={onSelect}
|
onSelect={onSelect}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
|
getPreviewData={getPreviewData}
|
||||||
/>
|
/>
|
||||||
</EuiFlexItem>
|
</EuiFlexItem>
|
||||||
{selectedItem ? (
|
{selectedItem ? (
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import { TabsBar } from './tabs_bar';
|
import { TabsBar } from './tabs_bar';
|
||||||
|
import { TabStatus } from '../../types';
|
||||||
import { servicesMock } from '../../../__mocks__/services';
|
import { servicesMock } from '../../../__mocks__/services';
|
||||||
|
|
||||||
const items = Array.from({ length: 5 }).map((_, i) => ({
|
const items = Array.from({ length: 5 }).map((_, i) => ({
|
||||||
|
@ -25,9 +26,17 @@ describe('TabsBar', () => {
|
||||||
const onSelect = jest.fn();
|
const onSelect = jest.fn();
|
||||||
const onLabelEdited = jest.fn();
|
const onLabelEdited = jest.fn();
|
||||||
const onClose = jest.fn();
|
const onClose = jest.fn();
|
||||||
|
const getPreviewData = jest.fn();
|
||||||
|
|
||||||
const selectedItem = items[0];
|
const selectedItem = items[0];
|
||||||
|
|
||||||
|
getPreviewData.mockReturnValue({
|
||||||
|
query: {
|
||||||
|
esql: 'SELECT * FROM table',
|
||||||
|
},
|
||||||
|
status: TabStatus.SUCCESS,
|
||||||
|
});
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<TabsBar
|
<TabsBar
|
||||||
tabContentId={tabContentId}
|
tabContentId={tabContentId}
|
||||||
|
@ -38,6 +47,7 @@ describe('TabsBar', () => {
|
||||||
onLabelEdited={onLabelEdited}
|
onLabelEdited={onLabelEdited}
|
||||||
onSelect={onSelect}
|
onSelect={onSelect}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
|
getPreviewData={getPreviewData}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -46,7 +56,9 @@ describe('TabsBar', () => {
|
||||||
|
|
||||||
items.forEach((tabItem, index) => {
|
items.forEach((tabItem, index) => {
|
||||||
const tab = tabs[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('id', `tab-${tabItem.id}`);
|
||||||
expect(tab).toHaveAttribute('aria-controls', tabContentId);
|
expect(tab).toHaveAttribute('aria-controls', tabContentId);
|
||||||
expect(tab).toHaveAttribute(
|
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();
|
tab.click();
|
||||||
expect(onSelect).toHaveBeenCalled();
|
expect(onSelect).toHaveBeenCalled();
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,7 @@ import { i18n } from '@kbn/i18n';
|
||||||
import { css } from '@emotion/react';
|
import { css } from '@emotion/react';
|
||||||
import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, useEuiTheme } from '@elastic/eui';
|
import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, useEuiTheme } from '@elastic/eui';
|
||||||
import { Tab, type TabProps } from '../tab';
|
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 { getTabIdAttribute } from '../../utils/get_tab_attributes';
|
||||||
import { useResponsiveTabs } from '../../hooks/use_responsive_tabs';
|
import { useResponsiveTabs } from '../../hooks/use_responsive_tabs';
|
||||||
import { TabsBarWithBackground } from '../tabs_visual_glue_to_header/tabs_bar_with_background';
|
import { TabsBarWithBackground } from '../tabs_visual_glue_to_header/tabs_bar_with_background';
|
||||||
|
@ -30,6 +30,7 @@ export type TabsBarProps = Pick<
|
||||||
maxItemsCount?: number;
|
maxItemsCount?: number;
|
||||||
services: TabsServices;
|
services: TabsServices;
|
||||||
onAdd: () => Promise<void>;
|
onAdd: () => Promise<void>;
|
||||||
|
getPreviewData: (item: TabItem) => TabPreviewData;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TabsBar: React.FC<TabsBarProps> = ({
|
export const TabsBar: React.FC<TabsBarProps> = ({
|
||||||
|
@ -43,6 +44,7 @@ export const TabsBar: React.FC<TabsBarProps> = ({
|
||||||
onLabelEdited,
|
onLabelEdited,
|
||||||
onSelect,
|
onSelect,
|
||||||
onClose,
|
onClose,
|
||||||
|
getPreviewData,
|
||||||
}) => {
|
}) => {
|
||||||
const { euiTheme } = useEuiTheme();
|
const { euiTheme } = useEuiTheme();
|
||||||
const [tabsContainerWithPlusElement, setTabsContainerWithPlusElement] =
|
const [tabsContainerWithPlusElement, setTabsContainerWithPlusElement] =
|
||||||
|
@ -107,6 +109,7 @@ export const TabsBar: React.FC<TabsBarProps> = ({
|
||||||
onLabelEdited={onLabelEdited}
|
onLabelEdited={onLabelEdited}
|
||||||
onSelect={onSelect}
|
onSelect={onSelect}
|
||||||
onClose={items.length > 1 ? onClose : undefined} // prevents closing the last tab
|
onClose={items.length > 1 ? onClose : undefined} // prevents closing the last tab
|
||||||
|
tabPreviewData={getPreviewData(item)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</EuiFlexGroup>
|
</EuiFlexGroup>
|
||||||
|
|
|
@ -11,3 +11,5 @@ export const MAX_TAB_LABEL_LENGTH = 500;
|
||||||
|
|
||||||
export const MAX_TAB_WIDTH = 280;
|
export const MAX_TAB_WIDTH = 280;
|
||||||
export const MIN_TAB_WIDTH = 96;
|
export const MIN_TAB_WIDTH = 96;
|
||||||
|
|
||||||
|
export const PREVIEW_WIDTH = 280;
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
* 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';
|
import type { CoreStart } from '@kbn/core/public';
|
||||||
|
|
||||||
export interface TabItem {
|
export interface TabItem {
|
||||||
|
@ -28,6 +29,19 @@ export interface TabsSizeConfig {
|
||||||
// TODO: extend with possibly different sizes for pinned tabs
|
// 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 TabMenuItem = TabMenuItemWithClick | 'divider';
|
||||||
|
|
||||||
export type GetTabMenuItems = (item: TabItem) => TabMenuItem[];
|
export type GetTabMenuItems = (item: TabItem) => TabMenuItem[];
|
||||||
|
|
|
@ -4,12 +4,6 @@
|
||||||
"outDir": "target/types"
|
"outDir": "target/types"
|
||||||
},
|
},
|
||||||
"include": ["**/*.ts", "**/*.tsx"],
|
"include": ["**/*.ts", "**/*.tsx"],
|
||||||
"exclude": [
|
"exclude": ["target/**/*"],
|
||||||
"target/**/*"
|
"kbn_references": ["@kbn/i18n", "@kbn/es-query", "@kbn/core-chrome-browser", "@kbn/core"]
|
||||||
],
|
|
||||||
"kbn_references": [
|
|
||||||
"@kbn/i18n",
|
|
||||||
"@kbn/core-chrome-browser",
|
|
||||||
"@kbn/core",
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue