diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a8dca630e5..62c071bc2ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -82,6 +82,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - [Workspace] Add permission control logic ([#6052](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6052)) - [Multiple Datasource] Add default icon for selectable component and make sure the default datasource shows automatically ([#6327](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6327)) - [Multiple Datasource] Pass selected data sources to plugin consumers when the multi-select component initially loads ([#6333](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6333)) +- [Mulitple Datasource] Add multi data source support to TSVB ([#6298](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6298)) - [Multiple Datasource] Add installedPlugins list to data source saved object ([#6348](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6348)) - [Multiple Datasource] Add default icon in multi-selectable picker ([#6357](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6357)) - [Workspace] Add APIs to support plugin state in request ([#6303](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6303)) diff --git a/src/plugins/data/server/index.ts b/src/plugins/data/server/index.ts index 46c2b1ca047..4bc3ad62a4a 100644 --- a/src/plugins/data/server/index.ts +++ b/src/plugins/data/server/index.ts @@ -145,6 +145,7 @@ export { FieldDescriptor as IndexPatternFieldDescriptor, shouldReadFieldFromDocValues, // used only in logstash_fields fixture FieldDescriptor, + decideClient, } from './index_patterns'; export { diff --git a/src/plugins/data/server/index_patterns/index.ts b/src/plugins/data/server/index_patterns/index.ts index b2e832294e4..771aa9c09ab 100644 --- a/src/plugins/data/server/index_patterns/index.ts +++ b/src/plugins/data/server/index_patterns/index.ts @@ -31,3 +31,4 @@ export * from './utils'; export { IndexPatternsFetcher, FieldDescriptor, shouldReadFieldFromDocValues } from './fetcher'; export { IndexPatternsService, IndexPatternsServiceStart } from './index_patterns_service'; +export { decideClient } from './routes'; diff --git a/src/plugins/data/server/index_patterns/routes.ts b/src/plugins/data/server/index_patterns/routes.ts index 3adc1970dd8..8b3c7139ffc 100644 --- a/src/plugins/data/server/index_patterns/routes.ts +++ b/src/plugins/data/server/index_patterns/routes.ts @@ -155,7 +155,7 @@ export function registerRoutes(http: HttpServiceSetup) { ); } -const decideClient = async ( +export const decideClient = async ( context: RequestHandlerContext, request: any ): Promise => { diff --git a/src/plugins/data_source_management/public/components/data_source_selector/data_source_selector.tsx b/src/plugins/data_source_management/public/components/data_source_selector/data_source_selector.tsx index bfe02b7288a..8d2a90943b9 100644 --- a/src/plugins/data_source_management/public/components/data_source_selector/data_source_selector.tsx +++ b/src/plugins/data_source_management/public/components/data_source_selector/data_source_selector.tsx @@ -34,6 +34,7 @@ export interface DataSourceSelectorProps { dataSourceFilter?: (dataSource: SavedObject) => boolean; compressed?: boolean; uiSettings?: IUiSettingsClient; + isClearable?: boolean; } interface DataSourceSelectorState { @@ -202,6 +203,7 @@ export class DataSourceSelector extends React.Component< return ( } + helpText={i18n.translate('visTypeTimeseries.indexPattern.searchByIndex', { + defaultMessage: + 'Use an asterisk (*) to match multiple indices. Spaces and the characters , /, ?, ", <, >, | are not allowed.', + })} fullWidth > )} + {!!dataSourceManagementEnabled && ( + + + + + + + + )} , | are not allowed.', + }) } > { + let handleChange: jest.Mock; + let changeHandler: (selectedOptions: []) => void; + + beforeEach(() => { + handleChange = jest.fn(); + changeHandler = createDataSourcePickerHandler(handleChange); + }); + + test.each([ + { + id: undefined, + }, + {}, + ])( + 'calls handleChange() and sets data_source_id to undefined if id cannot be found or is undefined', + ({ id }) => { + // @ts-ignore + changeHandler([{ id }]); + expect(handleChange.mock.calls.length).toEqual(1); + expect(handleChange.mock.calls[0][0]).toEqual({ + data_source_id: undefined, + }); + } + ); + + test.each([ + { + id: '', + }, + { + id: 'foo', + }, + ])('calls handleChange() function with partial and updates the data_source_id', ({ id }) => { + // @ts-ignore + changeHandler([{ id }]); + expect(handleChange.mock.calls.length).toEqual(1); + expect(handleChange.mock.calls[0][0]).toEqual({ + data_source_id: id, + }); + }); +}); diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/create_data_source_change_handler.ts b/src/plugins/vis_type_timeseries/public/application/components/lib/create_data_source_change_handler.ts new file mode 100644 index 00000000000..5fa18d74c5b --- /dev/null +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/create_data_source_change_handler.ts @@ -0,0 +1,16 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import _ from 'lodash'; + +import { PanelSchema } from 'src/plugins/vis_type_timeseries/common/types'; +import { DATA_SOURCE_ID_KEY } from '../../../../common/constants'; + +export const createDataSourcePickerHandler = (handleChange: (e: PanelSchema) => void) => { + return (selectedOptions: []): void => { + return handleChange?.({ + [DATA_SOURCE_ID_KEY]: _.get(selectedOptions, '[0].id', undefined), + } as PanelSchema); + }; +}; diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_editor.js b/src/plugins/vis_type_timeseries/public/application/components/vis_editor.js index e3d78bd0a82..c91d1f084f9 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_editor.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_editor.js @@ -41,6 +41,7 @@ import { createBrushHandler } from '../lib/create_brush_handler'; import { fetchFields } from '../lib/fetch_fields'; import { extractIndexPatterns } from '../../../../../plugins/vis_type_timeseries/common/extract_index_patterns'; import { getSavedObjectsClient, getUISettings, getDataStart, getCoreStart } from '../../services'; +import { DATA_SOURCE_ID_KEY } from '../../../common/constants'; import { CoreStartContextProvider } from '../contexts/query_input_bar_context'; import { OpenSearchDashboardsContextProvider } from '../../../../../plugins/opensearch_dashboards_react/public'; @@ -113,9 +114,13 @@ export class VisEditor extends Component { } if (this.props.isEditorMode) { + const dataSourceId = nextModel[DATA_SOURCE_ID_KEY] || undefined; const extractedIndexPatterns = extractIndexPatterns(nextModel); - if (!isEqual(this.state.extractedIndexPatterns, extractedIndexPatterns)) { - fetchFields(extractedIndexPatterns).then((visFields) => + if ( + !isEqual(this.state.extractedIndexPatterns, extractedIndexPatterns) || + !isEqual(this.state.model[DATA_SOURCE_ID_KEY], dataSourceId) + ) { + fetchFields(extractedIndexPatterns, dataSourceId).then((visFields) => this.setState({ visFields, extractedIndexPatterns, diff --git a/src/plugins/vis_type_timeseries/public/application/lib/fetch_fields.js b/src/plugins/vis_type_timeseries/public/application/lib/fetch_fields.js index cac4c910fee..8aa9bb618ca 100644 --- a/src/plugins/vis_type_timeseries/public/application/lib/fetch_fields.js +++ b/src/plugins/vis_type_timeseries/public/application/lib/fetch_fields.js @@ -30,9 +30,10 @@ import { i18n } from '@osd/i18n'; import { extractIndexPatterns } from '../../../common/extract_index_patterns'; +import { DATA_SOURCE_ID_KEY } from '../../../common/constants'; import { getCoreStart } from '../../services'; -export async function fetchFields(indexPatterns = ['*']) { +export async function fetchFields(indexPatterns = ['*'], dataSourceId = undefined) { const patterns = Array.isArray(indexPatterns) ? indexPatterns : [indexPatterns]; try { const indexFields = await Promise.all( @@ -40,6 +41,7 @@ export async function fetchFields(indexPatterns = ['*']) { return getCoreStart().http.get('/api/metrics/fields', { query: { index: pattern, + data_source: dataSourceId, }, }); }) @@ -62,7 +64,8 @@ export async function fetchFields(indexPatterns = ['*']) { } export async function fetchIndexPatternFields({ params, fields = {} }) { + const dataSourceId = params[DATA_SOURCE_ID_KEY] || undefined; const indexPatterns = extractIndexPatterns(params, fields); - return await fetchFields(indexPatterns); + return await fetchFields(indexPatterns, dataSourceId); } diff --git a/src/plugins/vis_type_timeseries/public/plugin.ts b/src/plugins/vis_type_timeseries/public/plugin.ts index da565a16016..0220a532042 100644 --- a/src/plugins/vis_type_timeseries/public/plugin.ts +++ b/src/plugins/vis_type_timeseries/public/plugin.ts @@ -36,6 +36,8 @@ import { CoreStart, Plugin, } from 'opensearch-dashboards/public'; +import { DataSourceManagementPluginSetup } from 'src/plugins/data_source_management/public'; +import { DataSourcePluginSetup } from 'src/plugins/data_source/public'; import { Plugin as ExpressionsPublicPlugin } from '../../expressions/public'; import { VisualizationsSetup } from '../../visualizations/public'; @@ -49,6 +51,8 @@ import { setCoreStart, setDataStart, setChartsSetup, + setDataSourceManagementSetup, + setNotifications, } from './services'; import { DataPublicPluginStart } from '../../data/public'; import { ChartsPluginSetup } from '../../charts/public'; @@ -58,6 +62,8 @@ export interface MetricsPluginSetupDependencies { expressions: ReturnType; visualizations: VisualizationsSetup; charts: ChartsPluginSetup; + dataSourceManagement?: DataSourceManagementPluginSetup; + dataSource?: DataSourcePluginSetup; } /** @internal */ @@ -75,12 +81,19 @@ export class MetricsPlugin implements Plugin, void> { public async setup( core: CoreSetup, - { expressions, visualizations, charts }: MetricsPluginSetupDependencies + { + expressions, + visualizations, + charts, + dataSourceManagement, + dataSource, + }: MetricsPluginSetupDependencies ) { expressions.registerFunction(createMetricsFn); setUISettings(core.uiSettings); setChartsSetup(charts); visualizations.createReactVisualization(metricsVisDefinition); + setDataSourceManagementSetup({ dataSourceManagement }); } public start(core: CoreStart, { data }: MetricsPluginStartDependencies) { @@ -89,5 +102,6 @@ export class MetricsPlugin implements Plugin, void> { setFieldFormats(data.fieldFormats); setDataStart(data); setCoreStart(core); + setNotifications(core.notifications); } } diff --git a/src/plugins/vis_type_timeseries/public/services.ts b/src/plugins/vis_type_timeseries/public/services.ts index 15532bc4fd6..5f54ac3e754 100644 --- a/src/plugins/vis_type_timeseries/public/services.ts +++ b/src/plugins/vis_type_timeseries/public/services.ts @@ -28,7 +28,14 @@ * under the License. */ -import { I18nStart, SavedObjectsStart, IUiSettingsClient, CoreStart } from 'src/core/public'; +import { + I18nStart, + SavedObjectsStart, + IUiSettingsClient, + CoreStart, + NotificationsStart, +} from 'src/core/public'; +import { DataSourceManagementPluginSetup } from 'src/plugins/data_source_management/public'; import { createGetterSetter } from '../../opensearch_dashboards_utils/public'; import { ChartsPluginSetup } from '../../charts/public'; import { DataPublicPluginStart } from '../../data/public'; @@ -52,3 +59,11 @@ export const [getI18n, setI18n] = createGetterSetter('I18n'); export const [getChartsSetup, setChartsSetup] = createGetterSetter( 'ChartsPluginSetup' ); + +export const [getDataSourceManagementSetup, setDataSourceManagementSetup] = createGetterSetter<{ + dataSourceManagement: DataSourceManagementPluginSetup | undefined; +}>('DataSourceManagementSetup'); + +export const [getNotifications, setNotifications] = createGetterSetter( + 'Notifications' +); diff --git a/src/plugins/vis_type_timeseries/server/lib/get_fields.ts b/src/plugins/vis_type_timeseries/server/lib/get_fields.ts index 1752d3f91f8..56a58b43b45 100644 --- a/src/plugins/vis_type_timeseries/server/lib/get_fields.ts +++ b/src/plugins/vis_type_timeseries/server/lib/get_fields.ts @@ -37,6 +37,7 @@ import { indexPatterns, IndexPatternFieldDescriptor, IndexPatternsFetcher, + decideClient, } from '../../../data/server'; import { ReqFacade } from './search_strategies/strategies/abstract_search_strategy'; @@ -50,15 +51,15 @@ export async function getFields( // removes the need to refactor many layers of dependencies on "req", and instead just augments the top // level object passed from here. The layers should be refactored fully at some point, but for now // this works and we are still using the New Platform services for these vis data portions. + const client = await decideClient(requestContext, request); + const reqFacade: ReqFacade = { requestContext, ...request, framework, payload: {}, pre: { - indexPatternsService: new IndexPatternsFetcher( - requestContext.core.opensearch.legacy.client.callAsCurrentUser - ), + indexPatternsService: new IndexPatternsFetcher(client), }, getUiSettingsService: () => requestContext.core.uiSettings.client, getSavedObjectsClient: () => requestContext.core.savedObjects.client, diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.js b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.js index fa130462e78..068a7ef06c0 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.js +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.js @@ -65,7 +65,7 @@ describe('AbstractSearchStrategy', () => { }); }); - test('should return response', async () => { + test('should return response for local cluster queries', async () => { const searches = [{ body: 'body', index: 'index' }]; const searchFn = jest.fn().mockReturnValue(Promise.resolve({})); @@ -107,4 +107,50 @@ describe('AbstractSearchStrategy', () => { } ); }); + + test('should return response for datasource query', async () => { + const searches = [{ body: 'body', index: 'index' }]; + const searchFn = jest.fn().mockReturnValue(Promise.resolve({})); + + const responses = await abstractSearchStrategy.search( + { + requestContext: {}, + framework: { + core: { + getStartServices: jest.fn().mockReturnValue( + Promise.resolve([ + {}, + { + data: { + search: { + search: searchFn, + }, + }, + }, + ]) + ), + }, + }, + }, + searches, + {}, + 'some-data-source-id' + ); + + expect(responses).toEqual([{}]); + expect(searchFn).toHaveBeenCalledWith( + {}, + { + dataSourceId: 'some-data-source-id', + params: { + body: 'body', + index: 'index', + }, + indexType: undefined, + }, + { + strategy: 'opensearch', + } + ); + }); }); diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts index 6b69628353a..7c333fad7f7 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts @@ -66,7 +66,7 @@ export class AbstractSearchStrategy { this.additionalParams = additionalParams; } - async search(req: ReqFacade, bodies: any[], options = {}) { + async search(req: ReqFacade, bodies: any[], options = {}, dataSourceId?: string) { const [, deps] = await req.framework.core.getStartServices(); const requests: any[] = []; bodies.forEach((body) => { @@ -74,6 +74,7 @@ export class AbstractSearchStrategy { deps.data.search.search( req.requestContext, { + ...(!!dataSourceId && { dataSourceId }), params: { ...body, ...this.additionalParams, diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/get_annotations.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/get_annotations.js index 16b526d1ba2..3dd88b83034 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/get_annotations.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/get_annotations.js @@ -54,6 +54,7 @@ export async function getAnnotations({ const annotations = panel.annotations.filter(validAnnotation); const lastSeriesTimestamp = getLastSeriesTimestamp(series); const handleAnnotationResponseBy = handleAnnotationResponse(lastSeriesTimestamp); + const panelDataSourceId = panel.data_source_id; const bodiesPromises = annotations.map((annotation) => getAnnotationRequestParams(req, panel, annotation, opensearchQueryConfig, capabilities) @@ -67,7 +68,7 @@ export async function getAnnotations({ if (!searches.length) return { responses: [] }; try { - const data = await searchStrategy.search(req, searches); + const data = await searchStrategy.search(req, searches, {}, panelDataSourceId); return annotations.reduce((acc, annotation, index) => { acc[annotation.id] = handleAnnotationResponseBy(data[index].rawResponse, annotation); diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/get_series_data.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/get_series_data.js index a5c8a239d2b..59d445f9332 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/get_series_data.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/get_series_data.js @@ -41,6 +41,7 @@ export async function getSeriesData(req, panel) { capabilities, } = await req.framework.searchStrategyRegistry.getViableStrategyForPanel(req, panel); const opensearchQueryConfig = await getOpenSearchQueryConfig(req); + const panelDataSourceId = panel.data_source_id; const meta = { type: panel.type, uiRestrictions: capabilities.uiRestrictions, @@ -56,7 +57,7 @@ export async function getSeriesData(req, panel) { [] ); - const data = await searchStrategy.search(req, searches); + const data = await searchStrategy.search(req, searches, {}, panelDataSourceId); const handleResponseBodyFn = handleResponseBody(panel); diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/get_table_data.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/get_table_data.js index 0b744638c3d..20dfa06c1a7 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/get_table_data.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/get_table_data.js @@ -37,6 +37,7 @@ import { getIndexPatternObject } from './helpers/get_index_pattern'; export async function getTableData(req, panel) { const panelIndexPattern = panel.index_pattern; + const panelDataSourceId = panel.data_source_id; const { searchStrategy, @@ -58,12 +59,17 @@ export async function getTableData(req, panel) { indexPatternObject, capabilities ); - const [resp] = await searchStrategy.search(req, [ - { - body, - index: panelIndexPattern, - }, - ]); + const [resp] = await searchStrategy.search( + req, + [ + { + body, + index: panelIndexPattern, + }, + ], + {}, + panelDataSourceId + ); const buckets = get( resp.rawResponse ? resp.rawResponse : resp, diff --git a/src/plugins/vis_type_timeseries/server/plugin.ts b/src/plugins/vis_type_timeseries/server/plugin.ts index 654c85d452b..af95e2e6b5d 100644 --- a/src/plugins/vis_type_timeseries/server/plugin.ts +++ b/src/plugins/vis_type_timeseries/server/plugin.ts @@ -40,6 +40,7 @@ import { } from 'src/core/server'; import { Observable } from 'rxjs'; import { Server } from '@hapi/hapi'; +import { DataSourcePluginSetup } from 'src/plugins/data_source/server'; import { VisTypeTimeseriesConfig } from './config'; import { getVisData, GetVisData, GetVisDataOptions } from './lib/get_vis_data'; import { ValidationTelemetryService } from './validation_telemetry'; @@ -57,6 +58,7 @@ export interface LegacySetup { interface VisTypeTimeseriesPluginSetupDependencies { usageCollection?: UsageCollectionSetup; + dataSource?: DataSourcePluginSetup; } interface VisTypeTimeseriesPluginStartDependencies { diff --git a/src/plugins/vis_type_timeseries/server/routes/fields.ts b/src/plugins/vis_type_timeseries/server/routes/fields.ts index bff34ee159f..80a04918c51 100644 --- a/src/plugins/vis_type_timeseries/server/routes/fields.ts +++ b/src/plugins/vis_type_timeseries/server/routes/fields.ts @@ -38,12 +38,17 @@ export const fieldsRoutes = (framework: Framework) => { { path: '/api/metrics/fields', validate: { - query: schema.object({ index: schema.string() }), + query: schema.object({ + index: schema.string(), + data_source: schema.maybe(schema.string()), + }), }, }, async (context, req, res) => { try { - return res.ok({ body: await getFields(context, req, framework, req.query.index) }); + return res.ok({ + body: await getFields(context, req, framework, req.query.index), + }); } catch (err) { if (isBoom(err) && err.output.statusCode === 401) { return res.customError({