diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 0e65d3f5ff5e9..c9b89be6d4c00 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -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 diff --git a/.i18nrc.json b/.i18nrc.json index 9bac033943f56..15b1622bf6371 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -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", diff --git a/examples/developer_examples/public/app.tsx b/examples/developer_examples/public/app.tsx index 0e915eeccf1ec..2aef630170adc 100644 --- a/examples/developer_examples/public/app.tsx +++ b/examples/developer_examples/public/app.tsx @@ -61,64 +61,66 @@ function DeveloperExamples({ startServices, examples, navigateToApp, getUrlForAp return ( - - - - - - The following examples showcase services and APIs that are available to developers. - - - - setSearch(e.target.value)} - isClearable={true} - aria-label="Search developer examples" - /> - - - - - - {filteredExamples.map((def) => ( - - - {def.description} - - } - title={ - - { - navigateToApp(def.appId); - }} - > - - {def.title} - - - - window.open(getUrlForApp(def.appId), '_blank', 'noopener, noreferrer') - } - > - Open in new tab - - - } - image={def.image} - footer={def.links ? : undefined} + + + + + + + The following examples showcase services and APIs that are available to developers. + + + + setSearch(e.target.value)} + isClearable={true} + aria-label="Search developer examples" /> - ))} - - + + + + + {filteredExamples.map((def) => ( + + + {def.description} + + } + title={ + + { + navigateToApp(def.appId); + }} + > + + {def.title} + + + + window.open(getUrlForApp(def.appId), '_blank', 'noopener, noreferrer') + } + > + Open in new tab + + + } + image={def.image} + footer={def.links ? : undefined} + /> + + ))} + + + ); } diff --git a/examples/unified_tabs_examples/README.md b/examples/unified_tabs_examples/README.md new file mode 100644 index 0000000000000..9102762c41f90 --- /dev/null +++ b/examples/unified_tabs_examples/README.md @@ -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 +``` \ No newline at end of file diff --git a/examples/unified_tabs_examples/common/index.ts b/examples/unified_tabs_examples/common/index.ts new file mode 100644 index 0000000000000..1206d6c6ac01c --- /dev/null +++ b/examples/unified_tabs_examples/common/index.ts @@ -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'; diff --git a/examples/unified_tabs_examples/kibana.jsonc b/examples/unified_tabs_examples/kibana.jsonc new file mode 100644 index 0000000000000..56b8b20fa5820 --- /dev/null +++ b/examples/unified_tabs_examples/kibana.jsonc @@ -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" + ] + } +} diff --git a/examples/unified_tabs_examples/public/application.tsx b/examples/unified_tabs_examples/public/application.tsx new file mode 100644 index 0000000000000..f89f4df65b149 --- /dev/null +++ b/examples/unified_tabs_examples/public/application.tsx @@ -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( + + + + + , + element + ); + + return () => { + ReactDOM.unmountComponentAtNode(element); + }; +}; diff --git a/examples/unified_tabs_examples/public/example_app.tsx b/examples/unified_tabs_examples/public/example_app.tsx new file mode 100644 index 0000000000000..1bd3ceb950376 --- /dev/null +++ b/examples/unified_tabs_examples/public/example_app.tsx @@ -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 = ({ + services, + setHeaderActionMenu, +}) => { + const { euiTheme } = useEuiTheme(); + const { navigation, data, unifiedSearch } = services; + const { IndexPatternSelect } = unifiedSearch.ui; + const [dataView, setDataView] = useState(); + const [selectedFieldNames, setSelectedFieldNames] = useState([]); + + 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 ( + } + title={

{PLUGIN_NAME}

} + body={

Loading...

} + /> + ); + } + + const SearchBar = navigation.ui.AggregateQueryTopNavMenu; + + return ( + + + {dataView ? ( +
+ {}} + createItem={() => { + TMP_COUNTER += 1; + return { + id: `tab_${TMP_COUNTER}`, + label: `Tab ${TMP_COUNTER}`, + }; + }} + renderContent={({ label }) => { + return ( + + + {}} + 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} + /> + + + + + + + + + {PLUGIN_NAME}} + body={

Tab: {label}

} + /> +
+
+
+
+
+ ); + }} + /> +
+ ) : ( + Make sure to have at least one data view} + body={ +

+ { + if (dataViewId) { + const newDataView = await data.dataViews.get(dataViewId); + setDataView(newDataView); + } else { + setDataView(undefined); + } + }} + isClearable={false} + data-test-subj="dataViewSelector" + /> +

