diff --git a/changelogs/fragments/8915.yml b/changelogs/fragments/8915.yml new file mode 100644 index 000000000000..46c124d3f25f --- /dev/null +++ b/changelogs/fragments/8915.yml @@ -0,0 +1,2 @@ +fix: +- Do not support data sources with no version for vis augmenter ([#8915](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8915)) \ No newline at end of file diff --git a/src/plugins/vis_augmenter/public/plugin.ts b/src/plugins/vis_augmenter/public/plugin.ts index 9760bfd75b2d..bd6e45a3967b 100644 --- a/src/plugins/vis_augmenter/public/plugin.ts +++ b/src/plugins/vis_augmenter/public/plugin.ts @@ -13,6 +13,7 @@ import { setUiActions, setEmbeddable, setQueryService, + setIndexPatterns, setVisualizations, setCore, } from './services'; @@ -62,6 +63,7 @@ export class VisAugmenterPlugin setUiActions(uiActions); setEmbeddable(embeddable); setQueryService(data.query); + setIndexPatterns(data.indexPatterns); setVisualizations(visualizations); setCore(core); setFlyoutState(VIEW_EVENTS_FLYOUT_STATE.CLOSED); diff --git a/src/plugins/vis_augmenter/public/services.ts b/src/plugins/vis_augmenter/public/services.ts index 1d7f3e2111db..44a7ea8b424b 100644 --- a/src/plugins/vis_augmenter/public/services.ts +++ b/src/plugins/vis_augmenter/public/services.ts @@ -8,7 +8,7 @@ import { IUiSettingsClient } from '../../../core/public'; import { SavedObjectLoaderAugmentVis } from './saved_augment_vis'; import { EmbeddableStart } from '../../embeddable/public'; import { UiActionsStart } from '../../ui_actions/public'; -import { DataPublicPluginStart } from '../../../plugins/data/public'; +import { DataPublicPluginStart, IndexPatternsContract } from '../../../plugins/data/public'; import { VisualizationsStart } from '../../visualizations/public'; import { CoreStart } from '../../../core/public'; @@ -26,6 +26,10 @@ export const [getQueryService, setQueryService] = createGetterSetter< DataPublicPluginStart['query'] >('Query'); +export const [getIndexPatterns, setIndexPatterns] = createGetterSetter( + 'IndexPatterns' +); + export const [getVisualizations, setVisualizations] = createGetterSetter( 'visualizations' ); diff --git a/src/plugins/vis_augmenter/public/utils/utils.test.ts b/src/plugins/vis_augmenter/public/utils/utils.test.ts index f831deef3955..05f90522fe4a 100644 --- a/src/plugins/vis_augmenter/public/utils/utils.test.ts +++ b/src/plugins/vis_augmenter/public/utils/utils.test.ts @@ -21,11 +21,12 @@ import { PluginResource, VisLayerErrorTypes, SavedObjectLoaderAugmentVis, + isEligibleForDataSource, } from '../'; import { PLUGIN_AUGMENTATION_ENABLE_SETTING } from '../../common/constants'; import { AggConfigs } from '../../../data/common'; import { uiSettingsServiceMock } from '../../../../core/public/mocks'; -import { setUISettings } from '../services'; +import { setIndexPatterns, setUISettings } from '../services'; import { STUB_INDEX_PATTERN_WITH_FIELDS, TYPES_REGISTRY, @@ -35,6 +36,7 @@ import { createPointInTimeEventsVisLayer, createVisLayer, } from '../mocks'; +import { dataPluginMock } from 'src/plugins/data/public/mocks'; describe('utils', () => { const uiSettingsMock = uiSettingsServiceMock.createStartContract(); @@ -60,7 +62,7 @@ describe('utils', () => { aggs: VALID_AGGS, }, } as unknown) as Vis; - expect(isEligibleForVisLayers(vis)).toEqual(false); + expect(await isEligibleForVisLayers(vis)).toEqual(false); }); it('vis is ineligible with no date_histogram', async () => { const invalidConfigStates = [ @@ -87,7 +89,7 @@ describe('utils', () => { invalidAggs, }, } as unknown) as Vis; - expect(isEligibleForVisLayers(vis)).toEqual(false); + expect(await isEligibleForVisLayers(vis)).toEqual(false); }); it('vis is ineligible with invalid aggs counts', async () => { const invalidConfigStates = [ @@ -111,7 +113,7 @@ describe('utils', () => { invalidAggs, }, } as unknown) as Vis; - expect(isEligibleForVisLayers(vis)).toEqual(false); + expect(await isEligibleForVisLayers(vis)).toEqual(false); }); it('vis is ineligible with no metric aggs', async () => { const invalidConfigStates = [ @@ -133,7 +135,7 @@ describe('utils', () => { invalidAggs, }, } as unknown) as Vis; - expect(isEligibleForVisLayers(vis)).toEqual(false); + expect(await isEligibleForVisLayers(vis)).toEqual(false); }); it('vis is ineligible with series param is not line type', async () => { const vis = ({ @@ -154,7 +156,7 @@ describe('utils', () => { aggs: VALID_AGGS, }, } as unknown) as Vis; - expect(isEligibleForVisLayers(vis)).toEqual(false); + expect(await isEligibleForVisLayers(vis)).toEqual(false); }); it('vis is ineligible with series param not all being line type', async () => { const vis = ({ @@ -178,7 +180,7 @@ describe('utils', () => { aggs: VALID_AGGS, }, } as unknown) as Vis; - expect(isEligibleForVisLayers(vis)).toEqual(false); + expect(await isEligibleForVisLayers(vis)).toEqual(false); }); it('vis is ineligible with invalid x-axis due to no segment aggregation', async () => { const badConfigStates = [ @@ -216,7 +218,7 @@ describe('utils', () => { badAggs, }, } as unknown) as Vis; - expect(isEligibleForVisLayers(invalidVis)).toEqual(false); + expect(await isEligibleForVisLayers(invalidVis)).toEqual(false); }); it('vis is ineligible with xaxis not on bottom', async () => { const invalidVis = ({ @@ -237,7 +239,7 @@ describe('utils', () => { aggs: VALID_AGGS, }, } as unknown) as Vis; - expect(isEligibleForVisLayers(invalidVis)).toEqual(false); + expect(await isEligibleForVisLayers(invalidVis)).toEqual(false); }); it('vis is ineligible with no seriesParams', async () => { const invalidVis = ({ @@ -253,16 +255,16 @@ describe('utils', () => { aggs: VALID_AGGS, }, } as unknown) as Vis; - expect(isEligibleForVisLayers(invalidVis)).toEqual(false); + expect(await isEligibleForVisLayers(invalidVis)).toEqual(false); }); it('vis is ineligible with valid type and disabled setting', async () => { uiSettingsMock.get.mockImplementation((key: string) => { return key !== PLUGIN_AUGMENTATION_ENABLE_SETTING; }); - expect(isEligibleForVisLayers(VALID_VIS)).toEqual(false); + expect(await isEligibleForVisLayers(VALID_VIS)).toEqual(false); }); it('vis is eligible with valid type', async () => { - expect(isEligibleForVisLayers(VALID_VIS)).toEqual(true); + expect(await isEligibleForVisLayers(VALID_VIS)).toEqual(true); }); }); @@ -660,4 +662,107 @@ describe('utils', () => { expect(mockDeleteFn).toHaveBeenCalledTimes(1); }); }); + + describe('isEligibleForDataSource', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + it('returns true if the Vis indexPattern does not have a dataSourceRef', async () => { + const indexPatternsMock = dataPluginMock.createStartContract().indexPatterns; + indexPatternsMock.getDataSource = jest.fn().mockReturnValue(undefined); + setIndexPatterns(indexPatternsMock); + const vis = { + data: { + indexPattern: { + id: '123', + }, + }, + } as Vis; + expect(await isEligibleForDataSource(vis)).toEqual(true); + }); + it('returns true if the Vis indexPattern has a dataSourceRef with a compatible version', async () => { + const indexPatternsMock = dataPluginMock.createStartContract().indexPatterns; + indexPatternsMock.getDataSource = jest.fn().mockReturnValue({ + id: '456', + attributes: { + dataSourceVersion: '1.2.3', + }, + }); + setIndexPatterns(indexPatternsMock); + const vis = { + data: { + indexPattern: { + id: '123', + dataSourceRef: { + id: '456', + }, + }, + }, + } as Vis; + expect(await isEligibleForDataSource(vis)).toEqual(true); + }); + it('returns false if the Vis indexPattern has a dataSourceRef with an incompatible version', async () => { + const indexPatternsMock = dataPluginMock.createStartContract().indexPatterns; + indexPatternsMock.getDataSource = jest.fn().mockReturnValue({ + id: '456', + attributes: { + dataSourceVersion: '.0', + }, + }); + setIndexPatterns(indexPatternsMock); + const vis = { + data: { + indexPattern: { + id: '123', + dataSourceRef: { + id: '456', + }, + }, + }, + } as Vis; + expect(await isEligibleForDataSource(vis)).toEqual(false); + }); + it('returns false if the Vis indexPattern has a dataSourceRef with an undefined version', async () => { + const indexPatternsMock = dataPluginMock.createStartContract().indexPatterns; + indexPatternsMock.getDataSource = jest.fn().mockReturnValue({ + id: '456', + attributes: { + dataSourceVersion: undefined, + }, + }); + setIndexPatterns(indexPatternsMock); + const vis = { + data: { + indexPattern: { + id: '123', + dataSourceRef: { + id: '456', + }, + }, + }, + } as Vis; + expect(await isEligibleForDataSource(vis)).toEqual(false); + }); + it('returns false if the Vis indexPattern has a dataSourceRef with an empty string version', async () => { + const indexPatternsMock = dataPluginMock.createStartContract().indexPatterns; + indexPatternsMock.getDataSource = jest.fn().mockReturnValue({ + id: '456', + attributes: { + dataSourceVersion: '', + }, + }); + setIndexPatterns(indexPatternsMock); + const vis = { + data: { + indexPattern: { + id: '123', + dataSourceRef: { + id: '456', + }, + }, + }, + } as Vis; + expect(await isEligibleForDataSource(vis)).toEqual(false); + }); + }); }); diff --git a/src/plugins/vis_augmenter/public/utils/utils.ts b/src/plugins/vis_augmenter/public/utils/utils.ts index ce44964e6173..0ae3c9ec93aa 100644 --- a/src/plugins/vis_augmenter/public/utils/utils.ts +++ b/src/plugins/vis_augmenter/public/utils/utils.ts @@ -4,6 +4,7 @@ */ import { get, isEmpty } from 'lodash'; +import semver from 'semver'; import { Vis } from '../../../../plugins/visualizations/public'; import { formatExpression, @@ -20,10 +21,13 @@ import { VisLayerErrorTypes, } from '../'; import { PLUGIN_AUGMENTATION_ENABLE_SETTING } from '../../common/constants'; -import { getUISettings } from '../services'; +import { getUISettings, getIndexPatterns } from '../services'; import { IUiSettingsClient } from '../../../../core/public'; -export const isEligibleForVisLayers = (vis: Vis, uiSettingsClient?: IUiSettingsClient): boolean => { +export const isEligibleForVisLayers = async ( + vis: Vis, + uiSettingsClient?: IUiSettingsClient +): Promise => { // Only support a date histogram const dateHistograms = vis.data?.aggs?.byTypeName?.('date_histogram'); if (!Array.isArray(dateHistograms) || dateHistograms.length !== 1) return false; @@ -53,6 +57,9 @@ export const isEligibleForVisLayers = (vis: Vis, uiSettingsClient?: IUiSettingsC ) return false; + // Check if the vis datasource is eligible for the augmentation + if (!(await isEligibleForDataSource(vis))) return false; + // Checks if the augmentation setting is enabled const config = uiSettingsClient ?? getUISettings(); return config.get(PLUGIN_AUGMENTATION_ENABLE_SETTING); @@ -163,7 +170,6 @@ export const getAnyErrors = (visLayers: VisLayer[], visTitle: string): Error | u * @param visLayers the produced VisLayers containing details if the resource has been deleted * @param visualizationsLoader the visualizations saved object loader to handle deletion */ - export const cleanupStaleObjects = ( augmentVisSavedObjs: ISavedAugmentVis[], visLayers: VisLayer[], @@ -187,3 +193,17 @@ export const cleanupStaleObjects = ( loader?.delete(objIdsToDelete); } }; + +/** + * Returns true if the Vis is eligible to be used with the DataSource feature. + * @param vis - The Vis to check + * @returns true if the Vis is eligible for the DataSource feature, false otherwise + */ +export const isEligibleForDataSource = async (vis: Vis) => { + const dataSourceRef = vis.data.indexPattern?.dataSourceRef; + if (!dataSourceRef) return true; + const dataSource = await getIndexPatterns().getDataSource(dataSourceRef.id); + if (!dataSource || !dataSource.attributes) return false; + const version = semver.coerce(dataSource.attributes.dataSourceVersion); + return version ? semver.satisfies(version, '>=1.0.0') : false; +}; diff --git a/src/plugins/vis_augmenter/public/view_events_flyout/actions/view_events_option_action.tsx b/src/plugins/vis_augmenter/public/view_events_flyout/actions/view_events_option_action.tsx index ac7f795c586e..f83f0e0b77d6 100644 --- a/src/plugins/vis_augmenter/public/view_events_flyout/actions/view_events_option_action.tsx +++ b/src/plugins/vis_augmenter/public/view_events_flyout/actions/view_events_option_action.tsx @@ -46,7 +46,7 @@ export class ViewEventsOptionAction implements Action { const vis = (embeddable as VisualizeEmbeddable).vis; return ( vis !== undefined && - isEligibleForVisLayers(vis) && + (await isEligibleForVisLayers(vis)) && !isEmpty((embeddable as VisualizeEmbeddable).visLayers) ); } diff --git a/src/plugins/vis_type_vislib/public/line_to_expression.ts b/src/plugins/vis_type_vislib/public/line_to_expression.ts index 8650c6013801..e8d207017c00 100644 --- a/src/plugins/vis_type_vislib/public/line_to_expression.ts +++ b/src/plugins/vis_type_vislib/public/line_to_expression.ts @@ -32,7 +32,7 @@ export const toExpressionAst = async (vis: Vis, params: any) => { if ( params.visLayers == null || Object.keys(params.visLayers).length === 0 || - !isEligibleForVisLayers(vis) + !(await isEligibleForVisLayers(vis)) ) { // Render using vislib instead of vega-lite const visConfig = { ...vis.params, dimensions }; diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts index 605c88067211..7bf996c148ea 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts @@ -541,7 +541,7 @@ export class VisualizeEmbeddable this.visAugmenterConfig?.visLayerResourceIds ); - if (!isEmpty(augmentVisSavedObjs) && !aborted && isEligibleForVisLayers(this.vis)) { + if (!isEmpty(augmentVisSavedObjs) && !aborted && (await isEligibleForVisLayers(this.vis))) { const visLayersPipeline = buildPipelineFromAugmentVisSavedObjs(augmentVisSavedObjs); // The initial input for the pipeline will just be an empty arr of VisLayers. As plugin // expression functions are ran, they will incrementally append their generated VisLayers to it.