-
Notifications
You must be signed in to change notification settings - Fork 867
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
[Backport 2.x] [MDS] Support for Timeline #6493
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,152 @@ | ||
/* | ||
* Copyright OpenSearch Contributors | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
import { savedObjectsClientMock } from '../../../../core/server/mocks'; | ||
import { fetchDataSourceIdByName } from './fetch_data_source_id'; | ||
import { OpenSearchFunctionConfig } from '../types'; | ||
|
||
jest.mock('./services', () => ({ | ||
getDataSourceEnabled: jest | ||
.fn() | ||
.mockReturnValueOnce({ enabled: false }) | ||
.mockReturnValue({ enabled: true }), | ||
})); | ||
|
||
describe('fetchDataSourceIdByName()', () => { | ||
const validId = 'some-valid-id'; | ||
const config: OpenSearchFunctionConfig = { | ||
q: null, | ||
metric: null, | ||
split: null, | ||
index: null, | ||
timefield: null, | ||
kibana: null, | ||
opensearchDashboards: null, | ||
interval: null, | ||
}; | ||
const client = savedObjectsClientMock.create(); | ||
client.find = jest.fn().mockImplementation((props) => { | ||
if (props.search === '"No Results With Filter"') { | ||
return Promise.resolve({ | ||
saved_objects: [ | ||
{ | ||
id: 'some-non-matching-id', | ||
attributes: { | ||
title: 'No Results With Filter Some Suffix', | ||
}, | ||
}, | ||
], | ||
}); | ||
} | ||
if (props.search === '"Duplicate Title"') { | ||
return Promise.resolve({ | ||
saved_objects: [ | ||
{ | ||
id: 'duplicate-id-1', | ||
attributes: { | ||
title: 'Duplicate Title', | ||
}, | ||
}, | ||
{ | ||
id: 'duplicate-id-2', | ||
attributes: { | ||
title: 'Duplicate Title', | ||
}, | ||
}, | ||
], | ||
}); | ||
} | ||
if (props.search === '"Some Data Source"') { | ||
return Promise.resolve({ | ||
saved_objects: [ | ||
{ | ||
id: validId, | ||
attributes: { | ||
title: 'Some Data Source', | ||
}, | ||
}, | ||
], | ||
}); | ||
} | ||
if (props.search === '"Some Prefix"') { | ||
return Promise.resolve({ | ||
saved_objects: [ | ||
{ | ||
id: 'some-id-2', | ||
attributes: { | ||
title: 'Some Prefix B', | ||
}, | ||
}, | ||
{ | ||
id: validId, | ||
attributes: { | ||
title: 'Some Prefix', | ||
}, | ||
}, | ||
], | ||
}); | ||
} | ||
|
||
return Promise.resolve({ saved_objects: [] }); | ||
}); | ||
|
||
it('should return undefined if data_source_name is not present', async () => { | ||
expect(await fetchDataSourceIdByName(config, client)).toBe(undefined); | ||
}); | ||
|
||
it('should return undefined if data_source_name is an empty string', async () => { | ||
expect(await fetchDataSourceIdByName({ ...config, data_source_name: '' }, client)).toBe( | ||
undefined | ||
); | ||
}); | ||
|
||
it('should throw errors when MDS is disabled', async () => { | ||
await expect( | ||
fetchDataSourceIdByName({ ...config, data_source_name: 'Some Data Source' }, client) | ||
).rejects.toThrowError( | ||
'To query from multiple data sources, first enable the data source feature' | ||
); | ||
}); | ||
|
||
it.each([ | ||
{ | ||
dataSourceName: 'Non-existent Data Source', | ||
expectedResultCount: 0, | ||
}, | ||
{ | ||
dataSourceName: 'No Results With Filter', | ||
expectedResultCount: 0, | ||
}, | ||
{ | ||
dataSourceName: 'Duplicate Title', | ||
expectedResultCount: 2, | ||
}, | ||
])( | ||
'should throw errors when non-existent or duplicate data_source_name is provided', | ||
async ({ dataSourceName, expectedResultCount }) => { | ||
await expect( | ||
fetchDataSourceIdByName({ ...config, data_source_name: dataSourceName }, client) | ||
).rejects.toThrowError( | ||
`Expected exactly 1 result for data_source_name "${dataSourceName}" but got ${expectedResultCount} results` | ||
); | ||
} | ||
); | ||
|
||
it.each([ | ||
{ | ||
dataSourceName: 'Some Data Source', | ||
}, | ||
{ | ||
dataSourceName: 'Some Prefix', | ||
}, | ||
])( | ||
'should return valid id when data_source_name exists and is unique', | ||
async ({ dataSourceName }) => { | ||
expect( | ||
await fetchDataSourceIdByName({ ...config, data_source_name: dataSourceName }, client) | ||
).toBe(validId); | ||
} | ||
); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
/* | ||
* Copyright OpenSearch Contributors | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
import { SavedObjectsClientContract } from 'src/core/server'; | ||
import { DataSourceAttributes } from 'src/plugins/data_source/common/data_sources'; | ||
import { getDataSourceEnabled } from './services'; | ||
import { OpenSearchFunctionConfig } from '../types'; | ||
|
||
export const fetchDataSourceIdByName = async ( | ||
config: OpenSearchFunctionConfig, | ||
client: SavedObjectsClientContract | ||
) => { | ||
if (!config.data_source_name) { | ||
return undefined; | ||
} | ||
|
||
if (!getDataSourceEnabled().enabled) { | ||
throw new Error('To query from multiple data sources, first enable the data source feature'); | ||
} | ||
|
||
const dataSources = await client.find<DataSourceAttributes>({ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think we should need to query the cluster every time to get the source. If I put an auto refresh interval then it seems like this will hit the cluster twice per request. I think we should caching these data sources like index patterns. If the data source exists in that cache then we can just pull that, if not then the service will pull the data sources. |
||
type: 'data-source', | ||
perPage: 100, | ||
search: `"${config.data_source_name}"`, | ||
searchFields: ['title'], | ||
fields: ['id', 'title'], | ||
}); | ||
|
||
const possibleDataSourceIds = dataSources.saved_objects.filter( | ||
(obj) => obj.attributes.title === config.data_source_name | ||
); | ||
|
||
if (possibleDataSourceIds.length !== 1) { | ||
throw new Error( | ||
`Expected exactly 1 result for data_source_name "${config.data_source_name}" but got ${possibleDataSourceIds.length} results` | ||
); | ||
} | ||
|
||
return possibleDataSourceIds.pop()?.id; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
/* | ||
* Copyright OpenSearch Contributors | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
import { createGetterSetter } from '../../../opensearch_dashboards_utils/common'; | ||
|
||
export const [getDataSourceEnabled, setDataSourceEnabled] = createGetterSetter<{ | ||
enabled: boolean; | ||
}>('DataSource'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can imagine a world where saved objects were imported from a cluster that enabled MDS to a new domain that didn't enable it. If the timeline vis was added to a dashboard now it will fail.
Do we want to consider that if it's not enabled it just queries to the local cluster?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Existing behavior with Timeline is that any invalid params are caught and a toast error is displayed, meaning before this merge, if a user types in
data_source_name
, a toast would be thrown anyway because this was not a named argument. This means that any syntax error or invalid param can and should be caught. If the visualization just queries a local cluster, then it can be confusing behavior especially since this change enables Timeline to query both local cluster and MDS queries within one visualization.