+ } + /> + )} +
+
+ ); +}; diff --git a/examples/unified_tabs_examples/public/field_list_sidebar.tsx b/examples/unified_tabs_examples/public/field_list_sidebar.tsx new file mode 100644 index 0000000000000..37b545debfac1 --- /dev/null +++ b/examples/unified_tabs_examples/public/field_list_sidebar.tsx @@ -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 = ({ + dataView, + selectedFieldNames, + services, + onAddFieldToWorkspace, + onRemoveFieldFromWorkspace, +}) => { + const unifiedFieldListContainerRef = useRef(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 ( + + ); +}; diff --git a/examples/unified_tabs_examples/public/index.ts b/examples/unified_tabs_examples/public/index.ts new file mode 100644 index 0000000000000..cb653d8e71fe6 --- /dev/null +++ b/examples/unified_tabs_examples/public/index.ts @@ -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'; diff --git a/examples/unified_tabs_examples/public/plugin.ts b/examples/unified_tabs_examples/public/plugin.ts new file mode 100644 index 0000000000000..95e6f610ae7f6 --- /dev/null +++ b/examples/unified_tabs_examples/public/plugin.ts @@ -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, + { 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() {} +} diff --git a/examples/unified_tabs_examples/public/types.ts b/examples/unified_tabs_examples/public/types.ts new file mode 100644 index 0000000000000..5a513bf2312b3 --- /dev/null +++ b/examples/unified_tabs_examples/public/types.ts @@ -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; +} diff --git a/examples/unified_tabs_examples/public/unified_tabs.png b/examples/unified_tabs_examples/public/unified_tabs.png new file mode 100644 index 0000000000000..5b9268b93d2ca Binary files /dev/null and b/examples/unified_tabs_examples/public/unified_tabs.png differ diff --git a/examples/unified_tabs_examples/tsconfig.json b/examples/unified_tabs_examples/tsconfig.json new file mode 100644 index 0000000000000..6b5537532e9b2 --- /dev/null +++ b/examples/unified_tabs_examples/tsconfig.json @@ -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", + ] +} diff --git a/package.json b/package.json index 2f5a79adc400c..3c56a8f1b5b1e 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/dev/storybook/aliases.ts b/src/dev/storybook/aliases.ts index df1d67b5d0d43..8f69942688cae 100644 --- a/src/dev/storybook/aliases.ts +++ b/src/dev/storybook/aliases.ts @@ -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', }; diff --git a/src/platform/packages/shared/kbn-unified-tabs/.storybook/main.js b/src/platform/packages/shared/kbn-unified-tabs/.storybook/main.js new file mode 100644 index 0000000000000..4c71be3362b05 --- /dev/null +++ b/src/platform/packages/shared/kbn-unified-tabs/.storybook/main.js @@ -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; diff --git a/src/platform/packages/shared/kbn-unified-tabs/README.md b/src/platform/packages/shared/kbn-unified-tabs/README.md new file mode 100644 index 0000000000000..461b6e0fe834f --- /dev/null +++ b/src/platform/packages/shared/kbn-unified-tabs/README.md @@ -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`. diff --git a/src/platform/packages/shared/kbn-unified-tabs/index.ts b/src/platform/packages/shared/kbn-unified-tabs/index.ts new file mode 100644 index 0000000000000..d4a44151200f9 --- /dev/null +++ b/src/platform/packages/shared/kbn-unified-tabs/index.ts @@ -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'; diff --git a/src/platform/packages/shared/kbn-unified-tabs/jest.config.js b/src/platform/packages/shared/kbn-unified-tabs/jest.config.js new file mode 100644 index 0000000000000..8512b18559aa3 --- /dev/null +++ b/src/platform/packages/shared/kbn-unified-tabs/jest.config.js @@ -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: ['/src/platform/packages/shared/kbn-unified-tabs'], +}; diff --git a/src/platform/packages/shared/kbn-unified-tabs/kibana.jsonc b/src/platform/packages/shared/kbn-unified-tabs/kibana.jsonc new file mode 100644 index 0000000000000..2049b93e7bdca --- /dev/null +++ b/src/platform/packages/shared/kbn-unified-tabs/kibana.jsonc @@ -0,0 +1,7 @@ +{ + "type": "shared-browser", + "id": "@kbn/unified-tabs", + "owner": "@elastic/kibana-data-discovery", + "group": "platform", + "visibility": "shared" +} diff --git a/src/platform/packages/shared/kbn-unified-tabs/package.json b/src/platform/packages/shared/kbn-unified-tabs/package.json new file mode 100644 index 0000000000000..56686296db070 --- /dev/null +++ b/src/platform/packages/shared/kbn-unified-tabs/package.json @@ -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 +} \ No newline at end of file diff --git a/src/platform/packages/shared/kbn-unified-tabs/src/components/__stories__/storybook_constants.ts b/src/platform/packages/shared/kbn-unified-tabs/src/components/__stories__/storybook_constants.ts new file mode 100644 index 0000000000000..6d0b9a6de4c7b --- /dev/null +++ b/src/platform/packages/shared/kbn-unified-tabs/src/components/__stories__/storybook_constants.ts @@ -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'; diff --git a/src/platform/packages/shared/kbn-unified-tabs/src/components/__stories__/tab.stories.tsx b/src/platform/packages/shared/kbn-unified-tabs/src/components/__stories__/tab.stories.tsx new file mode 100644 index 0000000000000..944c2c3b94c47 --- /dev/null +++ b/src/platform/packages/shared/kbn-unified-tabs/src/components/__stories__/tab.stories.tsx @@ -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> = (args) => ( + +); + +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, +}; diff --git a/src/platform/packages/shared/kbn-unified-tabs/src/components/__stories__/tabs.stories.tsx b/src/platform/packages/shared/kbn-unified-tabs/src/components/__stories__/tabs.stories.tsx new file mode 100644 index 0000000000000..cfe170a5a94a2 --- /dev/null +++ b/src/platform/packages/shared/kbn-unified-tabs/src/components/__stories__/tabs.stories.tsx @@ -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> = (args) => ( + { + TMP_COUNTER += 1; + return { + id: `tab_${TMP_COUNTER}`, + label: `Tab ${TMP_COUNTER}`, + }; + }} + onChanged={action('onClosed')} + renderContent={(item) => ( +
Content for tab: {item.label}
+ )} + /> +); + +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', +}; diff --git a/src/platform/packages/shared/kbn-unified-tabs/src/components/tab/index.ts b/src/platform/packages/shared/kbn-unified-tabs/src/components/tab/index.ts new file mode 100644 index 0000000000000..1ea506935d64a --- /dev/null +++ b/src/platform/packages/shared/kbn-unified-tabs/src/components/tab/index.ts @@ -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'; diff --git a/src/platform/packages/shared/kbn-unified-tabs/src/components/tab/tab.test.tsx b/src/platform/packages/shared/kbn-unified-tabs/src/components/tab/tab.test.tsx new file mode 100644 index 0000000000000..f344e8603a946 --- /dev/null +++ b/src/platform/packages/shared/kbn-unified-tabs/src/components/tab/tab.test.tsx @@ -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( + + ); + + 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); + }); +}); diff --git a/src/platform/packages/shared/kbn-unified-tabs/src/components/tab/tab.tsx b/src/platform/packages/shared/kbn-unified-tabs/src/components/tab/tab.tsx new file mode 100644 index 0000000000000..32418db39f1f4 --- /dev/null +++ b/src/platform/packages/shared/kbn-unified-tabs/src/components/tab/tab.tsx @@ -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 = ({ 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) => { + event.stopPropagation(); + + if (!isSelected) { + onSelect(item); + } + }, + [onSelect, item, isSelected] + ); + + const onCloseEvent = useCallback( + (event: MouseEvent) => { + event.stopPropagation(); + onClose(item); + }, + [onClose, item] + ); + + const onClickEvent = useCallback( + (event: MouseEvent) => { + 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 ( + + + + + + + ); +}; + +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; + `; +} diff --git a/src/platform/packages/shared/kbn-unified-tabs/src/components/tabbed_content/index.ts b/src/platform/packages/shared/kbn-unified-tabs/src/components/tabbed_content/index.ts new file mode 100644 index 0000000000000..5c49944e26aa5 --- /dev/null +++ b/src/platform/packages/shared/kbn-unified-tabs/src/components/tabbed_content/index.ts @@ -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'; diff --git a/src/platform/packages/shared/kbn-unified-tabs/src/components/tabbed_content/tabbed_content.tsx b/src/platform/packages/shared/kbn-unified-tabs/src/components/tabbed_content/tabbed_content.tsx new file mode 100644 index 0000000000000..5f35fbb2c753f --- /dev/null +++ b/src/platform/packages/shared/kbn-unified-tabs/src/components/tabbed_content/tabbed_content.tsx @@ -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 = ({ + initialItems, + initialSelectedItemId, + renderContent, + createItem, + onChanged, +}) => { + const [tabContentId] = useState(() => htmlIdGenerator()()); + const [state, _setState] = useState(() => { + 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 ( + + + + + {selectedItem ? ( + + {renderContent(selectedItem)} + + ) : null} + + ); +}; diff --git a/src/platform/packages/shared/kbn-unified-tabs/src/components/tabs_bar/index.ts b/src/platform/packages/shared/kbn-unified-tabs/src/components/tabs_bar/index.ts new file mode 100644 index 0000000000000..cd648b74a65a0 --- /dev/null +++ b/src/platform/packages/shared/kbn-unified-tabs/src/components/tabs_bar/index.ts @@ -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'; diff --git a/src/platform/packages/shared/kbn-unified-tabs/src/components/tabs_bar/tabs_bar.test.tsx b/src/platform/packages/shared/kbn-unified-tabs/src/components/tabs_bar/tabs_bar.test.tsx new file mode 100644 index 0000000000000..5918fc0c4bd01 --- /dev/null +++ b/src/platform/packages/shared/kbn-unified-tabs/src/components/tabs_bar/tabs_bar.test.tsx @@ -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( + + ); + + 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(); + }); +}); diff --git a/src/platform/packages/shared/kbn-unified-tabs/src/components/tabs_bar/tabs_bar.tsx b/src/platform/packages/shared/kbn-unified-tabs/src/components/tabs_bar/tabs_bar.tsx new file mode 100644 index 0000000000000..0ba5439106aa1 --- /dev/null +++ b/src/platform/packages/shared/kbn-unified-tabs/src/components/tabs_bar/tabs_bar.tsx @@ -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 = ({ + items, + selectedItem, + tabContentId, + onAdd, + onSelect, + onClose, +}) => { + const { euiTheme } = useEuiTheme(); + + const addButtonLabel = i18n.translate('unifiedTabs.createTabButton', { + defaultMessage: 'New', + }); + + return ( + + {items.map((item) => ( + + + + ))} + + + + + ); +}; diff --git a/src/platform/packages/shared/kbn-unified-tabs/src/index.ts b/src/platform/packages/shared/kbn-unified-tabs/src/index.ts new file mode 100644 index 0000000000000..0466ddebdde0d --- /dev/null +++ b/src/platform/packages/shared/kbn-unified-tabs/src/index.ts @@ -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'; diff --git a/src/platform/packages/shared/kbn-unified-tabs/src/types.ts b/src/platform/packages/shared/kbn-unified-tabs/src/types.ts new file mode 100644 index 0000000000000..16c76b660efa0 --- /dev/null +++ b/src/platform/packages/shared/kbn-unified-tabs/src/types.ts @@ -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; +} diff --git a/src/platform/packages/shared/kbn-unified-tabs/src/utils/get_tab_attributes.ts b/src/platform/packages/shared/kbn-unified-tabs/src/utils/get_tab_attributes.ts new file mode 100644 index 0000000000000..3f6a158bf1474 --- /dev/null +++ b/src/platform/packages/shared/kbn-unified-tabs/src/utils/get_tab_attributes.ts @@ -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, + }; +}; diff --git a/src/platform/packages/shared/kbn-unified-tabs/tsconfig.json b/src/platform/packages/shared/kbn-unified-tabs/tsconfig.json new file mode 100644 index 0000000000000..308623f8fba0a --- /dev/null +++ b/src/platform/packages/shared/kbn-unified-tabs/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types" + }, + "include": ["**/*.ts", "**/*.tsx"], + "exclude": [ + "target/**/*" + ], + "kbn_references": [ + "@kbn/i18n", + ] +} diff --git a/tsconfig.base.json b/tsconfig.base.json index b75f01b399c08..46cb4bdbb369c 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -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"], diff --git a/yarn.lock b/yarn.lock index 4899ea1ea11e2..b5918c235ab21 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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 ""