Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multiple Datasource Support #4931

72 changes: 72 additions & 0 deletions src/plugins/data/public/data_sources/datasource/datasource.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

/**
* Abstract class representing a data source. This class provides foundational
* interfaces for specific data sources. Any data source connection needs to extend
* and implement from this base class
*
* DataSourceMetaData: Represents metadata associated with the data source.
* SourceDataSet: Represents the dataset associated with the data source.
* DataSourceQueryResult: Represents the result from querying the data source.
*/

import { ConnectionStatus } from './types';

export abstract class DataSource<
DataSourceMetaData,
DataSetParams,
SourceDataSet,
DataSourceQueryParams,
DataSourceQueryResult
> {
constructor(
private readonly name: string,
private readonly type: string,
private readonly metadata: DataSourceMetaData
) {}

getName() {
return this.name;

Check warning on line 32 in src/plugins/data/public/data_sources/datasource/datasource.ts

View check run for this annotation

Codecov / codecov/patch

src/plugins/data/public/data_sources/datasource/datasource.ts#L32

Added line #L32 was not covered by tests
}

getType() {
return this.type;

Check warning on line 36 in src/plugins/data/public/data_sources/datasource/datasource.ts

View check run for this annotation

Codecov / codecov/patch

src/plugins/data/public/data_sources/datasource/datasource.ts#L36

Added line #L36 was not covered by tests
}

getMetadata(): DataSourceMetaData {
return this.metadata;

Check warning on line 40 in src/plugins/data/public/data_sources/datasource/datasource.ts

View check run for this annotation

Codecov / codecov/patch

src/plugins/data/public/data_sources/datasource/datasource.ts#L40

Added line #L40 was not covered by tests
}

/**
* Abstract method to get the dataset associated with the data source.
* Implementing classes need to provide the specific implementation.
*
* Data source selector needs to display data sources with pattern
* group (connection name) - a list of datasets. For example, get
* all available tables for flint datasources, and get all index
* patterns for OpenSearch data source
*
* @returns {SourceDataSet} Dataset associated with the data source.
*/
abstract getDataSet(dataSetParams?: DataSetParams): SourceDataSet;

/**
* Abstract method to run a query against the data source.
* Implementing classes need to provide the specific implementation.
*
* @returns {DataSourceQueryResult} Result from querying the data source.
*/
abstract runQuery(queryParams: DataSourceQueryParams): DataSourceQueryResult;

/**
* Abstract method to test the connection to the data source.
* Implementing classes should provide the specific logic to determine
* the connection status, typically indicating success or failure.
*
* @returns {ConnectionStatus | Promise<void>} Status of the connection test.
*/
abstract testConnection(): ConnectionStatus | Promise<void>;
}
68 changes: 68 additions & 0 deletions src/plugins/data/public/data_sources/datasource/factory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

/**
* The DataSourceFactory is responsible for managing the registration and creation of data source classes.
* It serves as a registry for different data source types and provides a way to instantiate them.
*/

import { DataSourceType } from '../datasource_services';

