diff --git a/src/plugins/vis_augmenter/common/augment_vis_saved_object_attributes.ts b/src/plugins/vis_augmenter/common/augment_vis_saved_object_attributes.ts new file mode 100644 index 000000000000..824f35c6e112 --- /dev/null +++ b/src/plugins/vis_augmenter/common/augment_vis_saved_object_attributes.ts @@ -0,0 +1,32 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { SavedObjectAttributes } from 'opensearch-dashboards/server'; + +export interface AugmentVisSavedObjectAttributes extends SavedObjectAttributes { + id: string; + title: string; + description?: string; + originPlugin: string; + pluginResource: { + type: string; + id: string; + }; + visLayerExpressionFn: { + type: string; + name: string; + }; + version: number; + // Following fields are optional since they will get set/removed during the extraction/injection + // of the vis reference + visName?: string; + visId?: string; + visReference?: { + id: string; + name: string; + }; + // Error may be populated if there is some issue when parsing the attribute values + error?: string; +} diff --git a/src/plugins/vis_augmenter/common/constants.ts b/src/plugins/vis_augmenter/common/constants.ts new file mode 100644 index 000000000000..7ce517aba77e --- /dev/null +++ b/src/plugins/vis_augmenter/common/constants.ts @@ -0,0 +1,12 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export const APP_PATH = { + STATS: '/stats', +}; +export const APP_API = '/api/vis_augmenter'; + +// used for limiting results received from the stats API +export const PER_PAGE_REQUEST_NUMBER = 50; diff --git a/src/plugins/vis_augmenter/common/index.ts b/src/plugins/vis_augmenter/common/index.ts new file mode 100644 index 000000000000..9762ce68770e --- /dev/null +++ b/src/plugins/vis_augmenter/common/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './constants'; +export { AugmentVisSavedObjectAttributes } from './augment_vis_saved_object_attributes'; 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 ffaa64e92304..88178a7a08cd 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 @@ -14,8 +14,8 @@ import { SavedObject, SavedObjectOpenSearchDashboardsServices, } from '../../../saved_objects/public'; -import { IIndexPattern } from '../../../data/public'; import { extractReferences, injectReferences } from './saved_augment_vis_references'; +import { AugmentVisSavedObjectAttributes } from '../../common'; const name = 'augment-vis'; @@ -24,28 +24,19 @@ export function createSavedAugmentVisClass(services: SavedObjectOpenSearchDashbo class SavedAugmentVis extends SavedObjectClass { public static type: string = name; - public static mapping: Record = { - description: 'text', - pluginResourceId: 'text', - visId: 'keyword', - visLayerExpressionFn: 'text', - version: 'integer', - }; + public static mapping: AugmentVisSavedObjectAttributes; - constructor(opts: Record | string = {}) { - if (typeof opts !== 'object') { - opts = { id: opts }; - } + constructor(opts: AugmentVisSavedObjectAttributes) { super({ type: SavedAugmentVis.type, mapping: SavedAugmentVis.mapping, extractReferences, injectReferences, id: (opts.id as string) || '', - indexPattern: opts.indexPattern as IIndexPattern, defaults: { description: get(opts, 'description', ''), - pluginResourceId: get(opts, 'pluginResourceId', ''), + originPlugin: get(opts, 'originPlugin', ''), + pluginResource: get(opts, 'pluginResource', {}), visId: get(opts, 'visId', ''), visLayerExpressionFn: get(opts, 'visLayerExpressionFn', {}), version: 1, @@ -55,5 +46,5 @@ export function createSavedAugmentVisClass(services: SavedObjectOpenSearchDashbo } } - return SavedAugmentVis as new (opts: Record | string) => SavedObject; + return SavedAugmentVis as new (opts: AugmentVisSavedObjectAttributes) => SavedObject; } 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 51360f72c331..7e2aabe25d69 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,12 +3,14 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { VisLayerExpressionFn, VisLayerTypes } from '../types'; +import { VisLayerTypes } from '../types'; +import { VisLayerExpressionFn } from '../expressions'; import { createSavedAugmentVisLoader, SavedObjectOpenSearchDashboardsServicesWithAugmentVis, } from './saved_augment_vis'; import { generateAugmentVisSavedObject, getMockAugmentVisSavedObjectClient } from './utils'; +import { ISavedPluginResource } from './types'; describe('SavedObjectLoaderAugmentVis', () => { const fn = { @@ -18,8 +20,25 @@ describe('SavedObjectLoaderAugmentVis', () => { testArg: 'test-value', }, } as VisLayerExpressionFn; - const validObj1 = generateAugmentVisSavedObject('valid-obj-id-1', fn, 'test-vis-id'); - const validObj2 = generateAugmentVisSavedObject('valid-obj-id-2', fn, 'test-vis-id'); + const originPlugin = 'test-plugin'; + const pluginResource = { + type: 'test-plugin', + id: 'test-plugin-resource-id', + }; + const validObj1 = generateAugmentVisSavedObject( + 'valid-obj-id-1', + fn, + 'test-vis-id', + originPlugin, + pluginResource + ); + const validObj2 = generateAugmentVisSavedObject( + 'valid-obj-id-2', + fn, + 'test-vis-id', + originPlugin, + pluginResource + ); const invalidFnTypeObj = generateAugmentVisSavedObject( 'invalid-fn-obj-id-1', { @@ -27,13 +46,48 @@ describe('SavedObjectLoaderAugmentVis', () => { // @ts-ignore type: 'invalid-type', }, - 'test-vis-id' + 'test-vis-id', + originPlugin, + pluginResource ); const missingFnObj = generateAugmentVisSavedObject( 'missing-fn-obj-id-1', {} as VisLayerExpressionFn, - 'test-vis-id' + 'test-vis-id', + originPlugin, + pluginResource + ); + + const missingOriginPluginObj = generateAugmentVisSavedObject( + 'missing-origin-plugin-obj-id-1', + fn, + 'test-vis-id', + // @ts-ignore + undefined, + pluginResource + ); + + const missingPluginResourceTypeObj = generateAugmentVisSavedObject( + 'missing-plugin-resource-type-obj-id-1', + fn, + 'test-vis-id', + // @ts-ignore + originPlugin, + { + id: pluginResource.id, + } as ISavedPluginResource + ); + + const missingPluginResourceIdObj = generateAugmentVisSavedObject( + 'missing-plugin-resource-id-obj-id-1', + fn, + 'test-vis-id', + // @ts-ignore + originPlugin, + { + type: pluginResource.type, + } as ISavedPluginResource ); it('find returns single saved obj', async () => { @@ -105,4 +159,36 @@ describe('SavedObjectLoaderAugmentVis', () => { expect(resp.hits[0].id).toEqual('valid-obj-id-1'); expect(resp.hits[0].error).toEqual('visReference is missing in augment-vis saved object'); }); + + it('findAll returns obj with missing originPlugin', async () => { + const loader = createSavedAugmentVisLoader({ + savedObjectsClient: getMockAugmentVisSavedObjectClient([missingOriginPluginObj]), + } as SavedObjectOpenSearchDashboardsServicesWithAugmentVis); + const resp = await loader.findAll(); + expect(resp.hits.length).toEqual(1); + expect(resp.hits[0].id).toEqual('missing-origin-plugin-obj-id-1'); + expect(resp.hits[0].error).toEqual('originPlugin is missing in augment-vis saved object'); + }); + + it('findAll returns obj with missing plugin resource type', async () => { + const loader = createSavedAugmentVisLoader({ + savedObjectsClient: getMockAugmentVisSavedObjectClient([missingPluginResourceTypeObj]), + } as SavedObjectOpenSearchDashboardsServicesWithAugmentVis); + const resp = await loader.findAll(); + expect(resp.hits.length).toEqual(1); + expect(resp.hits[0].id).toEqual('missing-plugin-resource-type-obj-id-1'); + expect(resp.hits[0].error).toEqual( + 'pluginResource.type is missing in augment-vis saved object' + ); + }); + + it('findAll returns obj with missing plugin resource id', async () => { + const loader = createSavedAugmentVisLoader({ + savedObjectsClient: getMockAugmentVisSavedObjectClient([missingPluginResourceIdObj]), + } as SavedObjectOpenSearchDashboardsServicesWithAugmentVis); + const resp = await loader.findAll(); + expect(resp.hits.length).toEqual(1); + expect(resp.hits[0].id).toEqual('missing-plugin-resource-id-obj-id-1'); + expect(resp.hits[0].error).toEqual('pluginResource.id is missing in augment-vis saved object'); + }); }); 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 910ef0b9ea75..5c76b3cec421 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 @@ -10,6 +10,7 @@ import { } from '../../../saved_objects/public'; import { createSavedAugmentVisClass } from './_saved_augment_vis'; import { VisLayerTypes } from '../types'; +import { AugmentVisSavedObjectAttributes } from '../../common'; // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface SavedObjectOpenSearchDashboardsServicesWithAugmentVis @@ -21,9 +22,9 @@ export function createSavedAugmentVisLoader( const { savedObjectsClient } = services; class SavedObjectLoaderAugmentVis extends SavedObjectLoader { - mapHitSource = (source: Record, id: string) => { + mapHitSource = (source: AugmentVisSavedObjectAttributes, id: string) => { source.id = id; - source.visId = get(source, 'visReference.id', ''); + source.visId = get(source, 'visReference.id', '') as string; if (isEmpty(source.visReference)) { source.error = 'visReference is missing in augment-vis saved object'; @@ -33,10 +34,22 @@ export function createSavedAugmentVisLoader( source.error = 'visLayerExpressionFn is missing in augment-vis saved object'; return source; } - if (!(get(source, 'visLayerExpressionFn.type', '') in VisLayerTypes)) { + if (!((get(source, 'visLayerExpressionFn.type', '') as string) in VisLayerTypes)) { source.error = 'Unknown VisLayer expression function type'; return source; } + if (get(source, 'originPlugin', undefined) === undefined) { + source.error = 'originPlugin is missing in augment-vis saved object'; + return source; + } + if (get(source, 'pluginResource.type', undefined) === undefined) { + source.error = 'pluginResource.type is missing in augment-vis saved object'; + return source; + } + if (get(source, 'pluginResource.id', undefined) === undefined) { + source.error = 'pluginResource.id is missing in augment-vis saved object'; + return source; + } return source; }; @@ -48,7 +61,7 @@ export function createSavedAugmentVisLoader( */ mapSavedObjectApiHits(hit: { references: any[]; - attributes: Record; + attributes: AugmentVisSavedObjectAttributes; id: string; }) { // For now we are assuming only one vis reference per saved object. 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 1b5a0ad6cd98..dd7aef79d9ad 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 @@ -8,7 +8,7 @@ import { injectReferences, VIS_REFERENCE_NAME, } from './saved_augment_vis_references'; -import { AugmentVisSavedObject } from '../types'; +import { AugmentVisSavedObject } from './types'; describe('extractReferences()', () => { test('extracts nothing if visId is null', () => { diff --git a/src/plugins/vis_augmenter/public/saved_augment_vis/saved_augment_vis_references.ts b/src/plugins/vis_augmenter/public/saved_augment_vis/saved_augment_vis_references.ts index 5b2cc3f3d0e5..7a915f93745e 100644 --- a/src/plugins/vis_augmenter/public/saved_augment_vis/saved_augment_vis_references.ts +++ b/src/plugins/vis_augmenter/public/saved_augment_vis/saved_augment_vis_references.ts @@ -4,7 +4,8 @@ */ import { SavedObjectAttributes, SavedObjectReference } from '../../../../core/public'; -import { AugmentVisSavedObject } from '../types'; +import { AugmentVisSavedObjectAttributes } from '../../common'; +import { AugmentVisSavedObject } from './types'; /** * Note that references aren't stored in the object's client-side interface (AugmentVisSavedObject). @@ -35,7 +36,7 @@ export function extractReferences({ attributes: SavedObjectAttributes; references: SavedObjectReference[]; }) { - const updatedAttributes = { ...attributes }; + const updatedAttributes = { ...attributes } as AugmentVisSavedObjectAttributes; const updatedReferences = [...references]; // Extract saved object diff --git a/src/plugins/vis_augmenter/public/saved_augment_vis/types.ts b/src/plugins/vis_augmenter/public/saved_augment_vis/types.ts index dee349cb9001..c2e5b8b19008 100644 --- a/src/plugins/vis_augmenter/public/saved_augment_vis/types.ts +++ b/src/plugins/vis_augmenter/public/saved_augment_vis/types.ts @@ -6,11 +6,17 @@ import { SavedObject } from '../../../saved_objects/public'; import { VisLayerExpressionFn } from '../expressions'; +export interface ISavedPluginResource { + type: string; + id: string; +} + export interface ISavedAugmentVis { id?: string; title: string; description?: string; - pluginResourceId: string; + originPlugin: string; + pluginResource: ISavedPluginResource; visName?: string; visId?: string; visLayerExpressionFn: VisLayerExpressionFn; diff --git a/src/plugins/vis_augmenter/public/saved_augment_vis/utils/helpers.ts b/src/plugins/vis_augmenter/public/saved_augment_vis/utils/helpers.ts index c3a54a377317..6c984e16d4d7 100644 --- a/src/plugins/vis_augmenter/public/saved_augment_vis/utils/helpers.ts +++ b/src/plugins/vis_augmenter/public/saved_augment_vis/utils/helpers.ts @@ -4,7 +4,7 @@ */ import { getSavedAugmentVisLoader } from '../../services'; -import { ISavedAugmentVis } from '../../types'; +import { ISavedAugmentVis } from '../types'; /** * Create an augment vis saved object given an object that 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 38e80646ed15..c162fd3e0a6c 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 @@ -4,22 +4,24 @@ */ import { cloneDeep } from 'lodash'; -import { VisLayerExpressionFn, ISavedAugmentVis } from '../../'; +import { VisLayerExpressionFn, ISavedAugmentVis, ISavedPluginResource } from '../../'; import { VIS_REFERENCE_NAME } from '../saved_augment_vis_references'; -const pluginResourceId = 'test-plugin-resource-id'; const title = 'test-title'; const version = 1; export const generateAugmentVisSavedObject = ( idArg: string, exprFnArg: VisLayerExpressionFn, - visIdArg: string + visIdArg: string, + originPluginArg: string, + pluginResourceArg: ISavedPluginResource ) => { return { id: idArg, title, - pluginResourceId, + originPlugin: originPluginArg, + pluginResource: pluginResourceArg, visLayerExpressionFn: exprFnArg, VIS_REFERENCE_NAME, visId: visIdArg, diff --git a/src/plugins/vis_augmenter/public/utils/utils.test.ts b/src/plugins/vis_augmenter/public/utils/utils.test.ts index c37e87194c97..6e55fd8a11fd 100644 --- a/src/plugins/vis_augmenter/public/utils/utils.test.ts +++ b/src/plugins/vis_augmenter/public/utils/utils.test.ts @@ -292,12 +292,35 @@ describe('utils', () => { testArg: 'test-value', }, } as VisLayerExpressionFn; + const originPlugin = 'test-plugin'; + const pluginResource = { + type: 'test-plugin', + id: 'test-plugin-resource-id', + }; const visId1 = 'test-vis-id-1'; const visId2 = 'test-vis-id-2'; const visId3 = 'test-vis-id-3'; - const obj1 = generateAugmentVisSavedObject('valid-obj-id-1', fn, visId1); - const obj2 = generateAugmentVisSavedObject('valid-obj-id-2', fn, visId1); - const obj3 = generateAugmentVisSavedObject('valid-obj-id-3', fn, visId2); + const obj1 = generateAugmentVisSavedObject( + 'valid-obj-id-1', + fn, + visId1, + originPlugin, + pluginResource + ); + const obj2 = generateAugmentVisSavedObject( + 'valid-obj-id-2', + fn, + visId1, + originPlugin, + pluginResource + ); + const obj3 = generateAugmentVisSavedObject( + 'valid-obj-id-3', + fn, + visId2, + originPlugin, + pluginResource + ); it('returns no matching saved objs with filtering', async () => { const loader = createSavedAugmentVisLoader({ @@ -334,7 +357,11 @@ describe('utils', () => { describe('buildPipelineFromAugmentVisSavedObjs', () => { const obj1 = { title: 'obj1', - pluginResourceId: 'obj-1-resource-id', + originPlugin: 'test-plugin', + pluginResource: { + type: 'test-resource-type', + id: 'obj-1-resource-id', + }, visLayerExpressionFn: { type: VisLayerTypes.PointInTimeEvents, name: 'fn-1', @@ -345,7 +372,11 @@ describe('utils', () => { } as ISavedAugmentVis; const obj2 = { title: 'obj2', - pluginResourceId: 'obj-2-resource-id', + originPlugin: 'test-plugin', + pluginResource: { + type: 'test-resource-type', + id: 'obj-2-resource-id', + }, visLayerExpressionFn: { type: VisLayerTypes.PointInTimeEvents, name: 'fn-2', diff --git a/src/plugins/vis_augmenter/server/plugin.ts b/src/plugins/vis_augmenter/server/plugin.ts index f30cf6c974fe..f6b41646c536 100644 --- a/src/plugins/vis_augmenter/server/plugin.ts +++ b/src/plugins/vis_augmenter/server/plugin.ts @@ -12,6 +12,7 @@ import { } from '../../../core/server'; import { augmentVisSavedObjectType } from './saved_objects'; import { capabilitiesProvider } from './capabilities_provider'; +import { registerStatsRoute } from './routes/stats'; // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface VisAugmenterPluginSetup {} @@ -30,6 +31,11 @@ export class VisAugmenterPlugin this.logger.debug('VisAugmenter: Setup'); core.savedObjects.registerType(augmentVisSavedObjectType); core.capabilities.registerProvider(capabilitiesProvider); + + // Register server-side APIs + const router = core.http.createRouter(); + registerStatsRoute(router, this.logger); + return {}; } diff --git a/src/plugins/vis_augmenter/server/routes/stats.ts b/src/plugins/vis_augmenter/server/routes/stats.ts new file mode 100644 index 000000000000..e8c4ae76bf24 --- /dev/null +++ b/src/plugins/vis_augmenter/server/routes/stats.ts @@ -0,0 +1,56 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Logger } from '@osd/logging'; +import { ResponseError } from '@opensearch-project/opensearch/lib/errors'; +import { + IOpenSearchDashboardsResponse, + IRouter, + SavedObjectsFindResponse, +} from '../../../../core/server'; +import { + APP_API, + APP_PATH, + PER_PAGE_REQUEST_NUMBER, + AugmentVisSavedObjectAttributes, +} from '../../common'; +import { getAugmentVisSavedObjects, getStats } from './stats_helpers'; + +export const registerStatsRoute = (router: IRouter, logger: Logger) => { + router.get( + { + path: `${APP_API}${APP_PATH.STATS}`, + validate: {}, + }, + async ( + context, + request, + response + ): Promise> => { + try { + const savedObjectsClient = context.core.savedObjects.client; + const augmentVisSavedObjects: SavedObjectsFindResponse = await getAugmentVisSavedObjects( + savedObjectsClient, + PER_PAGE_REQUEST_NUMBER + ); + const stats = getStats(augmentVisSavedObjects); + return response.ok({ + body: stats, + }); + } catch (error: any) { + logger.error(error); + return response.customError({ + statusCode: error.statusCode || 500, + body: { + message: error.message, + attributes: { + error: error.body?.error || error.message, + }, + }, + }); + } + } + ); +}; diff --git a/src/plugins/vis_augmenter/server/routes/stats_helpers.test.ts b/src/plugins/vis_augmenter/server/routes/stats_helpers.test.ts new file mode 100644 index 000000000000..96f561c49e05 --- /dev/null +++ b/src/plugins/vis_augmenter/server/routes/stats_helpers.test.ts @@ -0,0 +1,203 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { SavedObjectsFindResult } from '../../../../../src/core/server'; +import { AugmentVisSavedObjectAttributes } from '../../common'; +import { getAugmentVisSavedObjects, getStats } from './stats_helpers'; + +const ORIGIN_PLUGIN_1 = 'origin-plugin-1'; +const ORIGIN_PLUGIN_2 = 'origin-plugin-2'; +const PLUGIN_RESOURCE_TYPE_1 = 'plugin-resource-type-1'; +const PLUGIN_RESOURCE_TYPE_2 = 'plugin-resource-type-2'; +const PLUGIN_RESOURCE_ID_1 = 'plugin-resource-id-1'; +const PLUGIN_RESOURCE_ID_2 = 'plugin-resource-id-2'; +const PLUGIN_RESOURCE_ID_3 = 'plugin-resource-id-3'; +const VIS_ID_1 = 'vis-id-1'; +const VIS_ID_2 = 'vis-id-2'; +const PER_PAGE = 4; + +const SINGLE_SAVED_OBJ = [ + { + attributes: { + originPlugin: ORIGIN_PLUGIN_1, + pluginResource: { + type: PLUGIN_RESOURCE_TYPE_1, + id: PLUGIN_RESOURCE_ID_1, + }, + visId: VIS_ID_1, + }, + }, +] as Array>; + +const MULTIPLE_SAVED_OBJS = [ + { + attributes: { + originPlugin: ORIGIN_PLUGIN_1, + pluginResource: { + type: PLUGIN_RESOURCE_TYPE_1, + id: PLUGIN_RESOURCE_ID_1, + }, + visId: VIS_ID_1, + }, + }, + { + attributes: { + originPlugin: ORIGIN_PLUGIN_2, + pluginResource: { + type: PLUGIN_RESOURCE_TYPE_2, + id: PLUGIN_RESOURCE_ID_2, + }, + visId: VIS_ID_1, + }, + }, + { + attributes: { + originPlugin: ORIGIN_PLUGIN_2, + pluginResource: { + type: PLUGIN_RESOURCE_TYPE_2, + id: PLUGIN_RESOURCE_ID_2, + }, + visId: VIS_ID_2, + }, + }, + { + attributes: { + originPlugin: ORIGIN_PLUGIN_2, + pluginResource: { + type: PLUGIN_RESOURCE_TYPE_2, + id: PLUGIN_RESOURCE_ID_3, + }, + visId: VIS_ID_1, + }, + }, +] as Array>; + +describe('getAugmentVisSavedObjs()', function () { + const mockClient = { + find: jest.fn(), + }; + it('should return empty arr if no objs found', async function () { + mockClient.find.mockResolvedValueOnce({ + total: 0, + page: 1, + per_page: PER_PAGE, + saved_objects: [], + }); + + // @ts-ignore + const response = await getAugmentVisSavedObjects(mockClient, PER_PAGE); + expect(response.total).toEqual(0); + expect(response.saved_objects).toHaveLength(0); + }); + + it('should return all augment-vis saved objects', async function () { + mockClient.find.mockResolvedValueOnce({ + total: 4, + page: 1, + per_page: PER_PAGE, + saved_objects: MULTIPLE_SAVED_OBJS, + }); + + // @ts-ignore + const response = await getAugmentVisSavedObjects(mockClient, PER_PAGE); + expect(response.total).toEqual(4); + expect(response.saved_objects).toHaveLength(4); + expect(response.saved_objects[0].attributes.originPlugin).toEqual(ORIGIN_PLUGIN_1); + expect(response.saved_objects[1].attributes.originPlugin).toEqual(ORIGIN_PLUGIN_2); + expect(response.saved_objects[2].attributes.originPlugin).toEqual(ORIGIN_PLUGIN_2); + expect(response.saved_objects[3].attributes.originPlugin).toEqual(ORIGIN_PLUGIN_2); + }); + + it('should correctly perform pagination', async function () { + mockClient.find + .mockResolvedValueOnce({ + total: 5, + page: 1, + per_page: PER_PAGE, + saved_objects: MULTIPLE_SAVED_OBJS, + }) + .mockResolvedValueOnce({ + total: 5, + page: 2, + per_page: PER_PAGE, + saved_objects: SINGLE_SAVED_OBJ, + }); + + // @ts-ignore + const response = await getAugmentVisSavedObjects(mockClient, PER_PAGE); + expect(response.total).toEqual(5); + expect(response.saved_objects).toHaveLength(5); + expect(response.saved_objects[0].attributes.originPlugin).toEqual(ORIGIN_PLUGIN_1); + expect(response.saved_objects[1].attributes.originPlugin).toEqual(ORIGIN_PLUGIN_2); + expect(response.saved_objects[2].attributes.originPlugin).toEqual(ORIGIN_PLUGIN_2); + expect(response.saved_objects[3].attributes.originPlugin).toEqual(ORIGIN_PLUGIN_2); + expect(response.saved_objects[4].attributes.originPlugin).toEqual(ORIGIN_PLUGIN_1); + }); +}); + +describe('getStats()', function () { + it('should return total of 0 and empty mappings on empty response', function () { + const response = getStats({ + total: 0, + page: 1, + per_page: PER_PAGE, + saved_objects: [], + }); + expect(response.total_objs).toEqual(0); + expect(response.obj_breakdown.origin_plugin).toEqual({}); + expect(response.obj_breakdown.plugin_resource_type).toEqual({}); + expect(response.obj_breakdown.plugin_resource_id).toEqual({}); + expect(response.obj_breakdown.visualization_id).toEqual({}); + }); + + it('should return correct count and mappings on single-obj response', function () { + const response = getStats({ + total: 1, + page: 1, + per_page: PER_PAGE, + saved_objects: SINGLE_SAVED_OBJ, + }); + expect(response.total_objs).toEqual(1); + expect(response.obj_breakdown.origin_plugin).toEqual({ + [ORIGIN_PLUGIN_1]: 1, + }); + expect(response.obj_breakdown.plugin_resource_type).toEqual({ + [PLUGIN_RESOURCE_TYPE_1]: 1, + }); + expect(response.obj_breakdown.plugin_resource_id).toEqual({ + [PLUGIN_RESOURCE_ID_1]: 1, + }); + expect(response.obj_breakdown.visualization_id).toEqual({ + [VIS_ID_1]: 1, + }); + }); + + it('should return correct count and mappings on multiple-obj response', function () { + const response = getStats({ + total: MULTIPLE_SAVED_OBJS.length, + page: 1, + per_page: PER_PAGE, + saved_objects: MULTIPLE_SAVED_OBJS, + }); + expect(response.total_objs).toEqual(4); + expect(response.obj_breakdown.origin_plugin).toEqual({ + [ORIGIN_PLUGIN_1]: 1, + [ORIGIN_PLUGIN_2]: 3, + }); + expect(response.obj_breakdown.plugin_resource_type).toEqual({ + [PLUGIN_RESOURCE_TYPE_1]: 1, + [PLUGIN_RESOURCE_TYPE_2]: 3, + }); + expect(response.obj_breakdown.plugin_resource_id).toEqual({ + [PLUGIN_RESOURCE_ID_1]: 1, + [PLUGIN_RESOURCE_ID_2]: 2, + [PLUGIN_RESOURCE_ID_3]: 1, + }); + expect(response.obj_breakdown.visualization_id).toEqual({ + [VIS_ID_1]: 3, + [VIS_ID_2]: 1, + }); + }); +}); diff --git a/src/plugins/vis_augmenter/server/routes/stats_helpers.ts b/src/plugins/vis_augmenter/server/routes/stats_helpers.ts new file mode 100644 index 000000000000..33e73ec47306 --- /dev/null +++ b/src/plugins/vis_augmenter/server/routes/stats_helpers.ts @@ -0,0 +1,91 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { get } from 'lodash'; +import { + SavedObjectsFindResponse, + SavedObjectsClientContract, +} from '../../../../../src/core/server'; +import { AugmentVisSavedObjectAttributes } from '../../common'; + +interface ObjectBreakdown { + origin_plugin: { [key: string]: number }; + plugin_resource_type: { [key: string]: number }; + plugin_resource_id: { [key: string]: number }; + visualization_id: { [key: string]: number }; +} + +interface VisAugmenterStats { + total_objs: number; + obj_breakdown: ObjectBreakdown; +} + +export const getAugmentVisSavedObjects = async ( + savedObjectsClient: SavedObjectsClientContract, + perPage: number +): Promise> => { + const augmentVisSavedObjects: SavedObjectsFindResponse = await savedObjectsClient?.find( + { + type: 'augment-vis', + perPage, + } + ); + // If there are more than perPage of objs, we need to make additional requests + if (augmentVisSavedObjects.total > perPage) { + const iterations = Math.ceil(augmentVisSavedObjects.total / perPage); + for (let i = 1; i < iterations; i++) { + const augmentVisSavedObjectsPage: SavedObjectsFindResponse = await savedObjectsClient?.find( + { + type: 'augment-vis', + perPage, + page: i + 1, + } + ); + augmentVisSavedObjects.saved_objects = [ + ...augmentVisSavedObjects.saved_objects, + ...augmentVisSavedObjectsPage.saved_objects, + ]; + } + } + return augmentVisSavedObjects; +}; + +/** + * Given the _find response that contains all of the saved objects, iterate through them and + * increment counters for each unique value we are tracking + */ +export const getStats = ( + resp: SavedObjectsFindResponse +): VisAugmenterStats => { + const originPluginMap = {} as { [originPlugin: string]: number }; + const pluginResourceTypeMap = {} as { [pluginResourceType: string]: number }; + const pluginResourceIdMap = {} as { [pluginResourceId: string]: number }; + const visualizationIdMap = {} as { [visualizationId: string]: number }; + + resp.saved_objects.forEach((augmentVisObj) => { + const originPlugin = augmentVisObj.attributes.originPlugin; + const pluginResourceType = augmentVisObj.attributes.pluginResource.type; + const pluginResourceId = augmentVisObj.attributes.pluginResource.id; + const visualizationId = augmentVisObj.attributes.visId as string; + + originPluginMap[originPlugin] = (get(originPluginMap, originPlugin, 0) as number) + 1; + pluginResourceTypeMap[pluginResourceType] = + (get(pluginResourceTypeMap, pluginResourceType, 0) as number) + 1; + pluginResourceIdMap[pluginResourceId] = + (get(pluginResourceIdMap, pluginResourceId, 0) as number) + 1; + visualizationIdMap[visualizationId] = + (get(visualizationIdMap, visualizationId, 0) as number) + 1; + }); + + return { + total_objs: resp.total, + obj_breakdown: { + origin_plugin: originPluginMap, + plugin_resource_type: pluginResourceTypeMap, + plugin_resource_id: pluginResourceIdMap, + visualization_id: visualizationIdMap, + }, + }; +}; diff --git a/src/plugins/vis_augmenter/server/saved_objects/augment_vis.ts b/src/plugins/vis_augmenter/server/saved_objects/augment_vis.ts index 0efe98fc14ce..1202e6168f77 100644 --- a/src/plugins/vis_augmenter/server/saved_objects/augment_vis.ts +++ b/src/plugins/vis_augmenter/server/saved_objects/augment_vis.ts @@ -24,7 +24,13 @@ export const augmentVisSavedObjectType: SavedObjectsType = { properties: { title: { type: 'text' }, description: { type: 'text' }, - pluginResourceId: { type: 'text' }, + originPlugin: { type: 'text' }, + pluginResource: { + properties: { + type: { type: 'text' }, + id: { type: 'text' }, + }, + }, visName: { type: 'keyword', index: false, doc_values: false }, visLayerExpressionFn: { properties: {