diff --git a/CHANGELOG.md b/CHANGELOG.md index f093d3d95278..eafd642ad858 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,9 +13,11 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) * [MD] Support legacy client for data source ([#2204](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2204)) * [Plugin Helpers] Facilitate version changes ([#2398](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2398)) * [MD] Display error toast for create index pattern with data source ([#2506](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2506)) -* [Multi DataSource] UX enhacement on index pattern management stack ([#2505](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2505)) +* [Multi DataSource] UX enhancement on index pattern management stack ([#2505](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2505)) * [Multi DataSource] UX enhancement on Data source management stack ([#2521](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2521)) * [Multi DataSource] UX enhancement on Index Pattern management stack ([#2527](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2527)) +* [Multi DataSource] Add data source column into index pattern table ([#2542](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2542)) + ### 🐛 Bug Fixes * [Vis Builder] Fixes auto bounds for timeseries bar chart visualization ([2401](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2401)) * [Vis Builder] Fixes visualization shift when editing agg ([2401](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2401)) diff --git a/src/plugins/data_source/public/index.ts b/src/plugins/data_source/public/index.ts index 411838d0b1bd..b69e07784ff4 100644 --- a/src/plugins/data_source/public/index.ts +++ b/src/plugins/data_source/public/index.ts @@ -3,12 +3,12 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { DataSourcePublicPlugin } from './plugin'; +import { DataSourcePlugin } from './plugin'; // This exports static code and TypeScript types, // as well as, OpenSearch Dashboards Platform `plugin()` initializer. export function plugin() { - return new DataSourcePublicPlugin(); + return new DataSourcePlugin(); } -export { DataSourcePublicPluginSetup, DataSourcePublicPluginStart } from './types'; +export { DataSourcePluginSetup, DataSourcePluginStart } from './types'; diff --git a/src/plugins/data_source/public/plugin.ts b/src/plugins/data_source/public/plugin.ts index a84091be1d9a..672db78e15cb 100644 --- a/src/plugins/data_source/public/plugin.ts +++ b/src/plugins/data_source/public/plugin.ts @@ -4,15 +4,14 @@ */ import { CoreSetup, CoreStart, Plugin } from '../../../core/public'; -import { DataSourcePublicPluginSetup, DataSourcePublicPluginStart } from './types'; +import { DataSourcePluginSetup, DataSourcePluginStart } from './types'; -export class DataSourcePublicPlugin - implements Plugin { - public setup(core: CoreSetup): DataSourcePublicPluginSetup { +export class DataSourcePlugin implements Plugin { + public setup(core: CoreSetup): DataSourcePluginSetup { return {}; } - public start(core: CoreStart): DataSourcePublicPluginStart { + public start(core: CoreStart): DataSourcePluginStart { return {}; } diff --git a/src/plugins/data_source/public/types.ts b/src/plugins/data_source/public/types.ts index 95bab57ed148..0c57dd1ea1bb 100644 --- a/src/plugins/data_source/public/types.ts +++ b/src/plugins/data_source/public/types.ts @@ -4,7 +4,7 @@ */ // eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface DataSourcePublicPluginSetup {} +export interface DataSourcePluginSetup {} // eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface DataSourcePublicPluginStart {} +export interface DataSourcePluginStart {} diff --git a/src/plugins/data_source_management/opensearch_dashboards.json b/src/plugins/data_source_management/opensearch_dashboards.json index 39877108d9c3..e5b13f6c0a1f 100644 --- a/src/plugins/data_source_management/opensearch_dashboards.json +++ b/src/plugins/data_source_management/opensearch_dashboards.json @@ -3,7 +3,7 @@ "version": "opensearchDashboards", "server": false, "ui": true, - "requiredPlugins": ["management", "dataSource"], + "requiredPlugins": ["management", "dataSource", "indexPatternManagement"], "optionalPlugins": [], "requiredBundles": ["opensearchDashboardsReact"] } diff --git a/src/plugins/data_source_management/public/components/data_source_column/data_source_column.tsx b/src/plugins/data_source_management/public/components/data_source_column/data_source_column.tsx new file mode 100644 index 000000000000..6ac2258b7811 --- /dev/null +++ b/src/plugins/data_source_management/public/components/data_source_column/data_source_column.tsx @@ -0,0 +1,73 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { i18n } from '@osd/i18n'; +import { HttpStart, SavedObjectsStart } from 'opensearch-dashboards/public'; +import { EuiBadge, EuiLink } from '@elastic/eui'; +import React from 'react'; +import { + IndexPatternTableColumn, + IndexPatternTableRecord, +} from '../../../../index_pattern_management/public'; +import { getDataSources } from '../utils'; +import { DataSourceTableItem } from '../../types'; + +type DataSourceColumnItem = DataSourceTableItem & { relativeUrl: string }; +type DataSourceMap = Map | undefined; + +export class DataSourceColumn implements IndexPatternTableColumn { + public readonly id: string = 'data_source'; + public data: DataSourceMap; + + public euiColumn = { + field: 'referenceId', + name: i18n.translate('dataSource.management.dataSourceColumn', { + defaultMessage: 'Data Source', + }), + render: (referenceId: string, index: IndexPatternTableRecord) => { + if (!referenceId) { + return null; + } + + const dataSource = this.data?.get(referenceId); + if (!dataSource) { + // Index pattern has the referenceId but data source not found. + return Deleted; + } + + const { title, relativeUrl } = dataSource; + return {title}; + }, + }; + + constructor( + private readonly savedObjectPromise: Promise, + private readonly httpPromise: Promise + ) {} + + public loadData = async () => { + const savedObject = await this.savedObjectPromise; + const { basePath } = await this.httpPromise; + + return getDataSources(savedObject.client).then((dataSources?: DataSourceTableItem[]) => { + this.data = dataSources + ?.map((dataSource) => { + return { + ...dataSource, + relativeUrl: basePath.prepend( + `/app/management/opensearch-dashboards/dataSources/${encodeURIComponent( + dataSource.id + )}` + ), + }; + }) + ?.reduce( + (map, dataSource) => map.set(dataSource.id, dataSource), + new Map() + ); + return this.data; + }); + }; +} diff --git a/src/plugins/data_source_management/public/plugin.ts b/src/plugins/data_source_management/public/plugin.ts index 159295a5a808..9f22ac7d9bb8 100644 --- a/src/plugins/data_source_management/public/plugin.ts +++ b/src/plugins/data_source_management/public/plugin.ts @@ -8,22 +8,35 @@ import { CoreSetup, CoreStart, Plugin } from '../../../core/public'; import { PLUGIN_NAME } from '../common'; import { ManagementSetup } from '../../management/public'; +import { IndexPatternManagementSetup } from '../../index_pattern_management/public'; +import { DataSourceColumn } from './components/data_source_column/data_source_column'; export interface DataSourceManagementSetupDependencies { management: ManagementSetup; + indexPatternManagement: IndexPatternManagementSetup; } const DSM_APP_ID = 'dataSources'; export class DataSourceManagementPlugin implements Plugin { - public setup(core: CoreSetup, { management }: DataSourceManagementSetupDependencies) { + public setup( + core: CoreSetup, + { management, indexPatternManagement }: DataSourceManagementSetupDependencies + ) { const opensearchDashboardsSection = management.sections.section.opensearchDashboards; if (!opensearchDashboardsSection) { throw new Error('`opensearchDashboards` management section not found.'); } + const savedObjectPromise = core + .getStartServices() + .then(([coreStart]) => coreStart.savedObjects); + const httpPromise = core.getStartServices().then(([coreStart]) => coreStart.http); + const column = new DataSourceColumn(savedObjectPromise, httpPromise); + indexPatternManagement.columns.register(column); + opensearchDashboardsSection.registerApp({ id: DSM_APP_ID, title: PLUGIN_NAME, diff --git a/src/plugins/index_pattern_management/public/components/__snapshots__/utils.test.ts.snap b/src/plugins/index_pattern_management/public/components/__snapshots__/utils.test.ts.snap index 224a5c992d58..287e24f056b4 100644 --- a/src/plugins/index_pattern_management/public/components/__snapshots__/utils.test.ts.snap +++ b/src/plugins/index_pattern_management/public/components/__snapshots__/utils.test.ts.snap @@ -5,6 +5,7 @@ Array [ Object { "default": true, "id": "test", + "referenceId": undefined, "sort": "0test name", "tags": undefined, "title": "test name", @@ -12,6 +13,7 @@ Array [ Object { "default": false, "id": "test1", + "referenceId": undefined, "sort": "1test name 1", "tags": undefined, "title": "test name 1", diff --git a/src/plugins/index_pattern_management/public/components/index_pattern_table/index_pattern_table.tsx b/src/plugins/index_pattern_management/public/components/index_pattern_table/index_pattern_table.tsx index c19c9f5b0c2d..85b47b3a1ff8 100644 --- a/src/plugins/index_pattern_management/public/components/index_pattern_table/index_pattern_table.tsx +++ b/src/plugins/index_pattern_management/public/components/index_pattern_table/index_pattern_table.tsx @@ -44,7 +44,7 @@ import { FormattedMessage } from '@osd/i18n/react'; import { withRouter, RouteComponentProps } from 'react-router-dom'; import React, { useState, useEffect } from 'react'; import { i18n } from '@osd/i18n'; -import { useMount } from 'react-use'; +import { useEffectOnce, useMount } from 'react-use'; import { reactRouterNavigate, useOpenSearchDashboards, @@ -112,6 +112,9 @@ export const IndexPatternTable = ({ canSave, history }: Props) => { const [remoteClustersExist, setRemoteClustersExist] = useState(false); const [isLoadingSources, setIsLoadingSources] = useState(!dataSourceEnabled); const [isLoadingIndexPatterns, setIsLoadingIndexPatterns] = useState(true); + const [isColumnDataLoaded, setIsColumnDataLoaded] = useState(false); + + const { columns: columnRegistry } = indexPatternManagementStart; useMount(() => { setBreadcrumbs(getListBreadcrumbs()); @@ -153,6 +156,11 @@ export const IndexPatternTable = ({ canSave, history }: Props) => { ); }; + const loadColumnData = async () => { + await Promise.all(columnRegistry.getAll().map((column) => column.loadData())); + setIsColumnDataLoaded(true); + }; + useEffect(() => { if (!dataSourceEnabled) { getIndices({ http, pattern: '*', searchClient }).then((dataSources) => { @@ -165,6 +173,10 @@ export const IndexPatternTable = ({ canSave, history }: Props) => { } }, [http, creationOptions, searchClient, dataSourceEnabled]); + useEffectOnce(() => { + loadColumnData(); + }); + chrome.docTitle.change(title); const columns = [ @@ -197,6 +209,13 @@ export const IndexPatternTable = ({ canSave, history }: Props) => { dataType: 'string' as const, sortable: ({ sort }: { sort: string }) => sort, }, + ...columnRegistry.getAll().map((column) => { + return { + ...column.euiColumn, + sortable: false, + 'data-test-subj': `indexPatternTableColumn-${column.id}`, + }; + }), ]; const createButton = canSave ? ( diff --git a/src/plugins/index_pattern_management/public/components/utils.ts b/src/plugins/index_pattern_management/public/components/utils.ts index 3929563fdc1c..cb7edcdb99ff 100644 --- a/src/plugins/index_pattern_management/public/components/utils.ts +++ b/src/plugins/index_pattern_management/public/components/utils.ts @@ -50,18 +50,22 @@ export async function getIndexPatterns( .map((pattern) => { const id = pattern.id; const title = pattern.get('title'); + const references = pattern.references; const isDefault = defaultIndex === id; const tags = (indexPatternManagementStart as IndexPatternManagementStart).list.getIndexPatternTags( pattern, isDefault ); + const reference = Array.isArray(references) ? references[0] : undefined; + const referenceId = reference?.id; return { id, title, default: isDefault, tags, + referenceId, // the prepending of 0 at the default pattern takes care of prioritization // so the sorting will but the default index on top // or on bottom of a the table diff --git a/src/plugins/index_pattern_management/public/index.ts b/src/plugins/index_pattern_management/public/index.ts index 5df23bc6e346..55994b0ad334 100644 --- a/src/plugins/index_pattern_management/public/index.ts +++ b/src/plugins/index_pattern_management/public/index.ts @@ -55,4 +55,4 @@ export { export { DefaultFormatEditor } from './components/field_editor/components/field_format_editor'; -export { MlCardState } from './types'; +export { MlCardState, IndexPatternTableColumn, IndexPatternTableRecord } from './types'; diff --git a/src/plugins/index_pattern_management/public/management_app/mount_management_section.tsx b/src/plugins/index_pattern_management/public/management_app/mount_management_section.tsx index 8a9c1dc1e7d1..162d1d0876c6 100644 --- a/src/plugins/index_pattern_management/public/management_app/mount_management_section.tsx +++ b/src/plugins/index_pattern_management/public/management_app/mount_management_section.tsx @@ -36,7 +36,6 @@ import { i18n } from '@osd/i18n'; import { I18nProvider } from '@osd/i18n/react'; import { StartServicesAccessor } from 'src/core/public'; -import { EuiIconType } from '@elastic/eui/src/components/icon/icon'; import { OpenSearchDashboardsContextProvider } from '../../../opensearch_dashboards_react/public'; import { ManagementAppMountParams } from '../../../management/public'; import { diff --git a/src/plugins/index_pattern_management/public/service/column_service/column_service.ts b/src/plugins/index_pattern_management/public/service/column_service/column_service.ts new file mode 100644 index 000000000000..9bd1812d6107 --- /dev/null +++ b/src/plugins/index_pattern_management/public/service/column_service/column_service.ts @@ -0,0 +1,41 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { IndexPatternTableColumn } from '../../types'; + +export interface IndexPatternTableColumnServiceSetup { + /** + * register given column in the registry. + */ + register: (column: IndexPatternTableColumn) => void; +} + +export interface IndexPatternTableColumnServiceStart { + /** + * return all {@link IndexPatternTableColumn | columns} currently registered. + */ + getAll: () => Array>; +} + +export class IndexPatternTableColumnService { + private readonly columns = new Map>(); + + setup(): IndexPatternTableColumnServiceSetup { + return { + register: (column) => { + if (this.columns.has(column.id)) { + throw new Error(`Index Pattern Table Column with id '${column.id}' already exists`); + } + this.columns.set(column.id, column); + }, + }; + } + + start(): IndexPatternTableColumnServiceStart { + return { + getAll: () => [...this.columns.values()], + }; + } +} diff --git a/src/plugins/index_pattern_management/public/service/column_service/index.ts b/src/plugins/index_pattern_management/public/service/column_service/index.ts new file mode 100644 index 000000000000..0af136011372 --- /dev/null +++ b/src/plugins/index_pattern_management/public/service/column_service/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './column_service'; diff --git a/src/plugins/index_pattern_management/public/service/index_pattern_management_service.ts b/src/plugins/index_pattern_management/public/service/index_pattern_management_service.ts index 784728bb8355..270e7dfe6d9a 100644 --- a/src/plugins/index_pattern_management/public/service/index_pattern_management_service.ts +++ b/src/plugins/index_pattern_management/public/service/index_pattern_management_service.ts @@ -47,6 +47,7 @@ import { TruncateFormatEditor, UrlFormatEditor, } from '../components/field_editor/components/field_format_editor'; +import { IndexPatternTableColumnService } from './column_service'; interface SetupDependencies { httpClient: HttpSetup; @@ -62,12 +63,14 @@ export class IndexPatternManagementService { indexPatternListConfig: IndexPatternListManager; fieldFormatEditors: FieldFormatEditors; environmentService: EnvironmentService; + columnService: IndexPatternTableColumnService; constructor() { this.indexPatternCreationManager = new IndexPatternCreationManager(); this.indexPatternListConfig = new IndexPatternListManager(); this.fieldFormatEditors = new FieldFormatEditors(); this.environmentService = new EnvironmentService(); + this.columnService = new IndexPatternTableColumnService(); } public setup({ httpClient }: SetupDependencies) { @@ -98,6 +101,7 @@ export class IndexPatternManagementService { list: indexPatternListConfigSetup, fieldFormatEditors: fieldFormatEditorsSetup, environment: this.environmentService.setup(), + columns: this.columnService.setup(), }; } @@ -106,6 +110,7 @@ export class IndexPatternManagementService { creation: this.indexPatternCreationManager.start(), list: this.indexPatternListConfig.start(), fieldFormatEditors: this.fieldFormatEditors.start(), + columns: this.columnService.start(), }; } diff --git a/src/plugins/index_pattern_management/public/types.ts b/src/plugins/index_pattern_management/public/types.ts index bc96fc15c8ef..24dc2cd59c31 100644 --- a/src/plugins/index_pattern_management/public/types.ts +++ b/src/plugins/index_pattern_management/public/types.ts @@ -40,6 +40,7 @@ import { SavedObjectReference, } from 'src/core/public'; import { DataPublicPluginStart } from 'src/plugins/data/public'; +import { EuiTableFieldDataColumnType } from '@elastic/eui'; import { ManagementAppMountParams } from '../../management/public'; import { IndexPatternManagementStart } from './index'; import { OpenSearchDashboardsReactContextValue } from '../../opensearch_dashboards_react/public'; @@ -71,3 +72,16 @@ export enum MlCardState { } export type DataSourceRef = { title: string } & Pick; + +export interface IndexPatternTableRecord { + type: string; + id: string; + referenceId?: string; +} + +export interface IndexPatternTableColumn { + id: string; + euiColumn: Omit, 'sortable'>; + data?: T; + loadData: () => Promise; +}