export class DataSourceFactory {
// Holds the singleton instance of the DataSourceFactory.
private static factory: DataSourceFactory;

// A dictionary holding the data source type as the key and its corresponding class constructor as the value.
private dataSourceClasses: { [type: string]: new (config: any) => DataSourceType } = {};

Check warning on line 18 in src/plugins/data/public/data_sources/datasource/factory.ts

View check run for this annotation

Codecov / codecov/patch

src/plugins/data/public/data_sources/datasource/factory.ts#L18

Added line #L18 was not covered by tests

/**
* Private constructor to ensure only one instance of DataSourceFactory is created.
*/
private constructor() {}

/**
* Returns the singleton instance of the DataSourceFactory. If it doesn't exist, it creates one.
*
* @returns {DataSourceFactory} The single instance of DataSourceFactory.
*/
static getInstance(): DataSourceFactory {
if (!this.factory) {
this.factory = new DataSourceFactory();

Check warning on line 32 in src/plugins/data/public/data_sources/datasource/factory.ts

View check run for this annotation

Codecov / codecov/patch

src/plugins/data/public/data_sources/datasource/factory.ts#L32

Added line #L32 was not covered by tests
}
return this.factory;

Check warning on line 34 in src/plugins/data/public/data_sources/datasource/factory.ts

View check run for this annotation

Codecov / codecov/patch

src/plugins/data/public/data_sources/datasource/factory.ts#L34

Added line #L34 was not covered by tests
}

/**
* Registers a new data source type with its associated class.
* If the type has already been registered, an error is thrown.
*
* @param {string} type - The identifier for the data source type.
* @param {new (config: any) => DataSourceType} dataSourceClass - The constructor of the data source class.
* @throws {Error} Throws an error if the data source type has already been registered.
*/
registerDataSourceType(type: string, dataSourceClass: new (config: any) => DataSourceType): void {
if (this.dataSourceClasses[type]) {
throw new Error('This data source type has already been registered');

Check warning on line 47 in src/plugins/data/public/data_sources/datasource/factory.ts

View check run for this annotation

Codecov / codecov/patch

src/plugins/data/public/data_sources/datasource/factory.ts#L47

Added line #L47 was not covered by tests
}
this.dataSourceClasses[type] = dataSourceClass;

Check warning on line 49 in src/plugins/data/public/data_sources/datasource/factory.ts

View check run for this annotation

Codecov / codecov/patch

src/plugins/data/public/data_sources/datasource/factory.ts#L49

Added line #L49 was not covered by tests
}

/**
* Creates and returns an instance of the specified data source type with the given configuration.
* If the type hasn't been registered, an error is thrown.
*
* @param {string} type - The identifier for the data source type.
* @param {any} config - The configuration for the data source instance.
* @returns {DataSourceType} An instance of the specified data source type.
* @throws {Error} Throws an error if the data source type is not supported.
*/
getDataSourceInstance(type: string, config: any): DataSourceType {
const DataSourceClass = this.dataSourceClasses[type];

Check warning on line 62 in src/plugins/data/public/data_sources/datasource/factory.ts

View check run for this annotation

Codecov / codecov/patch

src/plugins/data/public/data_sources/datasource/factory.ts#L62

Added line #L62 was not covered by tests
if (!DataSourceClass) {
throw new Error('Unsupported data source type');

Check warning on line 64 in src/plugins/data/public/data_sources/datasource/factory.ts

View check run for this annotation

Codecov / codecov/patch

src/plugins/data/public/data_sources/datasource/factory.ts#L64

Added line #L64 was not covered by tests
}
return new DataSourceClass(config);

Check warning on line 66 in src/plugins/data/public/data_sources/datasource/factory.ts

View check run for this annotation

Codecov / codecov/patch

src/plugins/data/public/data_sources/datasource/factory.ts#L66

Added line #L66 was not covered by tests
}
}
15 changes: 15 additions & 0 deletions src/plugins/data/public/data_sources/datasource/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

export { DataSource } from './datasource';
export {
IDataSourceMetaData,
ISourceDataSet,
IDataSetParams,
IDataSourceQueryParams,
IDataSourceQueryResult,
ConnectionStatus,
} from './types';
export { DataSourceFactory } from './factory';
31 changes: 31 additions & 0 deletions src/plugins/data/public/data_sources/datasource/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { IndexPatternsContract } from '../../index_patterns';
import { DataSourceType } from '../datasource_services';

export interface IDataSourceMetaData {
name: string;
}

export interface IDataSourceGroup {
name: string;
}

export interface ISourceDataSet {
ds: DataSourceType;
data_sets: string[] | IndexPatternsContract;
}

export interface IDataSetParams {}

Check failure on line 22 in src/plugins/data/public/data_sources/datasource/types.ts

View workflow job for this annotation

GitHub Actions / Build and Verify on Linux (ciGroup1)

An empty interface is equivalent to `{}`

export interface IDataSourceQueryParams {}

Check failure on line 24 in src/plugins/data/public/data_sources/datasource/types.ts

View workflow job for this annotation

GitHub Actions / Build and Verify on Linux (ciGroup1)

An empty interface is equivalent to `{}`

export interface IDataSourceQueryResult {}

Check failure on line 26 in src/plugins/data/public/data_sources/datasource/types.ts

View workflow job for this annotation

GitHub Actions / Build and Verify on Linux (ciGroup1)

An empty interface is equivalent to `{}`

