From d061adf8d9a90fd8cd17928cace22aa25740233a Mon Sep 17 00:00:00 2001 From: Tyler Ohlsen Date: Thu, 22 Dec 2022 17:07:37 -0800 Subject: [PATCH] Collect VisLayers in VisualizeEmbeddable render flow Signed-off-by: Tyler Ohlsen --- src/plugins/vis_augmenter/common/types.ts | 60 -------------- .../public/expressions/vis_layers.ts | 2 +- src/plugins/vis_augmenter/public/index.ts | 12 ++- .../saved_augment_vis.test.ts | 3 +- .../saved_augment_vis/saved_augment_vis.ts | 2 +- .../saved_augment_vis_references.test.ts | 7 +- .../saved_augment_vis/utils/test_helpers.ts | 2 + .../{common => public}/types.test.ts | 0 src/plugins/vis_augmenter/public/types.ts | 57 ++++++++++++- .../{common => public/utils}/index.ts | 2 +- .../vis_augmenter/public/utils/utils.test.ts | 80 +++++++++++++++++++ .../vis_augmenter/public/utils/utils.ts | 55 +++++++++++++ .../visualizations/opensearch_dashboards.json | 2 +- .../create_vis_embeddable_from_object.ts | 4 + .../public/embeddable/visualize_embeddable.ts | 51 +++++++++++- .../public/legacy/build_pipeline.ts | 3 + src/plugins/visualizations/public/services.ts | 6 ++ 17 files changed, 276 insertions(+), 72 deletions(-) delete mode 100644 src/plugins/vis_augmenter/common/types.ts rename src/plugins/vis_augmenter/{common => public}/types.test.ts (100%) rename src/plugins/vis_augmenter/{common => public/utils}/index.ts (77%) create mode 100644 src/plugins/vis_augmenter/public/utils/utils.test.ts create mode 100644 src/plugins/vis_augmenter/public/utils/utils.ts diff --git a/src/plugins/vis_augmenter/common/types.ts b/src/plugins/vis_augmenter/common/types.ts deleted file mode 100644 index ceb8b1973ba7..000000000000 --- a/src/plugins/vis_augmenter/common/types.ts +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { ExpressionFunctionDefinition } from '../../expressions'; - -export enum VisLayerTypes { - PointInTimeEvents = 'PointInTimeEvents', -} - -export type PluginResourceType = string; - -export interface PluginResource { - type: PluginResourceType; - id: string; - name: string; - urlPath: string; -} - -export interface VisLayer { - type: keyof typeof VisLayerTypes; - originPlugin: string; - pluginResource: PluginResource; -} - -export type VisLayers = VisLayer[]; - -export interface EventMetadata { - pluginResourceId: string; - tooltip?: string; -} - -export interface PointInTimeEvent { - timestamp: number; - metadata: EventMetadata; -} - -export interface PointInTimeEventsVisLayer extends VisLayer { - events: PointInTimeEvent[]; -} - -export const isPointInTimeEventsVisLayer = (obj: any) => { - return obj?.type === VisLayerTypes.PointInTimeEvents; -}; - -export const isValidVisLayer = (obj: any) => { - return obj?.type in VisLayerTypes; -}; - -export interface VisLayerResponseValue { - visLayers: object; -} - -export type VisLayerFunctionDefinition = ExpressionFunctionDefinition< - string, - VisLayerResponseValue, - any, - Promise ->; diff --git a/src/plugins/vis_augmenter/public/expressions/vis_layers.ts b/src/plugins/vis_augmenter/public/expressions/vis_layers.ts index f99ead0407eb..8cfa7d4b4ae8 100644 --- a/src/plugins/vis_augmenter/public/expressions/vis_layers.ts +++ b/src/plugins/vis_augmenter/public/expressions/vis_layers.ts @@ -4,7 +4,7 @@ */ import { ExpressionTypeDefinition } from '../../../expressions'; -import { VisLayers } from '../../common'; +import { VisLayers } from '../'; const name = 'vis_layers'; diff --git a/src/plugins/vis_augmenter/public/index.ts b/src/plugins/vis_augmenter/public/index.ts index cf736bf6d3e6..1e4976d21709 100644 --- a/src/plugins/vis_augmenter/public/index.ts +++ b/src/plugins/vis_augmenter/public/index.ts @@ -18,4 +18,14 @@ export { SavedObjectOpenSearchDashboardsServicesWithAugmentVis, } from './saved_augment_vis'; -export { ISavedAugmentVis, VisLayerExpressionFn, AugmentVisSavedObject } from './types'; +export { + ISavedAugmentVis, + VisLayerExpressionFn, + AugmentVisSavedObject, + VisLayerFunctionDefinition, + VisLayer, + VisLayers, +} from './types'; + +export * from './expressions'; +export * from './utils'; diff --git a/src/plugins/vis_augmenter/public/saved_augment_vis/saved_augment_vis.test.ts b/src/plugins/vis_augmenter/public/saved_augment_vis/saved_augment_vis.test.ts index bc44a0ed6dc2..009dfe5e33c7 100644 --- a/src/plugins/vis_augmenter/public/saved_augment_vis/saved_augment_vis.test.ts +++ b/src/plugins/vis_augmenter/public/saved_augment_vis/saved_augment_vis.test.ts @@ -3,8 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { VisLayerExpressionFn } from '../types'; -import { VisLayerTypes } from '../../common'; +import { VisLayerExpressionFn, VisLayerTypes } from '../types'; import { createSavedAugmentVisLoader, SavedObjectOpenSearchDashboardsServicesWithAugmentVis, diff --git a/src/plugins/vis_augmenter/public/saved_augment_vis/saved_augment_vis.ts b/src/plugins/vis_augmenter/public/saved_augment_vis/saved_augment_vis.ts index 82e6e24a7e3e..910ef0b9ea75 100644 --- a/src/plugins/vis_augmenter/public/saved_augment_vis/saved_augment_vis.ts +++ b/src/plugins/vis_augmenter/public/saved_augment_vis/saved_augment_vis.ts @@ -9,7 +9,7 @@ import { SavedObjectOpenSearchDashboardsServices, } from '../../../saved_objects/public'; import { createSavedAugmentVisClass } from './_saved_augment_vis'; -import { VisLayerTypes } from '../../common'; +import { VisLayerTypes } from '../types'; // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface SavedObjectOpenSearchDashboardsServicesWithAugmentVis diff --git a/src/plugins/vis_augmenter/public/saved_augment_vis/saved_augment_vis_references.test.ts b/src/plugins/vis_augmenter/public/saved_augment_vis/saved_augment_vis_references.test.ts index 4a19b84dc40e..1b5a0ad6cd98 100644 --- a/src/plugins/vis_augmenter/public/saved_augment_vis/saved_augment_vis_references.test.ts +++ b/src/plugins/vis_augmenter/public/saved_augment_vis/saved_augment_vis_references.test.ts @@ -3,9 +3,12 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { extractReferences, injectReferences } from './saved_augment_vis_references'; +import { + extractReferences, + injectReferences, + VIS_REFERENCE_NAME, +} from './saved_augment_vis_references'; import { AugmentVisSavedObject } from '../types'; -import { VIS_REFERENCE_NAME } from './saved_augment_vis_references'; describe('extractReferences()', () => { test('extracts nothing if visId is null', () => { diff --git a/src/plugins/vis_augmenter/public/saved_augment_vis/utils/test_helpers.ts b/src/plugins/vis_augmenter/public/saved_augment_vis/utils/test_helpers.ts index c237fa7551c3..0c9a41062a95 100644 --- a/src/plugins/vis_augmenter/public/saved_augment_vis/utils/test_helpers.ts +++ b/src/plugins/vis_augmenter/public/saved_augment_vis/utils/test_helpers.ts @@ -9,11 +9,13 @@ import { VIS_REFERENCE_NAME } from '../saved_augment_vis_references'; const pluginResourceId = 'test-plugin-resource-id'; const visId = 'test-vis-id'; +const title = 'test-title'; const version = 1; export const generateAugmentVisSavedObject = (idArg: string, exprFnArg: VisLayerExpressionFn) => { return { id: idArg, + title, pluginResourceId, visLayerExpressionFn: exprFnArg, VIS_REFERENCE_NAME, diff --git a/src/plugins/vis_augmenter/common/types.test.ts b/src/plugins/vis_augmenter/public/types.test.ts similarity index 100% rename from src/plugins/vis_augmenter/common/types.test.ts rename to src/plugins/vis_augmenter/public/types.test.ts diff --git a/src/plugins/vis_augmenter/public/types.ts b/src/plugins/vis_augmenter/public/types.ts index 5ddd191cace5..78e812ba53fc 100644 --- a/src/plugins/vis_augmenter/public/types.ts +++ b/src/plugins/vis_augmenter/public/types.ts @@ -4,10 +4,54 @@ */ import { SavedObject } from '../../saved_objects/public'; -import { VisLayerTypes } from '../common'; +import { ExpressionFunctionDefinition } from '../../expressions'; + +export enum VisLayerTypes { + PointInTimeEvents = 'PointInTimeEvents', +} + +export type PluginResourceType = string; + +export interface PluginResource { + type: PluginResourceType; + id: string; + name: string; + urlPath: string; +} + +export interface VisLayer { + type: keyof typeof VisLayerTypes; + originPlugin: string; + pluginResource: PluginResource; +} + +export type VisLayers = VisLayer[]; + +export interface EventMetadata { + pluginResourceId: string; + tooltip?: string; +} + +export interface PointInTimeEvent { + timestamp: number; + metadata: EventMetadata; +} + +export interface PointInTimeEventsVisLayer extends VisLayer { + events: PointInTimeEvent[]; +} + +export const isPointInTimeEventsVisLayer = (obj: any) => { + return obj?.type === VisLayerTypes.PointInTimeEvents; +}; + +export const isValidVisLayer = (obj: any) => { + return obj?.type in VisLayerTypes; +}; export interface ISavedAugmentVis { id?: string; + title: string; description?: string; pluginResourceId: string; visName?: string; @@ -24,3 +68,14 @@ export interface VisLayerExpressionFn { } export interface AugmentVisSavedObject extends SavedObject, ISavedAugmentVis {} + +export interface VisLayerResponseValue { + visLayers: object; +} + +export type VisLayerFunctionDefinition = ExpressionFunctionDefinition< + string, + VisLayerResponseValue, + any, + Promise +>; diff --git a/src/plugins/vis_augmenter/common/index.ts b/src/plugins/vis_augmenter/public/utils/index.ts similarity index 77% rename from src/plugins/vis_augmenter/common/index.ts rename to src/plugins/vis_augmenter/public/utils/index.ts index 9f269633f307..079132ce99d2 100644 --- a/src/plugins/vis_augmenter/common/index.ts +++ b/src/plugins/vis_augmenter/public/utils/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './types'; +export * from './utils'; diff --git a/src/plugins/vis_augmenter/public/utils/utils.test.ts b/src/plugins/vis_augmenter/public/utils/utils.test.ts new file mode 100644 index 000000000000..a00b1be08e86 --- /dev/null +++ b/src/plugins/vis_augmenter/public/utils/utils.test.ts @@ -0,0 +1,80 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Vis } from '../../../visualizations/public'; +import { buildPipelineFromAugmentVisSavedObjs, isEligibleForVisLayers } from './utils'; +import { VisLayerTypes, ISavedAugmentVis } from '../types'; + +describe('utils', () => { + // TODO: redo / update this test suite when eligibility is finalized. + // Tracked in https://github.com/opensearch-project/OpenSearch-Dashboards/issues/3268 + describe('isEligibleForVisLayers', () => { + it('vis is ineligible with invalid type', async () => { + const vis = ({ + params: { + type: 'not-line', + }, + } as unknown) as Vis; + expect(isEligibleForVisLayers(vis)).toEqual(false); + }); + it('vis is eligible with valid type', async () => { + const vis = ({ + params: { + type: 'line', + }, + } as unknown) as Vis; + expect(isEligibleForVisLayers(vis)).toEqual(true); + }); + }); + + describe('getAugmentVisSavedObjs', () => { + // TODO: add tests after saved obj PR tests are added; can use + // them for instantiating mock saved obj loader + }); + + describe('buildPipelineFromAugmentVisSavedObjs', () => { + const obj1 = { + title: 'obj1', + pluginResourceId: 'obj-1-resource-id', + visLayerExpressionFn: { + type: VisLayerTypes.PointInTimeEvents, + name: 'fn-1', + args: { + arg1: 'value-1', + }, + }, + } as ISavedAugmentVis; + const obj2 = { + title: 'obj2', + pluginResourceId: 'obj-2-resource-id', + visLayerExpressionFn: { + type: VisLayerTypes.PointInTimeEvents, + name: 'fn-2', + args: { + arg2: 'value-2', + }, + }, + } as ISavedAugmentVis; + it('catches error with empty array', async () => { + try { + buildPipelineFromAugmentVisSavedObjs([]); + } catch (e: any) { + expect( + e.message.includes( + 'Expression function from augment-vis saved objects could not be generated' + ) + ); + } + }); + it('builds with one saved obj', async () => { + const str = buildPipelineFromAugmentVisSavedObjs([obj1]); + expect(str).toEqual('fn-1 arg1="value-1"'); + }); + it('builds with multiple saved objs', async () => { + const str = buildPipelineFromAugmentVisSavedObjs([obj1, obj2]); + expect(str).toEqual(`fn-1 arg1="value-1"\n| fn-2 arg2="value-2"`); + }); + }); +}); diff --git a/src/plugins/vis_augmenter/public/utils/utils.ts b/src/plugins/vis_augmenter/public/utils/utils.ts new file mode 100644 index 000000000000..df08eef6e38a --- /dev/null +++ b/src/plugins/vis_augmenter/public/utils/utils.ts @@ -0,0 +1,55 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { get } from 'lodash'; +import { Vis } from '../../../visualizations/public'; +import { + formatExpression, + buildExpressionFunction, + buildExpression, + ExpressionAstFunctionBuilder, +} from '../../../../plugins/expressions/public'; +import { ISavedAugmentVis, SavedAugmentVisLoader, VisLayerFunctionDefinition } from '../'; + +// TODO: provide a deeper eligibility check. +// Tracked in https://github.com/opensearch-project/OpenSearch-Dashboards/issues/3268 +export const isEligibleForVisLayers = (vis: Vis): boolean => { + return vis.params.type === 'line'; +}; + +export const getAugmentVisSavedObjs = async ( + visId: string | undefined, + loader: SavedAugmentVisLoader | undefined +): Promise => { + try { + const resp = await loader?.findAll(); + const allSavedObjects = (get(resp, 'hits', []) as any[]) as ISavedAugmentVis[]; + return allSavedObjects.filter((hit: ISavedAugmentVis) => hit.visId === visId); + } catch (e) { + // console.error('Unable to search for augment-vis saved objects: ', e); + return [] as ISavedAugmentVis[]; + } +}; + +export const buildPipelineFromAugmentVisSavedObjs = (objs: ISavedAugmentVis[]): string => { + const visLayerExpressionFns = [] as Array< + ExpressionAstFunctionBuilder + >; + + try { + objs.forEach((obj: ISavedAugmentVis) => { + visLayerExpressionFns.push( + buildExpressionFunction( + obj.visLayerExpressionFn.name, + obj.visLayerExpressionFn.args + ) + ); + }); + const ast = buildExpression(visLayerExpressionFns).toAst(); + return formatExpression(ast); + } catch (e) { + throw new Error('Expression function from augment-vis saved objects could not be generated'); + } +}; diff --git a/src/plugins/visualizations/opensearch_dashboards.json b/src/plugins/visualizations/opensearch_dashboards.json index 6223ffce3808..b7c5e4ab9b4e 100644 --- a/src/plugins/visualizations/opensearch_dashboards.json +++ b/src/plugins/visualizations/opensearch_dashboards.json @@ -5,5 +5,5 @@ "ui": true, "requiredPlugins": ["data", "expressions", "uiActions", "embeddable", "inspector", "dashboard"], "optionalPlugins": ["usageCollection"], - "requiredBundles": ["opensearchDashboardsUtils", "discover", "savedObjects"] + "requiredBundles": ["opensearchDashboardsUtils", "discover", "savedObjects", "visAugmenter"] } diff --git a/src/plugins/visualizations/public/embeddable/create_vis_embeddable_from_object.ts b/src/plugins/visualizations/public/embeddable/create_vis_embeddable_from_object.ts index 03666a199dca..e09f789f9a68 100644 --- a/src/plugins/visualizations/public/embeddable/create_vis_embeddable_from_object.ts +++ b/src/plugins/visualizations/public/embeddable/create_vis_embeddable_from_object.ts @@ -40,6 +40,7 @@ import { IContainer, ErrorEmbeddable } from '../../../embeddable/public'; import { DisabledLabEmbeddable } from './disabled_lab_embeddable'; import { getSavedVisualizationsLoader, + getSavedAugmentVisLoader, getUISettings, getHttp, getTimeFilter, @@ -88,6 +89,8 @@ export const createVisEmbeddableFromObject = (deps: VisualizeEmbeddableFactoryDe const editable = getCapabilities().visualize.save as boolean; + const savedAugmentVisLoader = getSavedAugmentVisLoader(); + return new VisualizeEmbeddable( getTimeFilter(), { @@ -101,6 +104,7 @@ export const createVisEmbeddableFromObject = (deps: VisualizeEmbeddableFactoryDe input, attributeService, savedVisualizationsLoader, + savedAugmentVisLoader, parent ); } catch (e) { diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts index f3808951d519..0caade9bd872 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts @@ -28,7 +28,7 @@ * under the License. */ -import _, { get } from 'lodash'; +import _, { get, isEmpty } from 'lodash'; import { Subscription } from 'rxjs'; import * as Rx from 'rxjs'; import { i18n } from '@osd/i18n'; @@ -64,6 +64,14 @@ import { TriggerId } from '../../../ui_actions/public'; import { SavedObjectAttributes } from '../../../../core/types'; import { AttributeService } from '../../../dashboard/public'; import { SavedVisualizationsLoader } from '../saved_visualizations'; +import { + SavedAugmentVisLoader, + ExprVisLayers, + VisLayers, + isEligibleForVisLayers, + getAugmentVisSavedObjs, + buildPipelineFromAugmentVisSavedObjs, +} from '../../../vis_augmenter/public'; import { VisSavedObject } from '../types'; const getKeys = (o: T): Array => Object.keys(o) as Array; @@ -127,6 +135,7 @@ export class VisualizeEmbeddable VisualizeByReferenceInput >; private savedVisualizationsLoader?: SavedVisualizationsLoader; + private savedAugmentVisLoader?: SavedAugmentVisLoader; constructor( timefilter: TimefilterContract, @@ -138,6 +147,7 @@ export class VisualizeEmbeddable VisualizeByReferenceInput >, savedVisualizationsLoader?: SavedVisualizationsLoader, + savedAugmentVisLoader?: SavedAugmentVisLoader, parent?: IContainer ) { super( @@ -160,7 +170,7 @@ export class VisualizeEmbeddable this.vis.uiState.on('reload', this.reload); this.attributeService = attributeService; this.savedVisualizationsLoader = savedVisualizationsLoader; - + this.savedAugmentVisLoader = savedAugmentVisLoader; this.autoRefreshFetchSubscription = timefilter .getAutoRefreshFetch$() .subscribe(this.updateHandler.bind(this)); @@ -393,10 +403,17 @@ export class VisualizeEmbeddable } this.abortController = new AbortController(); const abortController = this.abortController; + + let exprVisLayers = {} as ExprVisLayers; + if (isEligibleForVisLayers(this.vis)) { + exprVisLayers = await this.fetchVisLayers(expressionParams, abortController); + } + this.expression = await buildPipeline(this.vis, { timefilter: this.timefilter, timeRange: this.timeRange, abortSignal: this.abortController!.signal, + visLayers: !isEmpty(exprVisLayers) ? exprVisLayers.layers : ([] as VisLayers), }); if (this.handler && !abortController.signal.aborted) { @@ -465,4 +482,34 @@ export class VisualizeEmbeddable { showSaveModal: true, saveModalTitle } ); }; + + /** + * Collects any VisLayers from plugin expressions functions + * by fetching all AugmentVisSavedObjects that match the vis ID + */ + fetchVisLayers = async ( + expressionParams: IExpressionLoaderParams, + abortController: AbortController + ): Promise => { + let exprVisLayers = {} as ExprVisLayers; + const augmentVisSavedObjs = await getAugmentVisSavedObjs( + this.vis.id, + this.savedAugmentVisLoader + ); + if (!isEmpty(augmentVisSavedObjs) && !abortController.signal.aborted) { + const visLayersPipeline = buildPipelineFromAugmentVisSavedObjs(augmentVisSavedObjs); + const visLayersPipelineInput = { + type: 'vis_layers', + layers: [] as VisLayers, + }; + // We cannot use this.handler in this case, since it does not support the run() cmd + // we need here. So, we consume the expressions service to run this instead. + exprVisLayers = (await getExpressions().run( + visLayersPipeline, + visLayersPipelineInput, + expressionParams as Record + )) as ExprVisLayers; + } + return exprVisLayers; + }; } diff --git a/src/plugins/visualizations/public/legacy/build_pipeline.ts b/src/plugins/visualizations/public/legacy/build_pipeline.ts index 1cbb3bc38879..bd0e0d97678f 100644 --- a/src/plugins/visualizations/public/legacy/build_pipeline.ts +++ b/src/plugins/visualizations/public/legacy/build_pipeline.ts @@ -33,6 +33,8 @@ import moment from 'moment'; import { formatExpression, SerializedFieldFormat } from '../../../../plugins/expressions/public'; import { IAggConfig, search, TimefilterContract } from '../../../../plugins/data/public'; import { Vis, VisParams } from '../types'; +import { VisLayers } from '../../../vis_augmenter/public'; + const { isDateHistogramBucketAggConfig } = search.aggs; interface SchemaConfigParams { @@ -85,6 +87,7 @@ export interface BuildPipelineParams { timefilter: TimefilterContract; timeRange?: any; abortSignal?: AbortSignal; + visLayers?: VisLayers; } const vislibCharts: string[] = [ diff --git a/src/plugins/visualizations/public/services.ts b/src/plugins/visualizations/public/services.ts index e3f3ba56f6b1..1bb6213640d7 100644 --- a/src/plugins/visualizations/public/services.ts +++ b/src/plugins/visualizations/public/services.ts @@ -52,6 +52,7 @@ import { UiActionsStart } from '../../ui_actions/public'; import { SavedVisualizationsLoader } from './saved_visualizations'; import { SavedObjectLoader } from '../../saved_objects/public'; import { EmbeddableStart } from '../../embeddable/public'; +import { SavedAugmentVisLoader } from '../../vis_augmenter/public'; export const [getUISettings, setUISettings] = createGetterSetter('UISettings'); @@ -106,3 +107,8 @@ export const [getChrome, setChrome] = createGetterSetter('Chrome'); export const [getSavedSearchLoader, setSavedSearchLoader] = createGetterSetter( 'savedSearchLoader' ); + +// TODO: this should resolve after rebasing +export const [getSavedAugmentVisLoader, setSavedAugmentVisLoader] = createGetterSetter< + SavedAugmentVisLoader +>('SavedAugmentVisLoader');