[Discover][Tabs] New tabs package and initial implementation of components (#210674)

- Closes https://github.com/elastic/kibana/issues/210500
- Closes https://github.com/elastic/kibana/issues/210502

## Summary

This PR introduces a new package `@kbn/unified-tabs` which includes an
initial implementation for tabs UI/UX which we are planning to add to
Discover.

- [x] New package
- [x] Storybook support 
- [x] Initial styles and interactions
- [x] New example plugin for testing together with UnifiedSearch bar 
- [x] Minimal tests

### In the new Storybook

Start Storybook with `NODE_OPTIONS="--openssl-legacy-provider" node
scripts/storybook unified_tabs` and navigate to `http://localhost:9001`.

<img width="1024" alt="Screenshot 2025-02-12 at 13 35 46"
src="https://github.com/user-attachments/assets/0723b0c4-c3f7-44f8-af8d-f68d7a7b6ea8"
/>

### In the new Unified Tabs example plugin

Start Kibana with `yarn start --run-examples`. Then navigate to the
Unified Tabs example plugin
`http://localhost:5601/app/unifiedTabsExamples`.

<img width="1221" alt="Screenshot 2025-02-12 at 16 11 55"
src="https://github.com/user-attachments/assets/2edff817-0aae-424c-978c-c4c67450c9eb"
/>
<img width="1219" alt="Screenshot 2025-02-12 at 16 13 57"
src="https://github.com/user-attachments/assets/2e6e6b0b-88e9-4689-a175-9612e8507535"
/>

![Feb-12-2025
16-13-17](https://github.com/user-attachments/assets/a1d511da-d554-432c-bea9-cbe8349f9772)


### Checklist

- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md)
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [x] The PR description includes the appropriate Release Notes section,
and the correct `release_note:*` label is applied per the
[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Davis McPhee <davismcphee@hotmail.com>
This commit is contained in:
Julia Rechkunova 2025-02-21 14:00:40 +01:00 committed by GitHub
parent 4783925e56
commit 705df212b8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
39 changed files with 1423 additions and 56 deletions

2
.github/CODEOWNERS vendored
View file

@ -39,6 +39,7 @@ examples/ui_action_examples @elastic/appex-sharedux
examples/ui_actions_explorer @elastic/appex-sharedux
examples/unified_doc_viewer @elastic/kibana-core
examples/unified_field_list_examples @elastic/kibana-data-discovery
examples/unified_tabs_examples @elastic/kibana-data-discovery
examples/user_profile_examples @elastic/kibana-security
examples/v8_profiler_examples @elastic/response-ops
packages/kbn-ambient-common-types @elastic/kibana-operations
@ -539,6 +540,7 @@ src/platform/packages/shared/kbn-ui-theme @elastic/kibana-operations
src/platform/packages/shared/kbn-unified-data-table @elastic/kibana-data-discovery @elastic/security-threat-hunting-investigations
src/platform/packages/shared/kbn-unified-doc-viewer @elastic/kibana-data-discovery
src/platform/packages/shared/kbn-unified-field-list @elastic/kibana-data-discovery
src/platform/packages/shared/kbn-unified-tabs @elastic/kibana-data-discovery
src/platform/packages/shared/kbn-unsaved-changes-prompt @elastic/kibana-management
src/platform/packages/shared/kbn-use-tracked-promise @elastic/obs-ux-logs-team
src/platform/packages/shared/kbn-user-profile-components @elastic/kibana-security

View file

@ -163,6 +163,7 @@
"unifiedFieldList": "src/platform/packages/shared/kbn-unified-field-list",
"unifiedHistogram": "src/platform/plugins/shared/unified_histogram",
"unifiedDataTable": "src/platform/packages/shared/kbn-unified-data-table",
"unifiedTabs": "src/platform/packages/shared/kbn-unified-tabs",
"dataGridInTableSearch": "src/platform/packages/shared/kbn-data-grid-in-table-search",
"unsavedChangesBadge": "src/platform/packages/private/kbn-unsaved-changes-badge",
"unsavedChangesPrompt": "src/platform/packages/shared/kbn-unsaved-changes-prompt",

View file

@ -61,64 +61,66 @@ function DeveloperExamples({ startServices, examples, navigateToApp, getUrlForAp
return (
<KibanaRenderContextProvider {...startServices}>
<EuiPageTemplate.Header>
<EuiFlexGroup justifyContent={'spaceBetween'}>
<EuiFlexItem>
<EuiPageHeader pageTitle={'Developer examples'} />
<EuiText>
The following examples showcase services and APIs that are available to developers.
</EuiText>
</EuiFlexItem>
<EuiFlexItem>
<EuiFieldSearch
fullWidth
placeholder="Search"
value={search}
onChange={(e) => setSearch(e.target.value)}
isClearable={true}
aria-label="Search developer examples"
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPageTemplate.Header>
<EuiPageTemplate.Section>
<EuiFlexGroup wrap>
{filteredExamples.map((def) => (
<EuiFlexItem style={{ minWidth: 300, maxWidth: 500 }} key={def.appId}>
<EuiCard
description={
<EuiHighlight search={search} highlightAll={true}>
{def.description}
</EuiHighlight>
}
title={
<React.Fragment>
<EuiLink
onClick={() => {
navigateToApp(def.appId);
}}
>
<EuiHighlight search={search} highlightAll={true}>
{def.title}
</EuiHighlight>
</EuiLink>
<EuiButtonIcon
iconType="popout"
onClick={() =>
window.open(getUrlForApp(def.appId), '_blank', 'noopener, noreferrer')
}
>
Open in new tab
</EuiButtonIcon>
</React.Fragment>
}
image={def.image}
footer={def.links ? <EuiListGroup size={'s'} listItems={def.links} /> : undefined}
<EuiPageTemplate offset={0}>
<EuiPageTemplate.Header>
<EuiFlexGroup justifyContent={'spaceBetween'}>
<EuiFlexItem>
<EuiPageHeader pageTitle={'Developer examples'} />
<EuiText>
The following examples showcase services and APIs that are available to developers.
</EuiText>
</EuiFlexItem>
<EuiFlexItem>
<EuiFieldSearch
fullWidth
placeholder="Search"
value={search}
onChange={(e) => setSearch(e.target.value)}
isClearable={true}
aria-label="Search developer examples"
/>
</EuiFlexItem>
))}
</EuiFlexGroup>
</EuiPageTemplate.Section>
</EuiFlexGroup>
</EuiPageTemplate.Header>
<EuiPageTemplate.Section>
<EuiFlexGroup wrap>
{filteredExamples.map((def) => (
<EuiFlexItem style={{ minWidth: 300, maxWidth: 500 }} key={def.appId}>
<EuiCard
description={
<EuiHighlight search={search} highlightAll={true}>
{def.description}
</EuiHighlight>
}
title={
<React.Fragment>
<EuiLink
onClick={() => {
navigateToApp(def.appId);
}}
>
<EuiHighlight search={search} highlightAll={true}>
{def.title}
</EuiHighlight>
</EuiLink>
<EuiButtonIcon
iconType="popout"
onClick={() =>
window.open(getUrlForApp(def.appId), '_blank', 'noopener, noreferrer')
}
>
Open in new tab
</EuiButtonIcon>
</React.Fragment>
}
image={def.image}
footer={def.links ? <EuiListGroup size={'s'} listItems={def.links} /> : undefined}
/>
</EuiFlexItem>
))}
</EuiFlexGroup>
</EuiPageTemplate.Section>
</EuiPageTemplate>
</KibanaRenderContextProvider>
);
}

View file

@ -0,0 +1,9 @@
# unified_tabs_examples
Examples of unified tabs components.
To run this example, ensure you have data to search against (for example, the sample datasets) and start kibana with the `--run-examples` flag.
```bash
yarn start --run-examples
```

View file

@ -0,0 +1,11 @@
/*
* 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 const PLUGIN_ID = 'unifiedTabsExamples';
export const PLUGIN_NAME = 'Unified Tabs Examples';

View file

@ -0,0 +1,24 @@
{
"type": "plugin",
"id": "@kbn/unified-tabs-examples-plugin",
"owner": "@elastic/kibana-data-discovery",
"description": "Examples of using unified tabs.",
"plugin": {
"id": "unifiedTabsExamples",
"server": false,
"browser": true,
"requiredPlugins": [
"navigation",
"developerExamples",
"inspector",
"kibanaUtils",
"unifiedSearch",
"data",
"dataViews",
"dataViewFieldEditor",
"charts",
"fieldFormats",
"uiActions"
]
}
}

View file

@ -0,0 +1,41 @@
/*
* 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 ReactDOM from 'react-dom';
import { I18nProvider } from '@kbn/i18n-react';
import { KibanaThemeProvider } from '@kbn/react-kibana-context-theme';
import type { AppMountParameters, CoreStart } from '@kbn/core/public';
import { AppPluginStartDependencies } from './types';
import { UnifiedTabsExampleApp } from './example_app';
export const renderApp = (
core: CoreStart,
deps: AppPluginStartDependencies,
{ element, setHeaderActionMenu }: AppMountParameters
) => {
ReactDOM.render(
<I18nProvider>
<KibanaThemeProvider {...core}>
<UnifiedTabsExampleApp
services={{
core,
...deps,
}}
setHeaderActionMenu={setHeaderActionMenu}
/>
</KibanaThemeProvider>
</I18nProvider>,
element
);
return () => {
ReactDOM.unmountComponentAtNode(element);
};
};

View file

@ -0,0 +1,234 @@
/*
* 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 } from 'react';
import {
EuiFlexGroup,
EuiFlexItem,
EuiPage,
EuiPageBody,
EuiEmptyPrompt,
EuiLoadingLogo,
useEuiTheme,
EuiPanel,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { css } from '@emotion/react';
import type { AppMountParameters } from '@kbn/core-application-browser';
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 { UnifiedTabs } from '@kbn/unified-tabs';
import { PLUGIN_ID, PLUGIN_NAME } from '../common';
import { FieldListSidebar, FieldListSidebarProps } from './field_list_sidebar';
let TMP_COUNTER = 0;
interface UnifiedTabsExampleAppProps {
services: FieldListSidebarProps['services'];
setHeaderActionMenu: AppMountParameters['setHeaderActionMenu'];
}
export const UnifiedTabsExampleApp: React.FC<UnifiedTabsExampleAppProps> = ({
services,
setHeaderActionMenu,
}) => {
const { euiTheme } = useEuiTheme();
const { navigation, data, unifiedSearch } = services;
const { IndexPatternSelect } = unifiedSearch.ui;
const [dataView, setDataView] = useState<DataView | null>();
const [selectedFieldNames, setSelectedFieldNames] = useState<string[]>([]);
const onAddFieldToWorkspace = useCallback(
(field: DataViewField) => {
setSelectedFieldNames((names) => [...names, field.name]);
},
[setSelectedFieldNames]
);
const onRemoveFieldFromWorkspace = useCallback(
(field: DataViewField) => {
setSelectedFieldNames((names) => names.filter((name) => name !== field.name));
},
[setSelectedFieldNames]
);
useEffect(() => {
const setDefaultDataView = async () => {
try {
const defaultDataView = await data.dataViews.getDefault();
setDataView(defaultDataView);
} catch (e) {
setDataView(null);
}
};
setDefaultDataView();
}, [data]);
if (typeof dataView === 'undefined') {
return (
<EuiEmptyPrompt
icon={<EuiLoadingLogo logo="logoKibana" size="xl" />}
title={<h2>{PLUGIN_NAME}</h2>}
body={<p>Loading...</p>}
/>
);
}
const SearchBar = navigation.ui.AggregateQueryTopNavMenu;
return (
<EuiPage
grow={true}
css={css`
background-color: ${euiTheme.colors.backgroundBasePlain};
`}
>
<EuiPageBody paddingSize="none">
{dataView ? (
<div className="eui-fullHeight">
<UnifiedTabs
initialItems={[
{
id: 'tab_initial',
label: 'Initial tab',
},
]}
onChanged={() => {}}
createItem={() => {
TMP_COUNTER += 1;
return {
id: `tab_${TMP_COUNTER}`,
label: `Tab ${TMP_COUNTER}`,
};
}}
renderContent={({ label }) => {
return (
<EuiFlexGroup direction="column" gutterSize="none">
<EuiFlexItem grow={false}>
<SearchBar
appName={PLUGIN_ID}
indexPatterns={[dataView]}
onQuerySubmit={() => {}}
isLoading={false}
showDatePicker
allowSavingQueries
showSearchBar
dataViewPickerComponentProps={
{
trigger: {
label: dataView?.getName() || '',
'data-test-subj': 'discover-dataView-switch-link',
title: dataView?.getIndexPattern() || '',
},
currentDataViewId: dataView?.id,
} as DataViewPickerProps
}
useDefaultBehaviors
displayStyle="detached"
config={[
{
id: 'inspect',
label: 'Inspect',
run: () => {},
},
{
id: 'alerts',
label: 'Alerts',
run: () => {},
},
{
id: 'open',
label: 'Open',
iconType: 'folderOpen',
iconOnly: true,
run: () => {},
},
{
id: 'share',
label: 'Share',
iconType: 'share',
iconOnly: true,
run: () => {},
},
{
id: 'save',
label: 'Save',
emphasize: true,
run: () => {},
},
]}
setMenuMountPoint={setHeaderActionMenu}
/>
</EuiFlexItem>
<EuiFlexItem grow={true}>
<EuiFlexGroup direction="row" alignItems="stretch">
<EuiFlexItem
grow={false}
css={css`
background-color: ${euiTheme.colors.body};
border-right: ${euiTheme.border.thin};
`}
>
<FieldListSidebar
services={services}
dataView={dataView}
selectedFieldNames={selectedFieldNames}
onAddFieldToWorkspace={onAddFieldToWorkspace}
onRemoveFieldFromWorkspace={onRemoveFieldFromWorkspace}
/>
</EuiFlexItem>
<EuiFlexItem>
<EuiPanel hasShadow={false} paddingSize="l" className="eui-fullHeight">
<EuiEmptyPrompt
iconType="beaker"
title={<h3>{PLUGIN_NAME}</h3>}
body={<p>Tab: {label}</p>}
/>
</EuiPanel>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
);
}}
/>
</div>
) : (
<EuiEmptyPrompt
iconType="warning"
color="warning"
title={<h2>Make sure to have at least one data view</h2>}
body={
<p>
<IndexPatternSelect
placeholder={i18n.translate('searchSessionExample.selectDataViewPlaceholder', {
defaultMessage: 'Select data view',
})}
indexPatternId=""
onChange={async (dataViewId?: string) => {
if (dataViewId) {
const newDataView = await data.dataViews.get(dataViewId);
setDataView(newDataView);
} else {
setDataView(undefined);
}
}}
isClearable={false}
data-test-subj="dataViewSelector"
/>
</p>
}
/>
)}
</EuiPageBody>
</EuiPage>
);
};

View file

@ -0,0 +1,91 @@
/*
* 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, useMemo, useRef } from 'react';
import type { DataView } from '@kbn/data-views-plugin/public';
import { generateFilters } from '@kbn/data-plugin/public';
import {
UnifiedFieldListSidebarContainer,
type UnifiedFieldListSidebarContainerProps,
type UnifiedFieldListSidebarContainerApi,
type AddFieldFilterHandler,
} from '@kbn/unified-field-list';
import { type CoreStart } from '@kbn/core-lifecycle-browser';
import { PLUGIN_ID } from '../common';
import { type AppPluginStartDependencies } from './types';
const getCreationOptions: UnifiedFieldListSidebarContainerProps['getCreationOptions'] = () => {
return {
originatingApp: PLUGIN_ID,
localStorageKeyPrefix: 'examples',
timeRangeUpdatesType: 'timefilter',
buttonAddFieldVariant: 'toolbar',
compressed: true,
showSidebarToggleButton: true,
disablePopularFields: true,
};
};
export interface FieldListSidebarProps {
dataView: DataView;
selectedFieldNames: string[];
services: AppPluginStartDependencies & {
core: CoreStart;
};
onAddFieldToWorkspace: UnifiedFieldListSidebarContainerProps['onAddFieldToWorkspace'];
onRemoveFieldFromWorkspace: UnifiedFieldListSidebarContainerProps['onRemoveFieldFromWorkspace'];
}
export const FieldListSidebar: React.FC<FieldListSidebarProps> = ({
dataView,
selectedFieldNames,
services,
onAddFieldToWorkspace,
onRemoveFieldFromWorkspace,
}) => {
const unifiedFieldListContainerRef = useRef<UnifiedFieldListSidebarContainerApi>(null);
const filterManager = services.data?.query?.filterManager;
const onAddFilter: AddFieldFilterHandler | undefined = useMemo(
() =>
filterManager && dataView
? (clickedField, values, operation) => {
const newFilters = generateFilters(
filterManager,
clickedField,
values,
operation,
dataView
);
filterManager.addFilters(newFilters);
}
: undefined,
[dataView, filterManager]
);
const onFieldEdited = useCallback(async () => {
unifiedFieldListContainerRef.current?.refetchFieldsExistenceInfo();
}, [unifiedFieldListContainerRef]);
return (
<UnifiedFieldListSidebarContainer
ref={unifiedFieldListContainerRef}
variant="responsive"
getCreationOptions={getCreationOptions}
services={services}
dataView={dataView}
allFields={dataView.fields}
workspaceSelectedFieldNames={selectedFieldNames}
onAddFieldToWorkspace={onAddFieldToWorkspace}
onRemoveFieldFromWorkspace={onRemoveFieldFromWorkspace}
onAddFilter={onAddFilter}
onFieldEdited={onFieldEdited}
/>
);
};

View file

@ -0,0 +1,17 @@
/*
* 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 { UnifiedTabsExamplesPlugin } from './plugin';
// This exports static code and TypeScript types,
// as well as, Kibana Platform `plugin()` initializer.
export function plugin() {
return new UnifiedTabsExamplesPlugin();
}
export type { UnifiedTabsExamplesPluginSetup, UnifiedTabsExamplesPluginStart } from './types';

View file

@ -0,0 +1,72 @@
/*
* 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 { AppMountParameters, CoreSetup, CoreStart, Plugin } from '@kbn/core/public';
import {
AppPluginSetupDependencies,
AppPluginStartDependencies,
UnifiedTabsExamplesPluginSetup,
UnifiedTabsExamplesPluginStart,
} from './types';
import { PLUGIN_ID, PLUGIN_NAME } from '../common';
import image from './unified_tabs.png';
export class UnifiedTabsExamplesPlugin
implements
Plugin<
UnifiedTabsExamplesPluginSetup,
UnifiedTabsExamplesPluginStart,
AppPluginSetupDependencies,
AppPluginStartDependencies
>
{
public setup(
core: CoreSetup<AppPluginStartDependencies>,
{ developerExamples }: AppPluginSetupDependencies
): UnifiedTabsExamplesPluginSetup {
// Register an application into the side navigation menu
core.application.register({
id: PLUGIN_ID,
title: PLUGIN_NAME,
visibleIn: [],
mount: async (params: AppMountParameters) => {
// Load application bundle
const { renderApp } = await import('./application');
// Get start services as specified in kibana.json
const [coreStart, depsStart] = await core.getStartServices();
// Render the application
return renderApp(coreStart, depsStart, params);
},
});
developerExamples.register({
appId: PLUGIN_ID,
title: PLUGIN_NAME,
description: `Examples of unified tabs functionality.`,
image,
links: [
{
label: 'README',
href: 'https://github.com/elastic/kibana/tree/main/src/platform/packages/shared/kbn-unified-tabs/README.md',
iconType: 'logoGithub',
target: '_blank',
size: 's',
},
],
});
return {};
}
public start(core: CoreStart): UnifiedTabsExamplesPluginStart {
return {};
}
public stop() {}
}

View file

@ -0,0 +1,38 @@
/*
* 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 type { NavigationPublicPluginStart } from '@kbn/navigation-plugin/public';
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public';
import type { DeveloperExamplesSetup } from '@kbn/developer-examples-plugin/public';
import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
import type { ChartsPluginStart } from '@kbn/charts-plugin/public';
import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
import type { DataViewFieldEditorStart } from '@kbn/data-view-field-editor-plugin/public';
import type { UiActionsStart } from '@kbn/ui-actions-plugin/public';
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface UnifiedTabsExamplesPluginSetup {}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface UnifiedTabsExamplesPluginStart {}
export interface AppPluginSetupDependencies {
developerExamples: DeveloperExamplesSetup;
}
export interface AppPluginStartDependencies {
navigation: NavigationPublicPluginStart;
data: DataPublicPluginStart;
dataViews: DataViewsPublicPluginStart;
dataViewFieldEditor: DataViewFieldEditorStart;
unifiedSearch: UnifiedSearchPublicPluginStart;
charts: ChartsPluginStart;
fieldFormats: FieldFormatsStart;
uiActions: UiActionsStart;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 253 KiB

View file

@ -0,0 +1,35 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "target/types"
},
"include": [
"index.ts",
"common/**/*.ts",
"public/**/*.ts",
"public/**/*.tsx",
"../../typings/**/*",
],
"exclude": [
"target/**/*",
],
"kbn_references": [
"@kbn/core",
"@kbn/data-plugin",
"@kbn/data-views-plugin",
"@kbn/navigation-plugin",
"@kbn/developer-examples-plugin",
"@kbn/unified-search-plugin",
"@kbn/i18n-react",
"@kbn/i18n",
"@kbn/core-lifecycle-browser",
"@kbn/charts-plugin",
"@kbn/field-formats-plugin",
"@kbn/data-view-field-editor-plugin",
"@kbn/unified-field-list",
"@kbn/ui-actions-plugin",
"@kbn/react-kibana-context-theme",
"@kbn/unified-tabs",
"@kbn/core-application-browser",
]
}

View file

@ -981,6 +981,8 @@
"@kbn/unified-field-list-examples-plugin": "link:examples/unified_field_list_examples",
"@kbn/unified-histogram-plugin": "link:src/platform/plugins/shared/unified_histogram",
"@kbn/unified-search-plugin": "link:src/platform/plugins/shared/unified_search",
"@kbn/unified-tabs": "link:src/platform/packages/shared/kbn-unified-tabs",
"@kbn/unified-tabs-examples-plugin": "link:examples/unified_tabs_examples",
"@kbn/unsaved-changes-badge": "link:src/platform/packages/private/kbn-unsaved-changes-badge",
"@kbn/unsaved-changes-prompt": "link:src/platform/packages/shared/kbn-unsaved-changes-prompt",
"@kbn/upgrade-assistant-plugin": "link:x-pack/platform/plugins/private/upgrade_assistant",

View file

@ -75,6 +75,7 @@ export const storybookAliases = {
triggers_actions_ui: 'x-pack/platform/plugins/shared/triggers_actions_ui/.storybook',
ui_actions_enhanced: 'src/platform/plugins/shared/ui_actions_enhanced/.storybook',
unified_search: 'src/platform/plugins/shared/unified_search/.storybook',
unified_tabs: 'src/platform/packages/shared/kbn-unified-tabs/.storybook',
profiling: 'x-pack/solutions/observability/plugins/profiling/.storybook',
event_stacktrace: 'x-pack/platform/packages/shared/kbn-event-stacktrace/.storybook',
};

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".
*/
module.exports = require('@kbn/storybook').defaultConfig;

View file

@ -0,0 +1,15 @@
# @kbn/unified-tabs
Tabs bar components.
## Storybook
Run the following command:
`NODE_OPTIONS="--openssl-legacy-provider" node scripts/storybook unified_tabs`.
## Example plugin
Start Kibana with:
`yarn start --run-examples`.
Then navigate to the Unified Tabs example plugin `http://localhost:5601/app/unifiedTabsExamples`.

View file

@ -0,0 +1,14 @@
/*
* 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 type { TabItem } from './src/types';
export {
TabbedContent as UnifiedTabs,
type TabbedContentProps as UnifiedTabsProps,
} from './src/components/tabbed_content';

View file

@ -0,0 +1,14 @@
/*
* 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".
*/
module.exports = {
preset: '@kbn/test',
rootDir: '../../../../..',
roots: ['<rootDir>/src/platform/packages/shared/kbn-unified-tabs'],
};

View file

@ -0,0 +1,7 @@
{
"type": "shared-browser",
"id": "@kbn/unified-tabs",
"owner": "@elastic/kibana-data-discovery",
"group": "platform",
"visibility": "shared"
}

View file

@ -0,0 +1,7 @@
{
"name": "@kbn/unified-tabs",
"private": true,
"version": "1.0.0",
"license": "Elastic License 2.0 OR AGPL-3.0-only OR SSPL-1.0",
"sideEffects": false
}

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 const STORYBOOK_TITLE = 'Unified Tabs';

View file

@ -0,0 +1,64 @@
/*
* 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 type { ComponentStory } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { Tab, type TabProps } from '../tab';
import { STORYBOOK_TITLE } from './storybook_constants';
export default {
title: `${STORYBOOK_TITLE}/Tab`,
parameters: {
backgrounds: {
default: 'white',
values: [{ name: 'white', value: '#fff' }],
},
},
};
const TabTemplate: ComponentStory<React.FC<TabProps>> = (args) => (
<Tab {...args} onSelect={action('onSelect')} onClose={action('onClose')} />
);
export const Default = TabTemplate.bind({});
Default.args = {
item: {
id: '1',
label: 'Tab 1',
},
isSelected: false,
};
export const Selected = TabTemplate.bind({});
Selected.args = {
item: {
id: '1',
label: 'Tab 1',
},
isSelected: true,
};
export const WithLongLabel = TabTemplate.bind({});
WithLongLabel.args = {
item: {
id: '1',
label: 'Tab with a very long label that should be truncated',
},
isSelected: false,
};
export const WithLongLabelSelected = TabTemplate.bind({});
WithLongLabelSelected.args = {
item: {
id: '1',
label: 'Tab with a very long label that should be truncated',
},
isSelected: true,
};

View file

@ -0,0 +1,72 @@
/*
* 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 type { ComponentStory } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { TabbedContent, type TabbedContentProps } from '../tabbed_content';
import { STORYBOOK_TITLE } from './storybook_constants';
let TMP_COUNTER = 0;
export default {
title: `${STORYBOOK_TITLE}/Tabs`,
parameters: {
backgrounds: {
default: 'white',
values: [{ name: 'white', value: '#fff' }],
},
},
};
const TabbedContentTemplate: ComponentStory<React.FC<TabbedContentProps>> = (args) => (
<TabbedContent
{...args}
createItem={() => {
TMP_COUNTER += 1;
return {
id: `tab_${TMP_COUNTER}`,
label: `Tab ${TMP_COUNTER}`,
};
}}
onChanged={action('onClosed')}
renderContent={(item) => (
<div style={{ paddingTop: '16px' }}>Content for tab: {item.label}</div>
)}
/>
);
export const Default = TabbedContentTemplate.bind({});
Default.args = {
initialItems: [
{
id: '1',
label: 'Tab 1',
},
],
};
export const WithMultipleTabs = TabbedContentTemplate.bind({});
WithMultipleTabs.args = {
initialItems: [
{
id: '1',
label: 'Tab 1',
},
{
id: '2',
label: 'Tab 2',
},
{
id: '3',
label: 'Tab 3',
},
],
initialSelectedItemId: '3',
};

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 { Tab, type TabProps } from './tab';

View file

@ -0,0 +1,50 @@
/*
* 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 } from '@testing-library/react';
import { Tab } from './tab';
const tabItem = {
id: 'test-id',
label: 'test-label',
};
const tabContentId = 'test-content-id';
describe('Tab', () => {
it('renders tab', async () => {
const onSelect = jest.fn();
const onClose = jest.fn();
render(
<Tab
tabContentId={tabContentId}
item={tabItem}
isSelected={false}
onSelect={onSelect}
onClose={onClose}
/>
);
expect(screen.getByText(tabItem.label)).toBeInTheDocument();
const tab = screen.getByRole('tab');
expect(tab).toHaveAttribute('id', `tab-${tabItem.id}`);
expect(tab).toHaveAttribute('aria-controls', tabContentId);
tab.click();
expect(onSelect).toHaveBeenCalled();
expect(onClose).not.toHaveBeenCalled();
const closeButton = screen.getByTestId(`unifiedTabs_closeTabBtn_${tabItem.id}`);
closeButton.click();
expect(onClose).toHaveBeenCalled();
expect(onSelect).toHaveBeenCalledTimes(1);
});
});

View file

@ -0,0 +1,161 @@
/*
* 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, { MouseEvent, useCallback } from 'react';
import { i18n } from '@kbn/i18n';
import { css } from '@emotion/react';
import {
EuiButtonIcon,
EuiFlexGroup,
EuiFlexItem,
EuiText,
EuiThemeComputed,
useEuiTheme,
} from '@elastic/eui';
import { getTabAttributes } from '../../utils/get_tab_attributes';
import type { TabItem } from '../../types';
export interface TabProps {
item: TabItem;
isSelected: boolean;
tabContentId: string;
onSelect: (item: TabItem) => void;
onClose: (item: TabItem) => void;
}
export const Tab: React.FC<TabProps> = ({ item, isSelected, tabContentId, onSelect, onClose }) => {
const { euiTheme } = useEuiTheme();
const tabContainerDataTestSubj = `unifiedTabs_tab_${item.id}`;
const closeButtonLabel = i18n.translate('unifiedTabs.closeTabButton', {
defaultMessage: 'Close',
});
const onSelectEvent = useCallback(
(event: MouseEvent<HTMLElement>) => {
event.stopPropagation();
if (!isSelected) {
onSelect(item);
}
},
[onSelect, item, isSelected]
);
const onCloseEvent = useCallback(
(event: MouseEvent<HTMLButtonElement>) => {
event.stopPropagation();
onClose(item);
},
[onClose, item]
);
const onClickEvent = useCallback(
(event: MouseEvent<HTMLDivElement>) => {
if (event.currentTarget.getAttribute('data-test-subj') === tabContainerDataTestSubj) {
// if user presses on the space around the buttons, we should still trigger the onSelectEvent
onSelectEvent(event);
}
},
[onSelectEvent, tabContainerDataTestSubj]
);
return (
<EuiFlexGroup
alignItems="center"
css={getTabContainerCss(euiTheme, isSelected)}
data-test-subj={tabContainerDataTestSubj}
responsive={false}
gutterSize="none"
onClick={onClickEvent}
>
<button
{...getTabAttributes(item, tabContentId)}
aria-selected={isSelected}
css={getTabButtonCss(euiTheme)}
className="unifiedTabs__tabBtn"
data-test-subj={`unifiedTabs_selectTabBtn_${item.id}`}
role="tab"
type="button"
onClick={onSelectEvent}
>
<EuiText color="inherit" size="s" className="eui-textTruncate">
{item.label}
</EuiText>
</button>
<EuiFlexItem grow={false} className="unifiedTabs__closeTabBtn">
<EuiButtonIcon
aria-label={closeButtonLabel}
title={closeButtonLabel}
color="text"
data-test-subj={`unifiedTabs_closeTabBtn_${item.id}`}
iconType="cross"
onClick={onCloseEvent}
/>
</EuiFlexItem>
</EuiFlexGroup>
);
};
function getTabContainerCss(euiTheme: EuiThemeComputed, isSelected: boolean) {
// TODO: remove the usage of deprecated colors
return css`
display: inline-flex;
border-right: ${euiTheme.border.thin};
border-color: ${euiTheme.colors.lightShade};
height: ${euiTheme.size.xl};
padding-left: ${euiTheme.size.m};
padding-right: ${euiTheme.size.xs};
min-width: 96px;
max-width: 280px;
background-color: ${isSelected ? euiTheme.colors.emptyShade : euiTheme.colors.lightestShade};
color: ${isSelected ? euiTheme.colors.text : euiTheme.colors.subduedText};
transition: background-color ${euiTheme.animation.fast};
.unifiedTabs__closeTabBtn {
opacity: 0;
transition: opacity ${euiTheme.animation.fast};
}
&:hover {
.unifiedTabs__closeTabBtn {
opacity: 1;
}
}
${isSelected
? `
.unifiedTabs__tabBtn {
cursor: default;
}`
: `
cursor: pointer;
&:hover {
background-color: ${euiTheme.colors.lightShade};
color: ${euiTheme.colors.text};
}`}
`;
}
function getTabButtonCss(euiTheme: EuiThemeComputed) {
return css`
width: 100%;
min-width: 0;
flex-grow: 1;
padding-right: ${euiTheme.size.xs};
text-align: left;
color: inherit;
border: none;
border-radius: 0;
background: transparent;
`;
}

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 { TabbedContent, type TabbedContentProps } from './tabbed_content';

View file

@ -0,0 +1,125 @@
/*
* 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, useState } from 'react';
import { htmlIdGenerator, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { TabsBar } from '../tabs_bar';
import { getTabAttributes } from '../../utils/get_tab_attributes';
import { TabItem } from '../../types';
export interface TabbedContentProps {
initialItems: TabItem[];
initialSelectedItemId?: string;
'data-test-subj'?: string;
renderContent: (selectedItem: TabItem) => React.ReactNode;
createItem: () => TabItem;
onChanged: (state: TabbedContentState) => void;
}
export interface TabbedContentState {
items: TabItem[];
selectedItem: TabItem | null;
}
export const TabbedContent: React.FC<TabbedContentProps> = ({
initialItems,
initialSelectedItemId,
renderContent,
createItem,
onChanged,
}) => {
const [tabContentId] = useState(() => htmlIdGenerator()());
const [state, _setState] = useState<TabbedContentState>(() => {
return {
items: initialItems,
selectedItem:
(initialSelectedItemId && initialItems.find((item) => item.id === initialSelectedItemId)) ||
initialItems[0],
};
});
const { items, selectedItem } = state;
const changeState = useCallback(
(getNextState: (prevState: TabbedContentState) => TabbedContentState) => {
_setState((prevState) => {
const nextState = getNextState(prevState);
onChanged(nextState);
return nextState;
});
},
[_setState, onChanged]
);
const onSelect = useCallback(
(item: TabItem) => {
changeState((prevState) => ({
...prevState,
selectedItem: item,
}));
},
[changeState]
);
const onClose = useCallback(
(item: TabItem) => {
changeState((prevState) => {
const nextItems = prevState.items.filter((prevItem) => prevItem.id !== item.id);
// TODO: better selection logic
const nextSelectedItem = nextItems.length ? nextItems[nextItems.length - 1] : null;
return {
items: nextItems,
selectedItem:
prevState.selectedItem?.id !== item.id ? prevState.selectedItem : nextSelectedItem,
};
});
},
[changeState]
);
const onAdd = useCallback(() => {
const newItem = createItem();
changeState((prevState) => {
return {
items: [...prevState.items, newItem],
selectedItem: newItem,
};
});
}, [changeState, createItem]);
return (
<EuiFlexGroup
responsive={false}
direction="column"
gutterSize="none"
className="eui-fullHeight"
>
<EuiFlexItem grow={false}>
<TabsBar
items={items}
selectedItem={selectedItem}
tabContentId={tabContentId}
onAdd={onAdd}
onSelect={onSelect}
onClose={onClose}
/>
</EuiFlexItem>
{selectedItem ? (
<EuiFlexItem
data-test-subj="unifiedTabs_selectedTabContent"
role="tabpanel"
id={tabContentId}
aria-labelledby={getTabAttributes(selectedItem, tabContentId).id}
>
{renderContent(selectedItem)}
</EuiFlexItem>
) : null}
</EuiFlexGroup>
);
};

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 { TabsBar, type TabsBarProps } from './tabs_bar';

View file

@ -0,0 +1,64 @@
/*
* 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 } from '@testing-library/react';
import { TabsBar } from './tabs_bar';
const items = Array.from({ length: 5 }).map((_, i) => ({
id: `tab-${i}`,
label: `Tab ${i}`,
}));
const tabContentId = 'test-content-id';
describe('TabsBar', () => {
it('renders tabs bar', async () => {
const onAdd = jest.fn();
const onSelect = jest.fn();
const onClose = jest.fn();
const selectedItem = items[0];
render(
<TabsBar
tabContentId={tabContentId}
items={items}
selectedItem={selectedItem}
onAdd={onAdd}
onSelect={onSelect}
onClose={onClose}
/>
);
const tabs = screen.getAllByRole('tab');
expect(tabs).toHaveLength(items.length);
items.forEach((tabItem, index) => {
const tab = tabs[index];
expect(screen.getByText(tabItem.label)).toBeInTheDocument();
expect(tab).toHaveAttribute('id', `tab-${tabItem.id}`);
expect(tab).toHaveAttribute('aria-controls', tabContentId);
expect(tab).toHaveAttribute(
'aria-selected',
tabItem.id === selectedItem.id ? 'true' : 'false'
);
});
const tab = screen.getByText(items[1].label);
tab.click();
expect(onSelect).toHaveBeenCalled();
const addButton = screen.getByTestId('unifiedTabs_tabsBar_newTabBtn');
addButton.click();
expect(onAdd).toHaveBeenCalled();
expect(onClose).not.toHaveBeenCalled();
});
});

View file

@ -0,0 +1,79 @@
/*
* 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 { i18n } from '@kbn/i18n';
import { css } from '@emotion/react';
import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, useEuiTheme } from '@elastic/eui';
import { Tab } from '../tab';
import type { TabItem } from '../../types';
export interface TabsBarProps {
items: TabItem[];
selectedItem: TabItem | null;
tabContentId: string;
onAdd: () => void;
onSelect: (item: TabItem) => void;
onClose: (item: TabItem) => void;
}
export const TabsBar: React.FC<TabsBarProps> = ({
items,
selectedItem,
tabContentId,
onAdd,
onSelect,
onClose,
}) => {
const { euiTheme } = useEuiTheme();
const addButtonLabel = i18n.translate('unifiedTabs.createTabButton', {
defaultMessage: 'New',
});
return (
<EuiFlexGroup
role="tablist"
data-test-subj="unifiedTabs_tabsBar"
responsive={false}
alignItems="center"
gutterSize="none"
className="eui-scrollBar"
css={css`
background-color: ${euiTheme.colors.lightestShade};
overflow-x: auto;
`}
>
{items.map((item) => (
<EuiFlexItem key={item.id} grow={false}>
<Tab
item={item}
isSelected={selectedItem?.id === item.id}
tabContentId={tabContentId}
onSelect={onSelect}
onClose={onClose}
/>
</EuiFlexItem>
))}
<EuiFlexItem grow={false}>
<EuiButtonIcon
data-test-subj="unifiedTabs_tabsBar_newTabBtn"
iconType="plus"
color="text"
css={css`
margin-inline: ${euiTheme.size.s};
`}
aria-label={addButtonLabel}
title={addButtonLabel}
onClick={onAdd}
/>
</EuiFlexItem>
</EuiFlexGroup>
);
};

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 { TabbedContent, type TabbedContentProps } from './components/tabbed_content';

View file

@ -0,0 +1,13 @@
/*
* 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 interface TabItem {
id: string;
label: string;
}

View file

@ -0,0 +1,17 @@
/*
* 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 { TabItem } from '../types';
export const getTabAttributes = (item: TabItem, tabContentId: string) => {
return {
id: `tab-${item.id}`,
'aria-controls': tabContentId,
};
};

View file

@ -0,0 +1,13 @@
{
"extends": "../../../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "target/types"
},
"include": ["**/*.ts", "**/*.tsx"],
"exclude": [
"target/**/*"
],
"kbn_references": [
"@kbn/i18n",
]
}