export interface ConnectionStatus {
success: boolean;
info: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

export const SOURCE_PICKER_TITLE = 'available sources';
export const SOURCE_PICKER_BTN_DEFAULT_TEXT = 'Select a datasource';
export const SOURCE_PICKER_BTN_DEFAULT_WIDTH = '300px';
export const SOURCE_PICKER_PANEL_DEFAULT_WIDTH = '500px';
export const SOURCE_PICKER_PANEL_SEARCH_TEST_SUBJ = 'selectableDatasourcePanelSearch';
export const SOURCE_PICKER_PANEL_TEST_SUBJ = 'selectableDatasourcePanel';
export const SOURCE_PICKER_FOOTER_CANCEL_BTN_TEXT = 'Cancel';
export const SOURCE_PICKER_FOOTER_SELECT_BTN_TEXT = 'Select';
export const SOURCE_PICKER_FOOTER_SELECT_BTN_TEST_SUBJ = 'datasourcePickerSelect';
export const SOURCE_PICKER_BTN_TEST_SUBJ = 'sourcePickerButton';
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import React, { useEffect, useCallback } from 'react';
import { DataSourceSelector } from './datasource_selector';
import { DataSourceType } from '../datasource_services';
import { DataSourceList, DataSourceSelectableProps } from './types';

export const DataSourceSelectable = ({
dataSources,
dataSourceOptionList,
selectedSources,
setSelectedSources,
setDataSourceOptionList,
onFetchDataSetError,
singleSelection = true,
}: DataSourceSelectableProps) => {
const fetchDataSets = useCallback(
() => dataSources.map((ds: DataSourceType) => ds.getDataSet()),

Check warning on line 21 in src/plugins/data/public/data_sources/datasource_selector/datasource_selectable.tsx

View check run for this annotation

Codecov / codecov/patch

src/plugins/data/public/data_sources/datasource_selector/datasource_selectable.tsx#L20-L21

Added lines #L20 - L21 were not covered by tests
[dataSources]
);

const isIndexPatterns = (dataset) => dataset.attributes;

Check warning on line 25 in src/plugins/data/public/data_sources/datasource_selector/datasource_selectable.tsx

View check run for this annotation

Codecov / codecov/patch

src/plugins/data/public/data_sources/datasource_selector/datasource_selectable.tsx#L25

Added line #L25 was not covered by tests

useEffect(() => {
const getSourceOptions = (dataSource: DataSourceType, dataSet) => {

Check warning on line 28 in src/plugins/data/public/data_sources/datasource_selector/datasource_selectable.tsx

View check run for this annotation

Codecov / codecov/patch

src/plugins/data/public/data_sources/datasource_selector/datasource_selectable.tsx#L27-L28

Added lines #L27 - L28 were not covered by tests
if (isIndexPatterns(dataSet)) {
return {

Check warning on line 30 in src/plugins/data/public/data_sources/datasource_selector/datasource_selectable.tsx

View check run for this annotation

Codecov / codecov/patch

src/plugins/data/public/data_sources/datasource_selector/datasource_selectable.tsx#L30

Added line #L30 was not covered by tests
label: dataSet.attributes.title,
value: dataSet.id,
ds: dataSource,
};
}
return { label: dataSet, ds: dataSource };

Check warning on line 36 in src/plugins/data/public/data_sources/datasource_selector/datasource_selectable.tsx

View check run for this annotation

Codecov / codecov/patch

src/plugins/data/public/data_sources/datasource_selector/datasource_selectable.tsx#L36

Added line #L36 was not covered by tests
};

const getSourceList = (allDataSets) => {
const finalList = [] as DataSourceList[];
allDataSets.map((curDataSet) => {
const existingGroup = finalList.find((item) => item.label === curDataSet.ds.getType());

Check warning on line 42 in src/plugins/data/public/data_sources/datasource_selector/datasource_selectable.tsx

View check run for this annotation

Codecov / codecov/patch

src/plugins/data/public/data_sources/datasource_selector/datasource_selectable.tsx#L39-L42

Added lines #L39 - L42 were not covered by tests
// check if add new datasource group or add to existing one
if (existingGroup) {
existingGroup.options = [

Check warning on line 45 in src/plugins/data/public/data_sources/datasource_selector/datasource_selectable.tsx

View check run for this annotation

Codecov / codecov/patch

src/plugins/data/public/data_sources/datasource_selector/datasource_selectable.tsx#L45

Added line #L45 was not covered by tests
...existingGroup.options,
...curDataSet.data_sets?.map((dataSet) => {
return getSourceOptions(curDataSet.ds, dataSet);

Check warning on line 48 in src/plugins/data/public/data_sources/datasource_selector/datasource_selectable.tsx

View check run for this annotation

Codecov / codecov/patch

src/plugins/data/public/data_sources/datasource_selector/datasource_selectable.tsx#L48

Added line #L48 was not covered by tests
}),
];
} else {
finalList.push({

Check warning on line 52 in src/plugins/data/public/data_sources/datasource_selector/datasource_selectable.tsx

View check run for this annotation

Codecov / codecov/patch

src/plugins/data/public/data_sources/datasource_selector/datasource_selectable.tsx#L52

Added line #L52 was not covered by tests
label: curDataSet.ds.getType(),
options: curDataSet.data_sets?.map((dataSet) => {
return getSourceOptions(curDataSet.ds, dataSet);

Check warning on line 55 in src/plugins/data/public/data_sources/datasource_selector/datasource_selectable.tsx

View check run for this annotation

Codecov / codecov/patch

src/plugins/data/public/data_sources/datasource_selector/datasource_selectable.tsx#L55

Added line #L55 was not covered by tests
}),
});
}
});
return finalList;

Check warning on line 60 in src/plugins/data/public/data_sources/datasource_selector/datasource_selectable.tsx

View check run for this annotation

Codecov / codecov/patch

src/plugins/data/public/data_sources/datasource_selector/datasource_selectable.tsx#L60

Added line #L60 was not covered by tests
};

Promise.all(fetchDataSets())

Check warning on line 63 in src/plugins/data/public/data_sources/datasource_selector/datasource_selectable.tsx

View check run for this annotation

Codecov / codecov/patch

src/plugins/data/public/data_sources/datasource_selector/datasource_selectable.tsx#L63

Added line #L63 was not covered by tests
.then((results) => {
setDataSourceOptionList([...getSourceList(results)]);

Check warning on line 65 in src/plugins/data/public/data_sources/datasource_selector/datasource_selectable.tsx

View check run for this annotation

Codecov / codecov/patch

src/plugins/data/public/data_sources/datasource_selector/datasource_selectable.tsx#L65

Added line #L65 was not covered by tests
})
.catch((e) => onFetchDataSetError(e));

Check warning on line 67 in src/plugins/data/public/data_sources/datasource_selector/datasource_selectable.tsx

View check run for this annotation

Codecov / codecov/patch

src/plugins/data/public/data_sources/datasource_selector/datasource_selectable.tsx#L67

Added line #L67 was not covered by tests
}, [dataSources, fetchDataSets, setDataSourceOptionList, onFetchDataSetError]);

const handleSourceChange = (selectedOptions) => {
setSelectedSources(selectedOptions);

Check warning on line 71 in src/plugins/data/public/data_sources/datasource_selector/datasource_selectable.tsx

View check run for this annotation

Codecov / codecov/patch

src/plugins/data/public/data_sources/datasource_selector/datasource_selectable.tsx#L70-L71

Added lines #L70 - L71 were not covered by tests
};

return (

Check warning on line 74 in src/plugins/data/public/data_sources/datasource_selector/datasource_selectable.tsx

View check run for this annotation

Codecov / codecov/patch

src/plugins/data/public/data_sources/datasource_selector/datasource_selectable.tsx#L74

Added line #L74 was not covered by tests
<DataSourceSelector
dataSourceList={dataSourceOptionList}
selectedOptions={selectedSources}
onDataSourceChange={handleSourceChange}
singleSelection={singleSelection}
/>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import { DataSourceSelector } from './datasource_selector';

describe('DataSourceSelector', () => {
const mockOnDataSourceChange = jest.fn();

const sampleDataSources = [
{
label: 'Index patterns',
options: [
{ label: 'sample_log1', value: 'sample_log1' },
{ label: 'sample_log2', value: 'sample_log2' },
],
},
{
label: 'EMR',
options: [{ label: 'EMR_cluster', value: 'EMR_cluster' }],
},
];

const selectedSource = [{ label: 'sample_log1', value: 'sample_log1' }];

it('renders without crashing', () => {
const { getByText } = render(
<DataSourceSelector
dataSourceList={sampleDataSources}
selectedOptions={selectedSource}
onDataSourceChange={mockOnDataSourceChange}
/>
);

expect(getByText('sample_log1')).toBeInTheDocument();
});

it('triggers onDataSourceChange when a data source is selected', () => {
const { getByTestId, getByText } = render(
<DataSourceSelector
dataSourceList={sampleDataSources}
selectedOptions={selectedSource}
onDataSourceChange={mockOnDataSourceChange}
/>
);

fireEvent.click(getByTestId('comboBoxToggleListButton'));
fireEvent.click(getByText('sample_log2'));

expect(mockOnDataSourceChange).toHaveBeenCalledWith([
{ label: 'sample_log2', value: 'sample_log2' },
]);
});

it('has singleSelection set to true by default', () => {
const { rerender } = render(
<DataSourceSelector
dataSourceList={sampleDataSources}
selectedOptions={selectedSource}
onDataSourceChange={mockOnDataSourceChange}
/>
);

let comboBox = document.querySelector('[data-test-subj="comboBoxInput"]');
expect(comboBox).toBeInTheDocument();

rerender(
<DataSourceSelector
dataSourceList={sampleDataSources}
selectedOptions={selectedSource}
onDataSourceChange={mockOnDataSourceChange}
singleSelection={false}
/>
);

comboBox = document.querySelector('[data-test-subj="comboBoxInput"]');
expect(comboBox).toBeInTheDocument();
});

it('renders all data source options', () => {
const { getByText, getByTestId } = render(
<DataSourceSelector
dataSourceList={sampleDataSources}
selectedOptions={selectedSource}
onDataSourceChange={mockOnDataSourceChange}
/>
);

fireEvent.click(getByTestId('comboBoxToggleListButton'));

expect(getByText('Index patterns')).toBeInTheDocument();
expect(getByText('sample_log2')).toBeInTheDocument();
expect(getByText('EMR')).toBeInTheDocument();
expect(getByText('EMR_cluster')).toBeInTheDocument();
});
});
Loading
Loading