Skip to content

Commit

Permalink
[D&D] Enable basic saved object management (opensearch-project#1816)
Browse files Browse the repository at this point in the history
* [D&D] Enable basic saved object management

- Create README stub for saved_objects_management plugin
- Register wizard saved object loader with management plugin
- Add management methods to SavedObjectsType
- Add capabilities provider to wizard
- Add saved wizard vis SavedObjectClass and SavedObjectLoader
- Add public plugin start method

partially addresses opensearch-project#1620

Signed-off-by: Josh Romero <[email protected]>

* [Doc] Add clarifications to README

for save objects management plugin

Signed-off-by: Josh Romero <[email protected]>
  • Loading branch information
joshuarrrr committed Aug 1, 2022
1 parent b66abd7 commit 74eb899
Show file tree
Hide file tree
Showing 15 changed files with 199 additions and 23 deletions.
41 changes: 41 additions & 0 deletions src/plugins/saved_objects_management/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Save objects management

Provides a UI (via the `management` plugin) to find and manage all saved objects in one place (you can see the primary page by navigating to `/app/management/opensearch-dashboards/objects`). Not to be confused with the `savedObjects` plugin, which provides all the core capabilities of saved objects.

From the primary UI page, this plugin allows you to:
1. Search/view/delete saved objects and their relationships
2. Import/export saved objects
3. Inspect/edit raw saved object values without validation

For 3., this plugin can also be used to provide a route/page for editing, such as `/app/management/opensearch-dashboards/objects/savedVisualizations/{visualizationId}`, although plugins are alos free to provide or host alternate routes for this purpose (see index patterns, for instance, which provide their own integration and UI via the `management` plugin directly).
## Making a new saved object type manageable

1. Create a new `SavedObjectsType` or add the `management` property to an existing one. (See `SavedObjectsTypeManagementDefinition` for explanation of its properties: https://github.com/opensearch-project/OpenSearch-Dashboards/blob/e1380f14deb98cc7cce55c3b82c2d501826a78c3/src/core/server/saved_objects/types.ts#L247-L285)
2. Register saved object type via `core.savedObjects.registerType(...)` as part of plugin server setup method
3. Implement a way to save the object via `savedObjectsClient.create(...)`
4. After these steps, you should be able to save objects and view/search for them in Saved Objects management (`/app/management/opensearch-dashboards/objects`)

## Enabling edit links from saved objects management

1. Make sure `management.getInAppUrl` method of the `SavedObjectsType` is defined with a `path` (which will specify the link target) and the `uiCapabilitiesPath`
2. For `uiCapabilitiesPath` to work without additional hardcoding, it should be in the format `{plugin}.show`, so that [the default logic of `src/plugins/saved_objects_management/public/lib/in_app_url.ts`](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/a9984f63a38e964007ab94fae99237a14d8f9ee2/src/plugins/saved_objects_management/public/lib/in_app_url.ts#L48-L50) will correctly match. Otherwise, you'll need to [add a case for your `uiCapabilities` path](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/a9984f63a38e964007ab94fae99237a14d8f9ee2/src/plugins/saved_objects_management/public/lib/in_app_url.ts#L45-L47) to that function
3. Create default plugin capabilities provider
3. Register plugin capabilities via `core.capabilities.registerProvider(...);` as part of plugin server setup method

## Using saved objects management to inspect/edit new plugin objects

You'll notice that when clicking on the "Inspect" button from the saved objects management table, you'll usually be routed to something like `/app/management/opensearch-dashboards/objects/savedVisualizations/` (where the route itself is determined by the `management.getEditUrl` method of the `SavedObjectsType`). But to register a similar route for a new saved object type, you'll need to create a new `savedObjectLoader` and register it with the management plugin.

### Creating `savedObjectLoader`

1. In your plugin's public directory, create a class for your saved object that extends `SavedObjectClass`. The mapping should match the `mappings` defined in your `SavedObjectsType`.
2. Create a `savedObjectLoader` creation function that returns a `new SavedObjectLoader(YourSavedObjectClass, savedObjectsClient)`
3. Return that `savedObjectLoader` as part of your public plugin `start` method

### Registering

Ideally, we'd allow plugins to self-register their `savedObjectLoader` and (declare a dependency on this plugin). However, as currently implemented, any plugins that want this plugin to handle their inpect routes need to be added as optional dependencies and registered here.

1. Add your plugin to the `optionalPlugins` array in `./opensearch_dashboards.json`
2. Update the `StartDependencies` interface of this plugin to include the public plugin start type
3. Update `registerServices` to register a new type of `SavedObjectLoader`, where `id` will be the route, `title` will likely match your saved object type, and `service` is your `SavedObjectLoader` that is defined in your plugin start.
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"server": true,
"ui": true,
"requiredPlugins": ["management", "data"],
"optionalPlugins": ["dashboard", "visualizations", "discover", "home"],
"optionalPlugins": ["dashboard", "visualizations", "discover", "home", "wizard"],
"extraPublicDirs": ["public/lib"],
"requiredBundles": ["opensearchDashboardsReact", "home"]
}
3 changes: 3 additions & 0 deletions src/plugins/saved_objects_management/public/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@

import { i18n } from '@osd/i18n';
import { CoreSetup, CoreStart, Plugin } from 'src/core/public';

import { WizardStart } from '../../wizard/public';
import { ManagementSetup } from '../../management/public';
import { DataPublicPluginStart } from '../../data/public';
import { DashboardStart } from '../../dashboard/public';
Expand Down Expand Up @@ -69,6 +71,7 @@ export interface StartDependencies {
dashboard?: DashboardStart;
visualizations?: VisualizationsStart;
discover?: DiscoverStart;
wizard?: WizardStart;
}

export class SavedObjectsManagementPlugin
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export const registerServices = async (
registry: ISavedObjectsManagementServiceRegistry,
getStartServices: StartServicesAccessor<StartDependencies, SavedObjectsManagementPluginStart>
) => {
const [, { dashboard, visualizations, discover }] = await getStartServices();
const [, { dashboard, visualizations, discover, wizard }] = await getStartServices();

if (dashboard) {
registry.register({
Expand All @@ -61,4 +61,12 @@ export const registerServices = async (
service: discover.savedSearchLoader,
});
}

if (wizard) {
registry.register({
id: 'savedWizard',
title: 'wizard',
service: wizard.savedWizardLoader,
});
}
};
2 changes: 1 addition & 1 deletion src/plugins/wizard/public/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@ import { WizardPlugin } from './plugin';
export function plugin(initializerContext: PluginInitializerContext) {
return new WizardPlugin(initializerContext);
}
export { WizardServices, WizardPluginStartDependencies } from './types';
export { WizardServices, WizardPluginStartDependencies, WizardStart } from './types';
19 changes: 17 additions & 2 deletions src/plugins/wizard/public/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,17 @@ import {
WizardPluginStartDependencies,
WizardServices,
WizardSetup,
WizardStart,
} from './types';
import { PLUGIN_NAME } from '../common';
import { TypeService } from './services/type_service';
import { getPreloadedStore } from './application/utils/state_management';
import { setAggService, setIndexPatterns } from './plugin_services';
import { createSavedWizardLoader } from './saved_visualizations';

export class WizardPlugin
implements
Plugin<WizardSetup, void, WizardPluginSetupDependencies, WizardPluginStartDependencies> {
Plugin<WizardSetup, WizardStart, WizardPluginSetupDependencies, WizardPluginStartDependencies> {
private typeService = new TypeService();

constructor(public initializerContext: PluginInitializerContext) {}
Expand Down Expand Up @@ -101,7 +103,20 @@ export class WizardPlugin
};
}

public start(core: CoreStart) {}
public start(core: CoreStart, { data }: WizardPluginStartDependencies): WizardStart {
const typeService = this.typeService;

return {
...typeService.start(),
savedWizardLoader: createSavedWizardLoader({
savedObjectsClient: core.savedObjects.client,
indexPatterns: data.indexPatterns,
search: data.search,
chrome: core.chrome,
overlays: core.overlays,
}),
};
}

public stop() {}
}
53 changes: 53 additions & 0 deletions src/plugins/wizard/public/saved_visualizations/_saved_vis.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import {
createSavedObjectClass,
SavedObjectOpenSearchDashboardsServices,
} from '../../../saved_objects/public';

export function createSavedWizardVisClass(services: SavedObjectOpenSearchDashboardsServices) {
const SavedObjectClass = createSavedObjectClass(services);

class SavedWizardVis extends SavedObjectClass {
public static type = 'wizard';

// if type:wizard has no mapping, we push this mapping into OpenSearch
public static mapping = {
title: 'text',
description: 'text',
state: 'text',
// savedSearchId: 'keyword',
// version: 'integer',
};

// Order these fields to the top, the rest are alphabetical
static fieldOrder = ['title', 'description'];

// ID is optional, without it one will be generated on save.
constructor(id: string) {
super({
type: SavedWizardVis.type,
mapping: SavedWizardVis.mapping,

// if this is null/undefined then the SavedObject will be assigned the defaults
id,

// default values that will get assigned if the doc is new
defaults: {
title: '',
description: '',
state: '{}',
// savedSearchId,
// version: 1,
},
});
this.showInRecentlyAccessed = true;
this.getFullPath = () => `/app/wizard#/edit/${this.id}`;
}
}

return SavedWizardVis;
}
6 changes: 6 additions & 0 deletions src/plugins/wizard/public/saved_visualizations/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

export * from './saved_visualizations';
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import {
SavedObjectLoader,
SavedObjectOpenSearchDashboardsServices,
} from '../../../saved_objects/public';
import { createSavedWizardVisClass } from './_saved_vis';

export type SavedWizardLoader = ReturnType<typeof createSavedWizardLoader>;
export function createSavedWizardLoader(services: SavedObjectOpenSearchDashboardsServices) {
const { savedObjectsClient } = services;
const SavedWizardVisClass = createSavedWizardVisClass(services);

return new SavedObjectLoader(SavedWizardVisClass, savedObjectsClient);
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
* under the License.
*/

import { CoreService } from 'src/core/types';
import { CoreService } from '../../../../../core/types';
import { VisualizationTypeOptions } from './types';
import { VisualizationType } from './visualization_type';

Expand Down
18 changes: 11 additions & 7 deletions src/plugins/wizard/public/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,21 @@
* SPDX-License-Identifier: Apache-2.0
*/

import { SavedObjectsStart } from 'src/plugins/saved_objects/public';
import { AppMountParameters, CoreStart, ToastsStart } from 'opensearch-dashboards/public';
import { EmbeddableSetup } from 'src/plugins/embeddable/public';
import { DashboardStart } from 'src/plugins/dashboard/public';
import { VisualizationsSetup } from 'src/plugins/visualizations/public';
import { ExpressionsStart } from 'src/plugins/expressions/public';
import { SavedObjectsStart } from '../../saved_objects/public';
import { EmbeddableSetup } from '../../embeddable/public';
import { DashboardStart } from '../../dashboard/public';
import { VisualizationsSetup } from '../../visualizations/public';
import { ExpressionsStart } from '../../expressions/public';
import { NavigationPublicPluginStart } from '../../navigation/public';
import { DataPublicPluginStart, IndexPatternField } from '../../data/public';
import { DataPublicPluginStart } from '../../data/public';
import { TypeServiceSetup, TypeServiceStart } from './services/type_service';
import { SavedObjectLoader } from '../../saved_objects/public';
import { AppMountParameters, CoreStart, ToastsStart } from '../../../core/public';

export type WizardSetup = TypeServiceSetup;
export interface WizardStart extends TypeServiceStart {
savedWizardLoader: SavedObjectLoader;
}

export interface WizardPluginSetupDependencies {
embeddable: EmbeddableSetup;
Expand Down
17 changes: 17 additions & 0 deletions src/plugins/wizard/server/capabilities_provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

export const capabilitiesProvider = () => ({
wizard: {
// TODO: investigate which capabilities we need to provide
// createNew: true,
// createShortUrl: true,
// delete: true,
show: true,
// showWriteControls: true,
// save: true,
// saveQuery: true,
},
});
12 changes: 8 additions & 4 deletions src/plugins/wizard/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ import {
} from '../../../core/server';

import { WizardPluginSetup, WizardPluginStart } from './types';
import { capabilitiesProvider } from './capabilities_provider';
import { defineRoutes } from './routes';
import { wizardApp } from './saved_objects';
import { wizardSavedObjectType } from './saved_objects';

export class WizardPlugin implements Plugin<WizardPluginSetup, WizardPluginStart> {
private readonly logger: Logger;
Expand All @@ -22,20 +23,23 @@ export class WizardPlugin implements Plugin<WizardPluginSetup, WizardPluginStart
this.logger = initializerContext.logger.get();
}

public setup({ http, savedObjects }: CoreSetup) {
public setup({ capabilities, http, savedObjects }: CoreSetup) {
this.logger.debug('wizard: Setup');
const router = http.createRouter();

// Register server side APIs
defineRoutes(router);

// Register saved object types
savedObjects.registerType(wizardApp);
savedObjects.registerType(wizardSavedObjectType);

// Register capabilities
capabilities.registerProvider(capabilitiesProvider);

return {};
}

public start(core: CoreStart) {
public start(_core: CoreStart) {
this.logger.debug('wizard: Started');
return {};
}
Expand Down
2 changes: 1 addition & 1 deletion src/plugins/wizard/server/saved_objects/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
* SPDX-License-Identifier: Apache-2.0
*/

export { wizardApp } from './wizard_app';
export { wizardSavedObjectType } from './wizard_app';
17 changes: 12 additions & 5 deletions src/plugins/wizard/server/saved_objects/wizard_app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,26 @@
* SPDX-License-Identifier: Apache-2.0
*/

import { SavedObjectsType } from 'src/core/server';
import { WIZARD_SAVED_OBJECT } from '../../common';
import { SavedObject, SavedObjectsType } from '../../../../core/server';
import { WizardSavedObjectAttributes, WIZARD_SAVED_OBJECT } from '../../common';

export const wizardApp: SavedObjectsType = {
export const wizardSavedObjectType: SavedObjectsType = {
name: WIZARD_SAVED_OBJECT,
hidden: false,
namespaceType: 'single',
management: {
icon: 'visVisualBuilder', // TODO: Need a custom icon here
defaultSearchField: 'title',
importableAndExportable: true,
getTitle: (obj: { attributes: { title: string } }) => obj.attributes.title,
// getInAppUrl: TODO: Enable once editing is supported
getTitle: ({ attributes: { title } }: SavedObject<WizardSavedObjectAttributes>) => title,
getEditUrl: ({ id }: SavedObject) =>
`/management/opensearch-dashboards/objects/savedWizard/${encodeURIComponent(id)}`,
getInAppUrl({ id }: SavedObject) {
return {
path: `/app/wizard#/edit/${encodeURIComponent(id)}`,
uiCapabilitiesPath: 'wizard.show',
};
},
},
migrations: {},
mappings: {
Expand Down

0 comments on commit 74eb899

Please sign in to comment.