View file

@ -2024,6 +2024,10 @@
"@kbn/unified-histogram-plugin/*": ["src/platform/plugins/shared/unified_histogram/*"],
"@kbn/unified-search-plugin": ["src/platform/plugins/shared/unified_search"],
"@kbn/unified-search-plugin/*": ["src/platform/plugins/shared/unified_search/*"],
"@kbn/unified-tabs": ["src/platform/packages/shared/kbn-unified-tabs"],
"@kbn/unified-tabs/*": ["src/platform/packages/shared/kbn-unified-tabs/*"],
"@kbn/unified-tabs-examples-plugin": ["examples/unified_tabs_examples"],
"@kbn/unified-tabs-examples-plugin/*": ["examples/unified_tabs_examples/*"],
"@kbn/unsaved-changes-badge": ["src/platform/packages/private/kbn-unsaved-changes-badge"],
"@kbn/unsaved-changes-badge/*": ["src/platform/packages/private/kbn-unsaved-changes-badge/*"],
"@kbn/unsaved-changes-prompt": ["src/platform/packages/shared/kbn-unsaved-changes-prompt"],

View file

@ -7917,6 +7917,14 @@
version "0.0.0"
uid ""
"@kbn/unified-tabs-examples-plugin@link:examples/unified_tabs_examples":
version "0.0.0"
uid ""
"@kbn/unified-tabs@link:src/platform/packages/shared/kbn-unified-tabs":
version "0.0.0"
uid ""
"@kbn/unsaved-changes-badge@link:src/platform/packages/private/kbn-unsaved-changes-badge":
version "0.0.0"
uid ""