diff --git a/src/plugins/saved_objects_management/opensearch_dashboards.json b/src/plugins/saved_objects_management/opensearch_dashboards.json
index 1de1260afceb..f76b69999ecb 100644
--- a/src/plugins/saved_objects_management/opensearch_dashboards.json
+++ b/src/plugins/saved_objects_management/opensearch_dashboards.json
@@ -3,7 +3,7 @@
"version": "opensearchDashboards",
"server": true,
"ui": true,
- "requiredPlugins": ["management", "data"],
+ "requiredPlugins": ["management", "data", "uiActions"],
"optionalPlugins": [
"dashboard",
"visualizations",
diff --git a/src/plugins/saved_objects_management/public/index.ts b/src/plugins/saved_objects_management/public/index.ts
index 2377afe175c4..317b3079efa0 100644
--- a/src/plugins/saved_objects_management/public/index.ts
+++ b/src/plugins/saved_objects_management/public/index.ts
@@ -48,6 +48,8 @@ export {
} from './services';
export { ProcessedImportResponse, processImportResponse, FailedImport } from './lib';
export { SavedObjectRelation, SavedObjectWithMetadata, SavedObjectMetadata } from './types';
+export { SAVED_OBJECT_DELETE_TRIGGER, savedObjectDeleteTrigger } from './triggers';
+export { SavedObjectDeleteContext } from './ui_actions_bootstrap';
export function plugin(initializerContext: PluginInitializerContext) {
return new SavedObjectsManagementPlugin();
diff --git a/src/plugins/saved_objects_management/public/management_section/mount_section.tsx b/src/plugins/saved_objects_management/public/management_section/mount_section.tsx
index 2c42df5c7824..a1c7b5343eb1 100644
--- a/src/plugins/saved_objects_management/public/management_section/mount_section.tsx
+++ b/src/plugins/saved_objects_management/public/management_section/mount_section.tsx
@@ -59,7 +59,7 @@ export const mountManagementSection = async ({
mountParams,
serviceRegistry,
}: MountParams) => {
- const [coreStart, { data }, pluginStart] = await core.getStartServices();
+ const [coreStart, { data, uiActions }, pluginStart] = await core.getStartServices();
const { element, history, setBreadcrumbs } = mountParams;
if (allowedObjectTypes === undefined) {
allowedObjectTypes = await getAllowedTypes(coreStart.http);
@@ -88,6 +88,7 @@ export const mountManagementSection = async ({
}>
void;
history: ScopedHistory;
@@ -79,6 +82,7 @@ const SavedObjectsEditionPage = ({
savedObjectsClient={coreStart.savedObjects.client}
overlays={coreStart.overlays}
notifications={coreStart.notifications}
+ uiActions={uiActionsStart}
capabilities={capabilities}
notFoundType={query.notFound as string}
history={history}
diff --git a/src/plugins/saved_objects_management/public/plugin.test.ts b/src/plugins/saved_objects_management/public/plugin.test.ts
index e55760846a5f..c8e762f73dcc 100644
--- a/src/plugins/saved_objects_management/public/plugin.test.ts
+++ b/src/plugins/saved_objects_management/public/plugin.test.ts
@@ -32,6 +32,7 @@ import { coreMock } from '../../../core/public/mocks';
import { homePluginMock } from '../../home/public/mocks';
import { managementPluginMock } from '../../management/public/mocks';
import { dataPluginMock } from '../../data/public/mocks';
+import { uiActionsPluginMock } from '../../ui_actions/public/mocks';
import { SavedObjectsManagementPlugin } from './plugin';
describe('SavedObjectsManagementPlugin', () => {
@@ -48,8 +49,13 @@ describe('SavedObjectsManagementPlugin', () => {
});
const homeSetup = homePluginMock.createSetupContract();
const managementSetup = managementPluginMock.createSetupContract();
+ const uiActionsSetup = uiActionsPluginMock.createSetupContract();
- await plugin.setup(coreSetup, { home: homeSetup, management: managementSetup });
+ await plugin.setup(coreSetup, {
+ home: homeSetup,
+ management: managementSetup,
+ uiActions: uiActionsSetup,
+ });
expect(homeSetup.featureCatalogue.register).toHaveBeenCalledTimes(1);
expect(homeSetup.featureCatalogue.register).toHaveBeenCalledWith(
diff --git a/src/plugins/saved_objects_management/public/plugin.ts b/src/plugins/saved_objects_management/public/plugin.ts
index b2bcb614c50a..43356eb8f9e5 100644
--- a/src/plugins/saved_objects_management/public/plugin.ts
+++ b/src/plugins/saved_objects_management/public/plugin.ts
@@ -33,6 +33,7 @@ import { CoreSetup, CoreStart, Plugin } from 'src/core/public';
import { VisBuilderStart } from '../../vis_builder/public';
import { ManagementSetup } from '../../management/public';
+import { UiActionsSetup, UiActionsStart } from '../../ui_actions/public';
import { DataPublicPluginStart } from '../../data/public';
import { DashboardStart } from '../../dashboard/public';
import { DiscoverStart } from '../../discover/public';
@@ -53,6 +54,7 @@ import {
ISavedObjectsManagementServiceRegistry,
} from './services';
import { registerServices } from './register_services';
+import { bootstrap } from './ui_actions_bootstrap';
export interface SavedObjectsManagementPluginSetup {
actions: SavedObjectsManagementActionServiceSetup;
@@ -70,6 +72,7 @@ export interface SavedObjectsManagementPluginStart {
export interface SetupDependencies {
management: ManagementSetup;
home?: HomePublicPluginSetup;
+ uiActions: UiActionsSetup;
}
export interface StartDependencies {
@@ -79,6 +82,7 @@ export interface StartDependencies {
visAugmenter?: VisAugmenterStart;
discover?: DiscoverStart;
visBuilder?: VisBuilderStart;
+ uiActions: UiActionsStart;
}
export class SavedObjectsManagementPlugin
@@ -96,7 +100,7 @@ export class SavedObjectsManagementPlugin
public setup(
core: CoreSetup,
- { home, management }: SetupDependencies
+ { home, management, uiActions }: SetupDependencies
): SavedObjectsManagementPluginSetup {
const actionSetup = this.actionService.setup();
const columnSetup = this.columnService.setup();
@@ -136,6 +140,9 @@ export class SavedObjectsManagementPlugin
},
});
+ // sets up the context mappings and registers any triggers/actions for the plugin
+ bootstrap(uiActions);
+
// depends on `getStartServices`, should not be awaited
registerServices(this.serviceRegistry, core.getStartServices);
@@ -147,7 +154,7 @@ export class SavedObjectsManagementPlugin
};
}
- public start(core: CoreStart, { data }: StartDependencies) {
+ public start(core: CoreStart, { data, uiActions }: StartDependencies) {
const actionStart = this.actionService.start();
const columnStart = this.columnService.start();
const namespaceStart = this.namespaceService.start();
diff --git a/src/plugins/saved_objects_management/public/triggers/index.ts b/src/plugins/saved_objects_management/public/triggers/index.ts
new file mode 100644
index 000000000000..001864544cd3
--- /dev/null
+++ b/src/plugins/saved_objects_management/public/triggers/index.ts
@@ -0,0 +1,9 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+export {
+ SAVED_OBJECT_DELETE_TRIGGER,
+ savedObjectDeleteTrigger,
+} from './saved_object_delete_trigger';
diff --git a/src/plugins/saved_objects_management/public/triggers/saved_object_delete_trigger.ts b/src/plugins/saved_objects_management/public/triggers/saved_object_delete_trigger.ts
new file mode 100644
index 000000000000..9c3ced212d5d
--- /dev/null
+++ b/src/plugins/saved_objects_management/public/triggers/saved_object_delete_trigger.ts
@@ -0,0 +1,18 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { i18n } from '@osd/i18n';
+import { Trigger } from '../../../ui_actions/public';
+
+export const SAVED_OBJECT_DELETE_TRIGGER = 'SAVED_OBJECT_DELETE_TRIGGER';
+export const savedObjectDeleteTrigger: Trigger<'SAVED_OBJECT_DELETE_TRIGGER'> = {
+ id: SAVED_OBJECT_DELETE_TRIGGER,
+ title: i18n.translate('savedObjectsManagement.triggers.savedObjectDeleteTitle', {
+ defaultMessage: 'Saved object delete',
+ }),
+ description: i18n.translate('savedObjectsManagement.triggers.savedObjectDeleteDescription', {
+ defaultMessage: 'Perform additional actions after deleting a saved object',
+ }),
+};
diff --git a/src/plugins/saved_objects_management/public/ui_actions_bootstrap.ts b/src/plugins/saved_objects_management/public/ui_actions_bootstrap.ts
new file mode 100644
index 000000000000..e005ba660dee
--- /dev/null
+++ b/src/plugins/saved_objects_management/public/ui_actions_bootstrap.ts
@@ -0,0 +1,22 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { UiActionsSetup } from '../../ui_actions/public';
+import { SAVED_OBJECT_DELETE_TRIGGER, savedObjectDeleteTrigger } from './triggers';
+
+export interface SavedObjectDeleteContext {
+ type: string;
+ savedObjectId: string;
+}
+
+declare module '../../ui_actions/public' {
+ export interface TriggerContextMapping {
+ [SAVED_OBJECT_DELETE_TRIGGER]: SavedObjectDeleteContext;
+ }
+}
+
+export const bootstrap = (uiActions: UiActionsSetup) => {
+ uiActions.registerTrigger(savedObjectDeleteTrigger);
+};
diff --git a/src/plugins/vis_augmenter/opensearch_dashboards.json b/src/plugins/vis_augmenter/opensearch_dashboards.json
index 007cae78290b..9026bdd24859 100644
--- a/src/plugins/vis_augmenter/opensearch_dashboards.json
+++ b/src/plugins/vis_augmenter/opensearch_dashboards.json
@@ -12,5 +12,5 @@
"uiActions",
"embeddable"
],
- "requiredBundles": ["opensearchDashboardsReact"]
+ "requiredBundles": ["opensearchDashboardsReact", "savedObjectsManagement"]
}
diff --git a/src/plugins/vis_augmenter/public/actions/index.ts b/src/plugins/vis_augmenter/public/actions/index.ts
new file mode 100644
index 000000000000..893032f86813
--- /dev/null
+++ b/src/plugins/vis_augmenter/public/actions/index.ts
@@ -0,0 +1,10 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+export {
+ PLUGIN_RESOURCE_DELETE_ACTION,
+ PluginResourceDeleteAction,
+} from './plugin_resource_delete_action';
+export { SAVED_OBJECT_DELETE_ACTION, SavedObjectDeleteAction } from './saved_object_delete_action';
diff --git a/src/plugins/vis_augmenter/public/actions/plugin_resource_delete_action.test.ts b/src/plugins/vis_augmenter/public/actions/plugin_resource_delete_action.test.ts
new file mode 100644
index 000000000000..0060b7837f1f
--- /dev/null
+++ b/src/plugins/vis_augmenter/public/actions/plugin_resource_delete_action.test.ts
@@ -0,0 +1,81 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { createPointInTimeEventsVisLayer } from '../mocks';
+import { generateAugmentVisSavedObject } from '../saved_augment_vis';
+import { PluginResourceDeleteAction } from './plugin_resource_delete_action';
+
+const sampleSavedObj = generateAugmentVisSavedObject(
+ 'test-id',
+ {
+ type: 'PointInTimeEvents',
+ name: 'test-fn-name',
+ args: {},
+ },
+ 'test-vis-id',
+ 'test-origin-plugin',
+ {
+ type: 'test-resource-type',
+ id: 'test-resource-id',
+ }
+);
+
+const sampleVisLayer = createPointInTimeEventsVisLayer();
+
+describe('SavedObjectDeleteAction', () => {
+ it('is incompatible with invalid saved obj list', async () => {
+ const action = new PluginResourceDeleteAction();
+ const visLayers = [sampleVisLayer];
+ // @ts-ignore
+ expect(await action.isCompatible({ savedObjs: null, visLayers })).toBe(false);
+ // @ts-ignore
+ expect(await action.isCompatible({ savedObjs: undefined, visLayers })).toBe(false);
+ expect(await action.isCompatible({ savedObjs: [], visLayers })).toBe(false);
+ });
+
+ it('is incompatible with invalid vislayer list', async () => {
+ const action = new PluginResourceDeleteAction();
+ const savedObjs = [sampleSavedObj];
+ // @ts-ignore
+ expect(await action.isCompatible({ savedObjs, visLayers: null })).toBe(false);
+ // @ts-ignore
+ expect(await action.isCompatible({ savedObjs, visLayers: undefined })).toBe(false);
+ expect(await action.isCompatible({ savedObjs, visLayers: [] })).toBe(false);
+ });
+
+ it('execute throws error if incompatible saved objs list', async () => {
+ const action = new PluginResourceDeleteAction();
+ async function check(savedObjs: any, visLayers: any) {
+ await action.execute({ savedObjs, visLayers });
+ }
+ await expect(check(null, [sampleVisLayer])).rejects.toThrow(Error);
+ });
+
+ it('execute throws error if incompatible vis layer list', async () => {
+ const action = new PluginResourceDeleteAction();
+ async function check(savedObjs: any, visLayers: any) {
+ await action.execute({ savedObjs, visLayers });
+ }
+ await expect(check([sampleSavedObj], null)).rejects.toThrow(Error);
+ });
+
+ it('execute is successful if valid saved obj and vis layer lists', async () => {
+ const action = new PluginResourceDeleteAction();
+ async function check(savedObjs: any, visLayers: any) {
+ await action.execute({ savedObjs, visLayers });
+ }
+ await expect(check([sampleSavedObj], [sampleVisLayer])).resolves;
+ });
+
+ it('Returns display name', async () => {
+ const action = new PluginResourceDeleteAction();
+ expect(action.getDisplayName()).toBeDefined();
+ });
+
+ it('Returns icon type', async () => {
+ const action = new PluginResourceDeleteAction();
+ expect(action.getIconType()).toBeDefined();
+ });
+});
diff --git a/src/plugins/vis_augmenter/public/actions/plugin_resource_delete_action.ts b/src/plugins/vis_augmenter/public/actions/plugin_resource_delete_action.ts
new file mode 100644
index 000000000000..6e3939820d28
--- /dev/null
+++ b/src/plugins/vis_augmenter/public/actions/plugin_resource_delete_action.ts
@@ -0,0 +1,47 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { isEmpty } from 'lodash';
+import { i18n } from '@osd/i18n';
+import { EuiIconType } from '@elastic/eui/src/components/icon/icon';
+import { Action, IncompatibleActionError } from '../../../ui_actions/public';
+import { getSavedAugmentVisLoader } from '../services';
+import { PluginResourceDeleteContext } from '../ui_actions_bootstrap';
+import { cleanupStaleObjects } from '../utils';
+
+export const PLUGIN_RESOURCE_DELETE_ACTION = 'PLUGIN_RESOURCE_DELETE_ACTION';
+
+export class PluginResourceDeleteAction implements Action {
+ public readonly type = PLUGIN_RESOURCE_DELETE_ACTION;
+ public readonly id = PLUGIN_RESOURCE_DELETE_ACTION;
+ public order = 1;
+
+ public getIconType(): EuiIconType {
+ return `trash`;
+ }
+
+ public getDisplayName() {
+ return i18n.translate('dashboard.actions.deleteSavedObject.name', {
+ defaultMessage:
+ 'Clean up all augment-vis saved objects associated to the deleted visualization',
+ });
+ }
+
+ public async isCompatible({ savedObjs, visLayers }: PluginResourceDeleteContext) {
+ return !isEmpty(savedObjs) && !isEmpty(visLayers);
+ }
+
+ /**
+ * If we have just collected all of the saved objects and generated vis layers,
+ * sweep through them all and if any of the resources are deleted, delete those
+ * corresponding saved objects
+ */
+ public async execute({ savedObjs, visLayers }: PluginResourceDeleteContext) {
+ if (!(await this.isCompatible({ savedObjs, visLayers }))) {
+ throw new IncompatibleActionError();
+ }
+ cleanupStaleObjects(savedObjs, visLayers, getSavedAugmentVisLoader());
+ }
+}
diff --git a/src/plugins/vis_augmenter/public/actions/saved_object_delete_action.test.ts b/src/plugins/vis_augmenter/public/actions/saved_object_delete_action.test.ts
new file mode 100644
index 000000000000..cec742a218cc
--- /dev/null
+++ b/src/plugins/vis_augmenter/public/actions/saved_object_delete_action.test.ts
@@ -0,0 +1,84 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { SavedObjectDeleteAction } from './saved_object_delete_action';
+import services from '../services';
+
+jest.mock('src/plugins/vis_augmenter/public/services.ts', () => {
+ return {
+ getSavedAugmentVisLoader: () => {
+ return {
+ delete: () => {},
+ findAll: () => {
+ return {
+ hits: [],
+ };
+ },
+ };
+ },
+ };
+});
+
+describe('SavedObjectDeleteAction', () => {
+ it('is incompatible with invalid types', async () => {
+ const action = new SavedObjectDeleteAction();
+ const savedObjectId = '1234';
+ // @ts-ignore
+ expect(await action.isCompatible({ type: null, savedObjectId })).toBe(false);
+ // @ts-ignore
+ expect(await action.isCompatible({ type: undefined, savedObjectId })).toBe(false);
+ expect(await action.isCompatible({ type: '', savedObjectId })).toBe(false);
+ expect(await action.isCompatible({ type: 'not-visualization-type', savedObjectId })).toBe(
+ false
+ );
+ expect(await action.isCompatible({ type: 'savedSearch', savedObjectId })).toBe(false);
+ });
+
+ it('is incompatible with invalid saved obj ids', async () => {
+ const action = new SavedObjectDeleteAction();
+ const type = 'visualization';
+ // @ts-ignore
+ expect(await action.isCompatible({ type, savedObjectId: null })).toBe(false);
+ // @ts-ignore
+ expect(await action.isCompatible({ type, savedObjectId: undefined })).toBe(false);
+ expect(await action.isCompatible({ type, savedObjectId: '' })).toBe(false);
+ });
+
+ it('execute throws error if incompatible type', async () => {
+ const action = new SavedObjectDeleteAction();
+ async function check(type: any, id: any) {
+ await action.execute({ type, savedObjectId: id });
+ }
+ await expect(check(null, '1234')).rejects.toThrow(Error);
+ });
+
+ it('execute throws error if incompatible saved obj id', async () => {
+ const action = new SavedObjectDeleteAction();
+ async function check(type: any, id: any) {
+ await action.execute({ type, savedObjectId: id });
+ }
+ await expect(check('visualization', null)).rejects.toThrow(Error);
+ });
+
+ it('execute is successful if valid type and saved obj id', async () => {
+ const getLoaderSpy = jest.spyOn(services, 'getSavedAugmentVisLoader');
+ const action = new SavedObjectDeleteAction();
+ async function check(type: any, id: any) {
+ await action.execute({ type, savedObjectId: id });
+ }
+ await expect(check('visualization', 'test-id')).resolves;
+ expect(getLoaderSpy).toHaveBeenCalledTimes(1);
+ });
+
+ it('Returns display name', async () => {
+ const action = new SavedObjectDeleteAction();
+ expect(action.getDisplayName()).toBeDefined();
+ });
+
+ it('Returns icon type', async () => {
+ const action = new SavedObjectDeleteAction();
+ expect(action.getIconType()).toBeDefined();
+ });
+});
diff --git a/src/plugins/vis_augmenter/public/actions/saved_object_delete_action.ts b/src/plugins/vis_augmenter/public/actions/saved_object_delete_action.ts
new file mode 100644
index 000000000000..2db6d1210448
--- /dev/null
+++ b/src/plugins/vis_augmenter/public/actions/saved_object_delete_action.ts
@@ -0,0 +1,53 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { isEmpty } from 'lodash';
+import { i18n } from '@osd/i18n';
+import { EuiIconType } from '@elastic/eui/src/components/icon/icon';
+import { Action, IncompatibleActionError } from '../../../ui_actions/public';
+import { getAllAugmentVisSavedObjs } from '../utils';
+import { getSavedAugmentVisLoader } from '../services';
+import { SavedObjectDeleteContext } from '../ui_actions_bootstrap';
+
+export const SAVED_OBJECT_DELETE_ACTION = 'SAVED_OBJECT_DELETE_ACTION';
+
+export class SavedObjectDeleteAction implements Action {
+ public readonly type = SAVED_OBJECT_DELETE_ACTION;
+ public readonly id = SAVED_OBJECT_DELETE_ACTION;
+ public order = 1;
+
+ public getIconType(): EuiIconType {
+ return `trash`;
+ }
+
+ public getDisplayName() {
+ return i18n.translate('dashboard.actions.deleteSavedObject.name', {
+ defaultMessage: 'Clean up augment-vis saved objects associated to a deleted vis',
+ });
+ }
+
+ public async isCompatible({ type, savedObjectId }: SavedObjectDeleteContext) {
+ return type === 'visualization' && (savedObjectId ? true : false);
+ }
+
+ /**
+ * If deletion of a vis saved object has been triggered, we want to clean up
+ * any augment-vis saved objects that have a reference to this vis since it
+ * is now stale.
+ */
+ public async execute({ type, savedObjectId }: SavedObjectDeleteContext) {
+ if (!(await this.isCompatible({ type, savedObjectId }))) {
+ throw new IncompatibleActionError();
+ }
+
+ const loader = getSavedAugmentVisLoader();
+ const allAugmentVisObjs = await getAllAugmentVisSavedObjs(loader);
+ const augmentVisIdsToDelete = allAugmentVisObjs
+ .filter((augmentVisObj) => augmentVisObj.visId === savedObjectId)
+ .map((augmentVisObj) => augmentVisObj.id as string);
+
+ if (!isEmpty(augmentVisIdsToDelete)) loader.delete(augmentVisIdsToDelete);
+ }
+}
diff --git a/src/plugins/vis_augmenter/public/index.ts b/src/plugins/vis_augmenter/public/index.ts
index 9a4a5faeb803..3fc82ecc0267 100644
--- a/src/plugins/vis_augmenter/public/index.ts
+++ b/src/plugins/vis_augmenter/public/index.ts
@@ -34,3 +34,5 @@ export * from './constants';
export * from './vega';
export * from './saved_augment_vis';
export * from './test_constants';
+export * from './triggers';
+export * from './actions';
diff --git a/src/plugins/vis_augmenter/public/mocks.ts b/src/plugins/vis_augmenter/public/mocks.ts
index 34d6d81a1ef6..d3ff8422287d 100644
--- a/src/plugins/vis_augmenter/public/mocks.ts
+++ b/src/plugins/vis_augmenter/public/mocks.ts
@@ -90,6 +90,7 @@ const PLUGIN_RESOURCE = {
} as PluginResource;
const EVENT_COUNT = 3;
const ERROR_MESSAGE = 'test-error-message';
+const EVENT_TYPE = 'test-event-type';
export const createPluginResource = (
type: string = PLUGIN_RESOURCE.type,
@@ -171,6 +172,7 @@ export const createPointInTimeEventsVisLayer = (
type: VisLayerTypes.PointInTimeEvents,
pluginResource,
events,
+ pluginEventType: EVENT_TYPE,
error: error
? {
type: VisLayerErrorTypes.FETCH_FAILURE,
diff --git a/src/plugins/vis_augmenter/public/plugin.ts b/src/plugins/vis_augmenter/public/plugin.ts
index d7b09d57d790..9760bfd75b2d 100644
--- a/src/plugins/vis_augmenter/public/plugin.ts
+++ b/src/plugins/vis_augmenter/public/plugin.ts
@@ -8,7 +8,6 @@ import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '../../..
import { visLayers } from './expressions';
import { setSavedAugmentVisLoader, setUISettings } from './services';
import { createSavedAugmentVisLoader, SavedAugmentVisLoader } from './saved_augment_vis';
-import { registerTriggersAndActions } from './ui_actions_bootstrap';
import { UiActionsStart } from '../../ui_actions/public';
import {
setUiActions,
@@ -21,6 +20,7 @@ import { EmbeddableStart } from '../../embeddable/public';
import { DataPublicPluginStart } from '../../data/public';
import { VisualizationsStart } from '../../visualizations/public';
import { VIEW_EVENTS_FLYOUT_STATE, setFlyoutState } from './view_events_flyout';
+import { bootstrapUiActions } from './ui_actions_bootstrap';
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface VisAugmenterSetup {}
@@ -51,6 +51,7 @@ export class VisAugmenterPlugin
): VisAugmenterSetup {
expressions.registerType(visLayers);
setUISettings(core.uiSettings);
+
return {};
}
@@ -65,8 +66,6 @@ export class VisAugmenterPlugin
setCore(core);
setFlyoutState(VIEW_EVENTS_FLYOUT_STATE.CLOSED);
- registerTriggersAndActions(core);
-
const savedAugmentVisLoader = createSavedAugmentVisLoader({
savedObjectsClient: core.savedObjects.client,
indexPatterns: data.indexPatterns,
@@ -75,6 +74,10 @@ export class VisAugmenterPlugin
overlays: core.overlays,
});
setSavedAugmentVisLoader(savedAugmentVisLoader);
+
+ // sets up the context mappings and registers any triggers/actions for the plugin
+ bootstrapUiActions(uiActions);
+
return { savedAugmentVisLoader };
}
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 88178a7a08cd..d40382b3a104 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
@@ -24,9 +24,23 @@ export function createSavedAugmentVisClass(services: SavedObjectOpenSearchDashbo
class SavedAugmentVis extends SavedObjectClass {
public static type: string = name;
- public static mapping: AugmentVisSavedObjectAttributes;
+ public static mapping: Record = {
+ title: 'text',
+ description: 'text',
+ originPlugin: 'text',
+ pluginResource: 'object',
+ visLayerExpressionFn: 'object',
+ visId: 'keyword,',
+ version: 'integer',
+ };
- constructor(opts: AugmentVisSavedObjectAttributes) {
+ constructor(opts: Record | string = {}) {
+ // The default delete() fn from the saved object loader will only
+ // pass a string ID. To handle that case here, we embed it within
+ // an opts object.
+ if (typeof opts !== 'object') {
+ opts = { id: opts };
+ }
super({
type: SavedAugmentVis.type,
mapping: SavedAugmentVis.mapping,
diff --git a/src/plugins/vis_augmenter/public/services.ts b/src/plugins/vis_augmenter/public/services.ts
index 48a3233714e6..1d7f3e2111db 100644
--- a/src/plugins/vis_augmenter/public/services.ts
+++ b/src/plugins/vis_augmenter/public/services.ts
@@ -17,6 +17,7 @@ export const [getSavedAugmentVisLoader, setSavedAugmentVisLoader] = createGetter
>('savedAugmentVisLoader');
export const [getUISettings, setUISettings] = createGetterSetter('UISettings');
+
export const [getUiActions, setUiActions] = createGetterSetter('UIActions');
export const [getEmbeddable, setEmbeddable] = createGetterSetter('embeddable');
@@ -30,3 +31,15 @@ export const [getVisualizations, setVisualizations] = createGetterSetter('Core');
+
+// This is primarily used for mocking this module and each of its fns in tests.
+// eslint-disable-next-line import/no-default-export
+export default {
+ getSavedAugmentVisLoader,
+ getUISettings,
+ getUiActions,
+ getEmbeddable,
+ getQueryService,
+ getVisualizations,
+ getCore,
+};
diff --git a/src/plugins/vis_augmenter/public/triggers/index.ts b/src/plugins/vis_augmenter/public/triggers/index.ts
new file mode 100644
index 000000000000..5b1833e38d62
--- /dev/null
+++ b/src/plugins/vis_augmenter/public/triggers/index.ts
@@ -0,0 +1,9 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+export {
+ PLUGIN_RESOURCE_DELETE_TRIGGER,
+ pluginResourceDeleteTrigger,
+} from './plugin_resource_delete_trigger';
diff --git a/src/plugins/vis_augmenter/public/triggers/plugin_resource_delete_trigger.ts b/src/plugins/vis_augmenter/public/triggers/plugin_resource_delete_trigger.ts
new file mode 100644
index 000000000000..249bb61132eb
--- /dev/null
+++ b/src/plugins/vis_augmenter/public/triggers/plugin_resource_delete_trigger.ts
@@ -0,0 +1,18 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { i18n } from '@osd/i18n';
+import { Trigger } from '../../../ui_actions/public';
+
+export const PLUGIN_RESOURCE_DELETE_TRIGGER = 'PLUGIN_RESOURCE_DELETE_TRIGGER';
+export const pluginResourceDeleteTrigger: Trigger<'PLUGIN_RESOURCE_DELETE_TRIGGER'> = {
+ id: PLUGIN_RESOURCE_DELETE_TRIGGER,
+ title: i18n.translate('visAugmenter.triggers.pluginResourceDeleteTitle', {
+ defaultMessage: 'Plugin resource delete',
+ }),
+ description: i18n.translate('visAugmenter.triggers.pluginResourceDeleteDescription', {
+ defaultMessage: 'Delete augment-vis saved objs associated to the deleted plugin resource',
+ }),
+};
diff --git a/src/plugins/vis_augmenter/public/types.ts b/src/plugins/vis_augmenter/public/types.ts
index 27fd9d7c241f..35c00ea932a7 100644
--- a/src/plugins/vis_augmenter/public/types.ts
+++ b/src/plugins/vis_augmenter/public/types.ts
@@ -10,6 +10,7 @@ export enum VisLayerTypes {
export enum VisLayerErrorTypes {
PERMISSIONS_FAILURE = 'PERMISSIONS_FAILURE',
FETCH_FAILURE = 'FETCH_FAILURE',
+ RESOURCE_DELETED = 'RESOURCE_DELETED',
}
export enum VisFlyoutContext {
diff --git a/src/plugins/vis_augmenter/public/ui_actions_bootstrap.ts b/src/plugins/vis_augmenter/public/ui_actions_bootstrap.ts
index aae3f237a28f..27bd6284e71b 100644
--- a/src/plugins/vis_augmenter/public/ui_actions_bootstrap.ts
+++ b/src/plugins/vis_augmenter/public/ui_actions_bootstrap.ts
@@ -3,45 +3,76 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import { CoreStart } from 'opensearch-dashboards/public';
import {
OpenEventsFlyoutAction,
ViewEventsOptionAction,
OPEN_EVENTS_FLYOUT_ACTION,
VIEW_EVENTS_OPTION_ACTION,
} from './view_events_flyout';
-import { externalActionTrigger, EXTERNAL_ACTION_TRIGGER } from '../../ui_actions/public';
import { CONTEXT_MENU_TRIGGER, EmbeddableContext } from '../../embeddable/public';
-import { getUiActions } from './services';
+import { SAVED_OBJECT_DELETE_TRIGGER } from '../../saved_objects_management/public';
+import {
+ externalActionTrigger,
+ EXTERNAL_ACTION_TRIGGER,
+ UiActionsSetup,
+} from '../../ui_actions/public';
+import { ISavedAugmentVis } from './saved_augment_vis';
+import { VisLayer } from './types';
+import {
+ PLUGIN_RESOURCE_DELETE_ACTION,
+ PluginResourceDeleteAction,
+ SAVED_OBJECT_DELETE_ACTION,
+ SavedObjectDeleteAction,
+} from './actions';
+import { PLUGIN_RESOURCE_DELETE_TRIGGER, pluginResourceDeleteTrigger } from './triggers';
export interface AugmentVisContext {
savedObjectId: string;
}
+export interface SavedObjectDeleteContext {
+ type: string;
+ savedObjectId: string;
+}
+
+export interface PluginResourceDeleteContext {
+ savedObjs: ISavedAugmentVis[];
+ visLayers: VisLayer[];
+}
+
// Overriding the mappings defined in UIActions plugin so that
// the new trigger and action definitions resolve.
// This is a common pattern among internal Dashboards plugins.
declare module '../../ui_actions/public' {
export interface TriggerContextMapping {
[EXTERNAL_ACTION_TRIGGER]: AugmentVisContext;
+ [PLUGIN_RESOURCE_DELETE_TRIGGER]: PluginResourceDeleteContext;
}
export interface ActionContextMapping {
[OPEN_EVENTS_FLYOUT_ACTION]: AugmentVisContext;
[VIEW_EVENTS_OPTION_ACTION]: EmbeddableContext;
+ [SAVED_OBJECT_DELETE_ACTION]: SavedObjectDeleteContext;
+ [PLUGIN_RESOURCE_DELETE_ACTION]: PluginResourceDeleteContext;
}
}
-export const registerTriggersAndActions = (core: CoreStart) => {
- const openEventsFlyoutAction = new OpenEventsFlyoutAction(core);
- const viewEventsOptionAction = new ViewEventsOptionAction(core);
+export const bootstrapUiActions = (uiActions: UiActionsSetup) => {
+ const openEventsFlyoutAction = new OpenEventsFlyoutAction();
+ const viewEventsOptionAction = new ViewEventsOptionAction();
+ const savedObjectDeleteAction = new SavedObjectDeleteAction();
+ const pluginResourceDeleteAction = new PluginResourceDeleteAction();
+
+ uiActions.registerAction(openEventsFlyoutAction);
+ uiActions.registerAction(viewEventsOptionAction);
+ uiActions.registerAction(savedObjectDeleteAction);
+ uiActions.registerAction(pluginResourceDeleteAction);
- getUiActions().registerAction(openEventsFlyoutAction);
- getUiActions().registerAction(viewEventsOptionAction);
- getUiActions().registerTrigger(externalActionTrigger);
+ uiActions.registerTrigger(externalActionTrigger);
+ uiActions.registerTrigger(pluginResourceDeleteTrigger);
- // Opening View Events flyout from the chart
- getUiActions().addTriggerAction(EXTERNAL_ACTION_TRIGGER, openEventsFlyoutAction);
- // Opening View Events flyout from the context menu
- getUiActions().addTriggerAction(CONTEXT_MENU_TRIGGER, viewEventsOptionAction);
+ uiActions.addTriggerAction(EXTERNAL_ACTION_TRIGGER, openEventsFlyoutAction);
+ uiActions.addTriggerAction(CONTEXT_MENU_TRIGGER, viewEventsOptionAction);
+ uiActions.addTriggerAction(SAVED_OBJECT_DELETE_TRIGGER, savedObjectDeleteAction);
+ uiActions.addTriggerAction(PLUGIN_RESOURCE_DELETE_TRIGGER, pluginResourceDeleteAction);
};
diff --git a/src/plugins/vis_augmenter/public/utils/utils.test.ts b/src/plugins/vis_augmenter/public/utils/utils.test.ts
index 707c36f87073..250e17e8d595 100644
--- a/src/plugins/vis_augmenter/public/utils/utils.test.ts
+++ b/src/plugins/vis_augmenter/public/utils/utils.test.ts
@@ -16,10 +16,14 @@ import {
ISavedAugmentVis,
VisLayerTypes,
VisLayerExpressionFn,
+ cleanupStaleObjects,
+ VisLayer,
+ PluginResource,
+ VisLayerErrorTypes,
+ SavedObjectLoaderAugmentVis,
} from '../';
import { PLUGIN_AUGMENTATION_ENABLE_SETTING } from '../../common/constants';
-import { AggConfigs, AggTypesRegistryStart, IndexPattern } from '../../../data/common';
-import { mockAggTypesRegistry } from '../../../data/common/search/aggs/test_helpers';
+import { AggConfigs } from '../../../data/common';
import { uiSettingsServiceMock } from '../../../../core/public/mocks';
import { setUISettings } from '../services';
import {
@@ -28,6 +32,7 @@ import {
VALID_AGGS,
VALID_CONFIG_STATES,
VALID_VIS,
+ createPointInTimeEventsVisLayer,
createVisLayer,
} from '../mocks';
@@ -429,4 +434,129 @@ describe('utils', () => {
expect(err?.stack).toStrictEqual(`-----resource-type-1-----\nID: 1234\nMessage: "uh-oh!"`);
});
});
+
+ describe('cleanupStaleObjects', () => {
+ const fn = {
+ type: VisLayerTypes.PointInTimeEvents,
+ name: 'test-fn',
+ args: {
+ testArg: 'test-value',
+ },
+ } as VisLayerExpressionFn;
+ const originPlugin = 'test-plugin';
+ const resourceId1 = 'resource-1';
+ const resourceId2 = 'resource-2';
+ const resourceType1 = 'resource-type-1';
+ const augmentVisObj1 = generateAugmentVisSavedObject('id-1', fn, 'vis-id-1', originPlugin, {
+ type: resourceType1,
+ id: resourceId1,
+ });
+ const augmentVisObj2 = generateAugmentVisSavedObject('id-2', fn, 'vis-id-1', originPlugin, {
+ type: resourceType1,
+ id: resourceId2,
+ });
+ const resource1 = {
+ type: 'test-resource-type-1',
+ id: resourceId1,
+ name: 'resource-1',
+ urlPath: 'test-path',
+ } as PluginResource;
+ const resource2 = {
+ type: 'test-resource-type-1',
+ id: resourceId2,
+ name: 'resource-2',
+ urlPath: 'test-path',
+ } as PluginResource;
+ const validVisLayer1 = createPointInTimeEventsVisLayer(originPlugin, resource1, 1, false);
+ const staleVisLayer1 = {
+ ...createPointInTimeEventsVisLayer(originPlugin, resource1, 0, true),
+ error: {
+ type: VisLayerErrorTypes.RESOURCE_DELETED,
+ message: 'resource is deleted',
+ },
+ };
+ const staleVisLayer2 = {
+ ...createPointInTimeEventsVisLayer(originPlugin, resource2, 0, true),
+ error: {
+ type: VisLayerErrorTypes.RESOURCE_DELETED,
+ message: 'resource is deleted',
+ },
+ };
+
+ it('no augment-vis objs, no vislayers', async () => {
+ const mockDeleteFn = jest.fn();
+ const augmentVisObjs = [] as ISavedAugmentVis[];
+ const visLayers = [] as VisLayer[];
+ const augmentVisLoader = createSavedAugmentVisLoader({
+ savedObjectsClient: {
+ ...getMockAugmentVisSavedObjectClient(augmentVisObjs),
+ delete: mockDeleteFn,
+ },
+ } as any) as SavedObjectLoaderAugmentVis;
+
+ cleanupStaleObjects(augmentVisObjs, visLayers, augmentVisLoader);
+
+ expect(mockDeleteFn).toHaveBeenCalledTimes(0);
+ });
+ it('no stale vislayers', async () => {
+ const mockDeleteFn = jest.fn();
+ const augmentVisObjs = [augmentVisObj1];
+ const visLayers = [validVisLayer1];
+ const augmentVisLoader = createSavedAugmentVisLoader({
+ savedObjectsClient: {
+ ...getMockAugmentVisSavedObjectClient(augmentVisObjs),
+ delete: mockDeleteFn,
+ },
+ } as any) as SavedObjectLoaderAugmentVis;
+
+ cleanupStaleObjects(augmentVisObjs, visLayers, augmentVisLoader);
+
+ expect(mockDeleteFn).toHaveBeenCalledTimes(0);
+ });
+ it('1 stale vislayer', async () => {
+ const mockDeleteFn = jest.fn();
+ const augmentVisObjs = [augmentVisObj1];
+ const visLayers = [staleVisLayer1];
+ const augmentVisLoader = createSavedAugmentVisLoader({
+ savedObjectsClient: {
+ ...getMockAugmentVisSavedObjectClient(augmentVisObjs),
+ delete: mockDeleteFn,
+ },
+ } as any) as SavedObjectLoaderAugmentVis;
+
+ cleanupStaleObjects(augmentVisObjs, visLayers, augmentVisLoader);
+
+ expect(mockDeleteFn).toHaveBeenCalledTimes(1);
+ });
+ it('multiple stale vislayers', async () => {
+ const mockDeleteFn = jest.fn();
+ const augmentVisObjs = [augmentVisObj1, augmentVisObj2];
+ const visLayers = [staleVisLayer1, staleVisLayer2];
+ const augmentVisLoader = createSavedAugmentVisLoader({
+ savedObjectsClient: {
+ ...getMockAugmentVisSavedObjectClient(augmentVisObjs),
+ delete: mockDeleteFn,
+ },
+ } as any) as SavedObjectLoaderAugmentVis;
+
+ cleanupStaleObjects(augmentVisObjs, visLayers, augmentVisLoader);
+
+ expect(mockDeleteFn).toHaveBeenCalledTimes(2);
+ });
+ it('stale and valid vislayers', async () => {
+ const mockDeleteFn = jest.fn();
+ const augmentVisObjs = [augmentVisObj1, augmentVisObj2];
+ const visLayers = [validVisLayer1, staleVisLayer2];
+ const augmentVisLoader = createSavedAugmentVisLoader({
+ savedObjectsClient: {
+ ...getMockAugmentVisSavedObjectClient(augmentVisObjs),
+ delete: mockDeleteFn,
+ },
+ } as any) as SavedObjectLoaderAugmentVis;
+
+ cleanupStaleObjects(augmentVisObjs, visLayers, augmentVisLoader);
+
+ expect(mockDeleteFn).toHaveBeenCalledTimes(1);
+ });
+ });
});
diff --git a/src/plugins/vis_augmenter/public/utils/utils.ts b/src/plugins/vis_augmenter/public/utils/utils.ts
index 9d683ff962c1..907867630cbe 100644
--- a/src/plugins/vis_augmenter/public/utils/utils.ts
+++ b/src/plugins/vis_augmenter/public/utils/utils.ts
@@ -11,12 +11,14 @@ import {
buildExpression,
ExpressionAstFunctionBuilder,
} from '../../../../plugins/expressions/public';
+import { SavedObjectLoader } from '../../../../plugins/saved_objects/public';
import {
ISavedAugmentVis,
SavedAugmentVisLoader,
VisLayerFunctionDefinition,
VisLayer,
isVisLayerWithError,
+ VisLayerErrorTypes,
} from '../';
import { PLUGIN_AUGMENTATION_ENABLE_SETTING } from '../../common/constants';
import { getUISettings } from '../services';
@@ -48,9 +50,8 @@ export const isEligibleForVisLayers = (vis: Vis): boolean => {
};
/**
- * Using a SavedAugmentVisLoader, fetch all saved objects that are of 'augment-vis' type
- * and filter out to return the ones associated to the particular vis via
- * matching vis ID.
+ * Using a SavedAugmentVisLoader, fetch all saved objects that are of 'augment-vis' type.
+ * Filter by vis ID.
*/
export const getAugmentVisSavedObjs = async (
visId: string | undefined,
@@ -66,14 +67,27 @@ export const getAugmentVisSavedObjs = async (
);
}
try {
- const resp = await loader?.findAll();
- const allSavedObjects = (get(resp, 'hits', []) as any[]) as ISavedAugmentVis[];
+ const allSavedObjects = await getAllAugmentVisSavedObjs(loader);
return allSavedObjects.filter((hit: ISavedAugmentVis) => hit.visId === visId);
} catch (e) {
return [] as ISavedAugmentVis[];
}
};
+/**
+ * Using a SavedAugmentVisLoader, fetch all saved objects that are of 'augment-vis' type.
+ */
+export const getAllAugmentVisSavedObjs = async (
+ loader: SavedAugmentVisLoader | undefined
+): Promise => {
+ try {
+ const resp = await loader?.findAll();
+ return (get(resp, 'hits', []) as any[]) as ISavedAugmentVis[];
+ } catch (e) {
+ return [] as ISavedAugmentVis[];
+ }
+};
+
/**
* Given an array of augment-vis saved objects that contain expression function details,
* construct a pipeline that will execute each of these expression functions.
@@ -130,3 +144,36 @@ export const getAnyErrors = (visLayers: VisLayer[], visTitle: string): Error | u
return undefined;
}
};
+
+/**
+ * Cleans up any stale saved objects caused by plugin resources being deleted. Kicks
+ * off an async call to delete the stale objs.
+ *
+ * @param augmentVisSavedObs the original augment-vis saved objs for this particular vis
+ * @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[],
+ loader: SavedAugmentVisLoader | undefined
+): void => {
+ const staleVisLayers = visLayers
+ .filter((visLayer) => isVisLayerWithError(visLayer))
+ .filter(
+ (visLayerWithError) => visLayerWithError.error?.type === VisLayerErrorTypes.RESOURCE_DELETED
+ );
+ if (!isEmpty(staleVisLayers)) {
+ const objIdsToDelete = [] as string[];
+ staleVisLayers.forEach((staleVisLayer) => {
+ // Match the VisLayer to its origin saved obj to extract the to-be-deleted saved obj ID
+ const deletedPluginResourceId = staleVisLayer.pluginResource.id;
+ const savedObjId = augmentVisSavedObjs.find(
+ (savedObj) => savedObj.pluginResource.id === deletedPluginResourceId
+ )?.id;
+ if (savedObjId !== undefined) objIdsToDelete.push(savedObjId);
+ });
+ loader?.delete(objIdsToDelete);
+ }
+};
diff --git a/src/plugins/vis_augmenter/public/view_events_flyout/actions/open_events_flyout.tsx b/src/plugins/vis_augmenter/public/view_events_flyout/actions/open_events_flyout.tsx
index cd7d90aedf11..c9f6d75e1190 100644
--- a/src/plugins/vis_augmenter/public/view_events_flyout/actions/open_events_flyout.tsx
+++ b/src/plugins/vis_augmenter/public/view_events_flyout/actions/open_events_flyout.tsx
@@ -3,19 +3,18 @@
* SPDX-License-Identifier: Apache-2.0
*/
import React from 'react';
-import { CoreStart } from 'src/core/public';
import { toMountPoint } from '../../../../opensearch_dashboards_react/public';
import { ViewEventsFlyout } from '../components';
import { VIEW_EVENTS_FLYOUT_STATE, setFlyoutState } from '../flyout_state';
+import { getCore } from '../../services';
interface Props {
- core: CoreStart;
savedObjectId: string;
}
export async function openViewEventsFlyout(props: Props) {
setFlyoutState(VIEW_EVENTS_FLYOUT_STATE.OPEN);
- const flyoutSession = props.core.overlays.openFlyout(
+ const flyoutSession = getCore().overlays.openFlyout(
toMountPoint(
{
diff --git a/src/plugins/vis_augmenter/public/view_events_flyout/actions/open_events_flyout_action.test.ts b/src/plugins/vis_augmenter/public/view_events_flyout/actions/open_events_flyout_action.test.ts
index 381cbcb2e453..e6cb654ab422 100644
--- a/src/plugins/vis_augmenter/public/view_events_flyout/actions/open_events_flyout_action.test.ts
+++ b/src/plugins/vis_augmenter/public/view_events_flyout/actions/open_events_flyout_action.test.ts
@@ -3,10 +3,9 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import { coreMock } from '../../../../../core/public/mocks';
-import { CoreStart } from 'opensearch-dashboards/public';
import { OpenEventsFlyoutAction } from './open_events_flyout_action';
import flyoutStateModule from '../flyout_state';
+import servicesModule from '../../services';
// Mocking the flyout state service. Defaulting to CLOSED. May override
// getFlyoutState() in below individual tests to test out different scenarios.
@@ -21,37 +20,46 @@ jest.mock('src/plugins/vis_augmenter/public/view_events_flyout/flyout_state', ()
};
});
-let coreStart: CoreStart;
-beforeEach(async () => {
- coreStart = coreMock.createStart();
+// Mocking core service as needed when making calls to the core's overlays service
+jest.mock('src/plugins/vis_augmenter/public/services.ts', () => {
+ return {
+ getCore: () => {
+ return {
+ overlays: {
+ openFlyout: () => {},
+ },
+ };
+ },
+ };
});
+
afterEach(async () => {
jest.clearAllMocks();
});
describe('OpenEventsFlyoutAction', () => {
it('is incompatible with null saved obj id', async () => {
- const action = new OpenEventsFlyoutAction(coreStart);
+ const action = new OpenEventsFlyoutAction();
const savedObjectId = null;
// @ts-ignore
expect(await action.isCompatible({ savedObjectId })).toBe(false);
});
it('is incompatible with undefined saved obj id', async () => {
- const action = new OpenEventsFlyoutAction(coreStart);
+ const action = new OpenEventsFlyoutAction();
const savedObjectId = undefined;
// @ts-ignore
expect(await action.isCompatible({ savedObjectId })).toBe(false);
});
it('is incompatible with empty saved obj id', async () => {
- const action = new OpenEventsFlyoutAction(coreStart);
+ const action = new OpenEventsFlyoutAction();
const savedObjectId = '';
expect(await action.isCompatible({ savedObjectId })).toBe(false);
});
it('execute throws error if incompatible saved obj id', async () => {
- const action = new OpenEventsFlyoutAction(coreStart);
+ const action = new OpenEventsFlyoutAction();
async function check(id: any) {
await action.execute({ savedObjectId: id });
}
@@ -64,10 +72,13 @@ describe('OpenEventsFlyoutAction', () => {
const getFlyoutStateSpy = jest
.spyOn(flyoutStateModule, 'getFlyoutState')
.mockImplementation(() => 'CLOSED');
+ // openFlyout exists within core.overlays service. We spy on the initial getCore() fn call indicating
+ // that openFlyout is getting called.
+ const openFlyoutStateSpy = jest.spyOn(servicesModule, 'getCore');
const savedObjectId = 'test-id';
- const action = new OpenEventsFlyoutAction(coreStart);
+ const action = new OpenEventsFlyoutAction();
await action.execute({ savedObjectId });
- expect(coreStart.overlays.openFlyout).toHaveBeenCalledTimes(1);
+ expect(openFlyoutStateSpy).toHaveBeenCalledTimes(1);
expect(getFlyoutStateSpy).toHaveBeenCalledTimes(1);
});
@@ -75,20 +86,21 @@ describe('OpenEventsFlyoutAction', () => {
const getFlyoutStateSpy = jest
.spyOn(flyoutStateModule, 'getFlyoutState')
.mockImplementation(() => 'OPEN');
+ const openFlyoutStateSpy = jest.spyOn(servicesModule, 'getCore');
const savedObjectId = 'test-id';
- const action = new OpenEventsFlyoutAction(coreStart);
+ const action = new OpenEventsFlyoutAction();
await action.execute({ savedObjectId });
- expect(coreStart.overlays.openFlyout).toHaveBeenCalledTimes(0);
+ expect(openFlyoutStateSpy).toHaveBeenCalledTimes(0);
expect(getFlyoutStateSpy).toHaveBeenCalledTimes(1);
});
it('Returns display name', async () => {
- const action = new OpenEventsFlyoutAction(coreStart);
+ const action = new OpenEventsFlyoutAction();
expect(action.getDisplayName()).toBeDefined();
});
it('Returns undefined icon type', async () => {
- const action = new OpenEventsFlyoutAction(coreStart);
+ const action = new OpenEventsFlyoutAction();
expect(action.getIconType()).toBeUndefined();
});
});
diff --git a/src/plugins/vis_augmenter/public/view_events_flyout/actions/open_events_flyout_action.ts b/src/plugins/vis_augmenter/public/view_events_flyout/actions/open_events_flyout_action.ts
index c141a361048a..cb47e5d6a85c 100644
--- a/src/plugins/vis_augmenter/public/view_events_flyout/actions/open_events_flyout_action.ts
+++ b/src/plugins/vis_augmenter/public/view_events_flyout/actions/open_events_flyout_action.ts
@@ -25,7 +25,7 @@ export class OpenEventsFlyoutAction implements Action {
public readonly id = OPEN_EVENTS_FLYOUT_ACTION;
public order = 1;
- constructor(private core: CoreStart) {}
+ constructor() {}
public getIconType() {
return undefined;
@@ -53,7 +53,6 @@ export class OpenEventsFlyoutAction implements Action {
// re-opening it.
if (getFlyoutState() === VIEW_EVENTS_FLYOUT_STATE.CLOSED) {
openViewEventsFlyout({
- core: this.core,
savedObjectId,
});
}
diff --git a/src/plugins/vis_augmenter/public/view_events_flyout/actions/view_events_option_action.test.ts b/src/plugins/vis_augmenter/public/view_events_flyout/actions/view_events_option_action.test.ts
index 64b75547d019..451087e48a15 100644
--- a/src/plugins/vis_augmenter/public/view_events_flyout/actions/view_events_option_action.test.ts
+++ b/src/plugins/vis_augmenter/public/view_events_flyout/actions/view_events_option_action.test.ts
@@ -3,11 +3,10 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import { coreMock } from '../../../../../core/public/mocks';
-import { CoreStart } from 'opensearch-dashboards/public';
import { ViewEventsOptionAction } from './view_events_option_action';
import { createMockErrorEmbeddable, createMockVisEmbeddable } from '../../mocks';
import flyoutStateModule from '../flyout_state';
+import servicesModule from '../../services';
// Mocking the flyout state service. Defaulting to CLOSED. May override
// getFlyoutState() in below individual tests to test out different scenarios.
@@ -24,6 +23,7 @@ jest.mock('src/plugins/vis_augmenter/public/view_events_flyout/flyout_state', ()
// Mocking the UISettings service. This is needed when making eligibility checks for the actions,
// which does UISettings checks to ensure the feature is enabled.
+// Also mocking core service as needed when making calls to the core's overlays service
jest.mock('src/plugins/vis_augmenter/public/services.ts', () => {
return {
getUISettings: () => {
@@ -40,40 +40,42 @@ jest.mock('src/plugins/vis_augmenter/public/services.ts', () => {
},
};
},
+ getCore: () => {
+ return {
+ overlays: {
+ openFlyout: () => {},
+ },
+ };
+ },
};
});
-let coreStart: CoreStart;
-
-beforeEach(async () => {
- coreStart = coreMock.createStart();
-});
afterEach(async () => {
jest.clearAllMocks();
});
describe('ViewEventsOptionAction', () => {
it('is incompatible with ErrorEmbeddables', async () => {
- const action = new ViewEventsOptionAction(coreStart);
+ const action = new ViewEventsOptionAction();
const errorEmbeddable = createMockErrorEmbeddable();
expect(await action.isCompatible({ embeddable: errorEmbeddable })).toBe(false);
});
it('is incompatible with VisualizeEmbeddable with invalid vis', async () => {
const visEmbeddable = createMockVisEmbeddable('test-saved-obj-id', 'test-title', false);
- const action = new ViewEventsOptionAction(coreStart);
+ const action = new ViewEventsOptionAction();
expect(await action.isCompatible({ embeddable: visEmbeddable })).toBe(false);
});
it('is compatible with VisualizeEmbeddable with valid vis', async () => {
const visEmbeddable = createMockVisEmbeddable('test-saved-obj-id', 'test-title');
- const action = new ViewEventsOptionAction(coreStart);
+ const action = new ViewEventsOptionAction();
expect(await action.isCompatible({ embeddable: visEmbeddable })).toBe(true);
});
it('execute throws error if incompatible embeddable', async () => {
const errorEmbeddable = createMockErrorEmbeddable();
- const action = new ViewEventsOptionAction(coreStart);
+ const action = new ViewEventsOptionAction();
async function check() {
await action.execute({ embeddable: errorEmbeddable });
}
@@ -84,10 +86,13 @@ describe('ViewEventsOptionAction', () => {
const getFlyoutStateSpy = jest
.spyOn(flyoutStateModule, 'getFlyoutState')
.mockImplementation(() => 'CLOSED');
+ // openFlyout exists within core.overlays service. We spy on the initial getCore() fn call indicating
+ // that openFlyout is getting called.
+ const openFlyoutStateSpy = jest.spyOn(servicesModule, 'getCore');
const visEmbeddable = createMockVisEmbeddable('test-saved-obj-id', 'test-title');
- const action = new ViewEventsOptionAction(coreStart);
+ const action = new ViewEventsOptionAction();
await action.execute({ embeddable: visEmbeddable });
- expect(coreStart.overlays.openFlyout).toHaveBeenCalledTimes(1);
+ expect(openFlyoutStateSpy).toHaveBeenCalledTimes(1);
expect(getFlyoutStateSpy).toHaveBeenCalledTimes(1);
});
@@ -95,20 +100,21 @@ describe('ViewEventsOptionAction', () => {
const getFlyoutStateSpy = jest
.spyOn(flyoutStateModule, 'getFlyoutState')
.mockImplementation(() => 'OPEN');
+ const openFlyoutStateSpy = jest.spyOn(servicesModule, 'getCore');
const visEmbeddable = createMockVisEmbeddable('test-saved-obj-id', 'test-title');
- const action = new ViewEventsOptionAction(coreStart);
+ const action = new ViewEventsOptionAction();
await action.execute({ embeddable: visEmbeddable });
- expect(coreStart.overlays.openFlyout).toHaveBeenCalledTimes(0);
+ expect(openFlyoutStateSpy).toHaveBeenCalledTimes(0);
expect(getFlyoutStateSpy).toHaveBeenCalledTimes(1);
});
it('Returns display name', async () => {
- const action = new ViewEventsOptionAction(coreStart);
+ const action = new ViewEventsOptionAction();
expect(action.getDisplayName()).toBeDefined();
});
it('Returns an icon type', async () => {
- const action = new ViewEventsOptionAction(coreStart);
+ const action = new ViewEventsOptionAction();
expect(action.getIconType()).toBeDefined();
});
});
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 1ad2a9c57fe2..975f6ac0cc4c 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
@@ -21,7 +21,7 @@ export class ViewEventsOptionAction implements Action {
public readonly id = VIEW_EVENTS_OPTION_ACTION;
public order = 1;
- constructor(private core: CoreStart) {}
+ constructor() {}
public getIconType(): EuiIconType {
return 'apmTrace';
@@ -52,7 +52,6 @@ export class ViewEventsOptionAction implements Action {
// re-opening it.
if (getFlyoutState() === VIEW_EVENTS_FLYOUT_STATE.CLOSED) {
openViewEventsFlyout({
- core: this.core,
savedObjectId,
});
}
diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts
index be6c832d3385..505c7b045cb4 100644
--- a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts
+++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts
@@ -72,16 +72,12 @@ import {
getAugmentVisSavedObjs,
buildPipelineFromAugmentVisSavedObjs,
getAnyErrors,
- VisLayerErrorTypes,
AugmentVisContext,
-} from '../../../vis_augmenter/public';
-import { VisSavedObject } from '../types';
-import {
- PointInTimeEventsVisLayer,
VisLayer,
- VisLayerTypes,
VisAugmenterEmbeddableConfig,
+ PLUGIN_RESOURCE_DELETE_TRIGGER,
} from '../../../vis_augmenter/public';
+import { VisSavedObject } from '../types';
const getKeys = (o: T): Array => Object.keys(o) as Array;
@@ -519,11 +515,6 @@ export class VisualizeEmbeddable
* Collects any VisLayers from plugin expressions functions
* by fetching all AugmentVisSavedObjects that match the vis
* saved object ID.
- *
- * TODO: final eligibility will be defined as part of a separate effort.
- * Right now we have a placeholder function isEligibleForVisLayers() which
- * is used below. For more details, see
- * https://github.com/opensearch-project/OpenSearch-Dashboards/issues/3268
*/
fetchVisLayers = async (): Promise => {
try {
@@ -558,6 +549,15 @@ export class VisualizeEmbeddable
expressionParams as Record
)) as ExprVisLayers;
const visLayers = exprVisLayers.layers;
+
+ // There may be some stale saved objs if any plugin resources have been deleted since last time
+ // data was fetched from them via the expression functions. Execute this trigger so any listening
+ // action can perform cleanup.
+ getUiActions().getTrigger(PLUGIN_RESOURCE_DELETE_TRIGGER).exec({
+ savedObjs: augmentVisSavedObjs,
+ visLayers,
+ });
+
const err = getAnyErrors(visLayers, this.vis.title);
// This is only true when one or more VisLayers has an error
if (err !== undefined) {
diff --git a/src/plugins/visualizations/public/index.ts b/src/plugins/visualizations/public/index.ts
index 957c9d8c80cd..6c8cf4ec51d2 100644
--- a/src/plugins/visualizations/public/index.ts
+++ b/src/plugins/visualizations/public/index.ts
@@ -76,3 +76,4 @@ export {
export { ExprVisAPIEvents } from './expressions/vis';
export { VisualizationListItem } from './vis_types/vis_type_alias_registry';
export { VISUALIZE_ENABLE_LABS_SETTING } from '../common/constants';
+export { createSavedVisLoader } from './saved_visualizations';
diff --git a/src/plugins/visualizations/public/plugin.ts b/src/plugins/visualizations/public/plugin.ts
index 07b40066018c..9b094c9ef754 100644
--- a/src/plugins/visualizations/public/plugin.ts
+++ b/src/plugins/visualizations/public/plugin.ts
@@ -182,6 +182,14 @@ export class VisualizationsPlugin
{ data, expressions, uiActions, embeddable, dashboard }: VisualizationsStartDeps
): VisualizationsStart {
const types = this.types.start();
+ const savedAugmentVisLoader = createSavedAugmentVisLoader({
+ savedObjectsClient: core.savedObjects.client,
+ indexPatterns: data.indexPatterns,
+ search: data.search,
+ chrome: core.chrome,
+ overlays: core.overlays,
+ });
+ setSavedAugmentVisLoader(savedAugmentVisLoader);
setI18n(core.i18n);
setTypes(types);
setEmbeddable(embeddable);
@@ -205,6 +213,7 @@ export class VisualizationsPlugin
chrome: core.chrome,
overlays: core.overlays,
visualizationTypes: types,
+ savedAugmentVisLoader,
});
setSavedVisualizationsLoader(savedVisualizationsLoader);
const savedSearchLoader = createSavedSearchesLoader({
@@ -214,14 +223,6 @@ export class VisualizationsPlugin
chrome: core.chrome,
overlays: core.overlays,
});
- const savedAugmentVisLoader = createSavedAugmentVisLoader({
- savedObjectsClient: core.savedObjects.client,
- indexPatterns: data.indexPatterns,
- search: data.search,
- chrome: core.chrome,
- overlays: core.overlays,
- });
- setSavedAugmentVisLoader(savedAugmentVisLoader);
setSavedSearchLoader(savedSearchLoader);
setNotifications(core.notifications);
return {
diff --git a/src/plugins/visualize/opensearch_dashboards.json b/src/plugins/visualize/opensearch_dashboards.json
index c898f7da3779..47573b58b9d2 100644
--- a/src/plugins/visualize/opensearch_dashboards.json
+++ b/src/plugins/visualize/opensearch_dashboards.json
@@ -19,6 +19,7 @@
"opensearchDashboardsReact",
"home",
"discover",
- "visDefaultEditor"
+ "visDefaultEditor",
+ "savedObjectsManagement"
]
}
diff --git a/src/plugins/visualize/public/application/components/visualize_listing.tsx b/src/plugins/visualize/public/application/components/visualize_listing.tsx
index 46db44bb066d..ec768e19885a 100644
--- a/src/plugins/visualize/public/application/components/visualize_listing.tsx
+++ b/src/plugins/visualize/public/application/components/visualize_listing.tsx
@@ -29,7 +29,7 @@
*/
import './visualize_listing.scss';
-
+import { get } from 'lodash';
import React, { useCallback, useRef, useMemo, useEffect } from 'react';
import { i18n } from '@osd/i18n';
import { useUnmount, useMount } from 'react-use';
@@ -43,6 +43,8 @@ import { VISUALIZE_ENABLE_LABS_SETTING } from '../../../../visualizations/public
import { VisualizeServices } from '../types';
import { VisualizeConstants } from '../visualize_constants';
import { getTableColumns, getNoItemsMessage } from '../utils';
+import { getUiActions } from '../../services';
+import { SAVED_OBJECT_DELETE_TRIGGER } from '../../../../saved_objects_management/public';
export const VisualizeListing = () => {
const {
@@ -134,15 +136,25 @@ export const VisualizeListing = () => {
const deleteItems = useCallback(
async (selectedItems: object[]) => {
+ const uiActions = getUiActions();
await Promise.all(
- selectedItems.map((item: any) => savedObjects.client.delete(item.savedObjectType, item.id))
- ).catch((error) => {
- toastNotifications.addError(error, {
- title: i18n.translate('visualize.visualizeListingDeleteErrorTitle', {
- defaultMessage: 'Error deleting visualization',
- }),
- });
- });
+ selectedItems.map((item: any) =>
+ savedObjects.client
+ .delete(item.savedObjectType, item.id)
+ .then(() => {
+ uiActions
+ .getTrigger(SAVED_OBJECT_DELETE_TRIGGER)
+ .exec({ type: item.savedObjectType, savedObjectId: item.id });
+ })
+ .catch((error) => {
+ toastNotifications.addError(error, {
+ title: i18n.translate('visualize.visualizeListingDeleteErrorTitle', {
+ defaultMessage: 'Error deleting visualization',
+ }),
+ });
+ })
+ )
+ );
},
[savedObjects.client, toastNotifications]
);
diff --git a/src/plugins/visualize/public/plugin.ts b/src/plugins/visualize/public/plugin.ts
index 297db26c48de..c146efef1fab 100644
--- a/src/plugins/visualize/public/plugin.ts
+++ b/src/plugins/visualize/public/plugin.ts
@@ -60,13 +60,14 @@ import { DEFAULT_APP_CATEGORIES } from '../../../core/public';
import { SavedObjectsStart } from '../../saved_objects/public';
import { EmbeddableStart } from '../../embeddable/public';
import { DashboardStart } from '../../dashboard/public';
-import { UiActionsSetup, VISUALIZE_FIELD_TRIGGER } from '../../ui_actions/public';
+import { UiActionsSetup, UiActionsStart, VISUALIZE_FIELD_TRIGGER } from '../../ui_actions/public';
import {
setUISettings,
setApplication,
setIndexPatterns,
setQueryService,
setShareService,
+ setUiActions,
} from './services';
import { visualizeFieldAction } from './actions/visualize_field_action';
import { createVisualizeUrlGenerator } from './url_generator';
@@ -80,6 +81,7 @@ export interface VisualizePluginStartDependencies {
urlForwarding: UrlForwardingStart;
savedObjects: SavedObjectsStart;
dashboard: DashboardStart;
+ uiActions: UiActionsStart;
}
export interface VisualizePluginSetupDependencies {
@@ -248,6 +250,7 @@ export class VisualizePlugin
if (plugins.share) {
setShareService(plugins.share);
}
+ setUiActions(plugins.uiActions);
}
stop() {
diff --git a/src/plugins/visualize/public/services.ts b/src/plugins/visualize/public/services.ts
index c0f359e8a002..ac367522ab7e 100644
--- a/src/plugins/visualize/public/services.ts
+++ b/src/plugins/visualize/public/services.ts
@@ -32,6 +32,7 @@ import { ApplicationStart, IUiSettingsClient } from '../../../core/public';
import { createGetterSetter } from '../../../plugins/opensearch_dashboards_utils/public';
import { IndexPatternsContract, DataPublicPluginStart } from '../../../plugins/data/public';
import { SharePluginStart } from '../../share/public';
+import { UiActionsStart } from '../../ui_actions/public';
export const [getUISettings, setUISettings] = createGetterSetter('UISettings');
@@ -46,3 +47,5 @@ export const [getIndexPatterns, setIndexPatterns] = createGetterSetter('Query');
+
+export const [getUiActions, setUiActions] = createGetterSetter('UIActions');